diff --git a/shared/echotron/.github/workflows/build.yml b/shared/echotron/.github/workflows/build.yml
new file mode 100644
index 0000000..0e686f2
--- /dev/null
+++ b/shared/echotron/.github/workflows/build.yml
@@ -0,0 +1,20 @@
+---
+name: Build
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ go-version: [1.19.x]
+ os: [ubuntu-latest, macos-latest, windows-latest]
+
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/setup-go@v3
+ with:
+ go-version: ${{ matrix.go-version }}
+
+ - uses: actions/checkout@v3
+ - run: go build
diff --git a/shared/echotron/.github/workflows/test.yml b/shared/echotron/.github/workflows/test.yml
new file mode 100644
index 0000000..2404dea
--- /dev/null
+++ b/shared/echotron/.github/workflows/test.yml
@@ -0,0 +1,25 @@
+---
+name: Test
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ go-version: [1.19.x]
+ os: [ubuntu-latest]
+
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/setup-go@v3
+ with:
+ go-version: ${{ matrix.go-version }}
+
+ - uses: actions/checkout@v3
+ - run: go test -coverprofile=coverage.out -covermode=atomic
+
+ - uses: codecov/codecov-action@v4
+ with:
+ files: ./coverage.out
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/shared/echotron/COPYING.LESSER b/shared/echotron/COPYING.LESSER
new file mode 100644
index 0000000..65c5ca8
--- /dev/null
+++ b/shared/echotron/COPYING.LESSER
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/shared/echotron/LICENSE b/shared/echotron/LICENSE
new file mode 100644
index 0000000..9ff5cb5
--- /dev/null
+++ b/shared/echotron/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ echotron
+ Copyright (C) 2019 Nicolò Santamaria
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ echotron Copyright (C) 2019 Nicolò Santamaria
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/shared/echotron/README.md b/shared/echotron/README.md
new file mode 100644
index 0000000..bff8f61
--- /dev/null
+++ b/shared/echotron/README.md
@@ -0,0 +1,339 @@
+| 
[](https://golang.org/) [](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [](https://codecov.io/gh/NicoNex/echotron) [](https://github.com/NicoNex/echotron/blob/master/LICENSE) [](https://github.com/avelino/awesome-go) [](https://t.me/echotronnews) |
+| :------: |
+
+**Echotron** is an elegant and concurrent library for the Telegram bot API in Go.
+
+Fetch with
+
+```bash
+go get github.com/NicoNex/echotron/v3
+```
+
+## Example
+### Simplest implementations
+#### Long polling
+```golang
+package main
+
+import "github.com/NicoNex/echotron/v3"
+
+const token = "MY TELEGRAM TOKEN"
+
+func main() {
+ api := echotron.NewAPI(token)
+
+ for u := range echotron.PollingUpdates(token) {
+ if u.Message.Text == "/start" {
+ api.SendMessage("Hello world", u.ChatID(), nil)
+ }
+ }
+}
+```
+#### Webhook
+```golang
+package main
+
+import "github.com/NicoNex/echotron/v3"
+
+const token = "MY TELEGRAM TOKEN"
+
+func main() {
+ api := echotron.NewAPI(token)
+
+ for u := range echotron.WebhookUpdates("https://example.com:443/my_token", token) {
+ if u.Message.Text == "/start" {
+ api.SendMessage("Hello world", u.ChatID(), nil)
+ }
+ }
+}
+```
+For more scalable and recommended implementations see the other examples.
+
+### Long Polling
+
+```golang
+package main
+
+import (
+ "log"
+ "time"
+
+ "github.com/NicoNex/echotron/v3"
+)
+
+// Struct useful for managing internal states in your bot, but it could be of
+// any type such as `type bot int64` if you only need to store the chatID.
+type bot struct {
+ chatID int64
+ echotron.API
+}
+
+const token = "MY TELEGRAM TOKEN"
+
+// This function needs to be of type 'echotron.NewBotFn' and is called by
+// the echotron dispatcher upon any new message from a chatID that has never
+// interacted with the bot before.
+// This means that echotron keeps one instance of the echotron.Bot implementation
+// for each chat where the bot is used.
+func newBot(chatID int64) echotron.Bot {
+ return &bot{
+ chatID,
+ echotron.NewAPI(token),
+ }
+}
+
+// This method is needed to implement the echotron.Bot interface.
+func (b *bot) Update(update *echotron.Update) {
+ if update.Message.Text == "/start" {
+ b.SendMessage("Hello world", b.chatID, nil)
+ }
+}
+
+func main() {
+ // This is the entry point of echotron library.
+ dsp := echotron.NewDispatcher(token, newBot)
+ for {
+ log.Println(dsp.Poll())
+ // In case of connection issues wait 5 seconds before trying to reconnect.
+ time.Sleep(5 * time.Second)
+ }
+}
+```
+
+## Design
+
+**Echotron** makes a new instance of the struct bot for each open chat with a Telegram user, channel or group.
+This allows to:
+- safely call the `Update(*echotron.Update)` method concurrently
+- give to the user a convenient way to manage the bot internal states across all the chats
+- make sure that, even if one instance of the bot is deadlocked, the other ones keep running just fine, making the bot work for other users without any issues and/or slowdowns.
+
+Please note that the the aforementioned behaviour is dictated by the `echotron.Dispatcher` object whose usage is not mandatory and for special needs can be ignored and implemented in different ways still keeping all the methods in the `echotron.API` object.
+
+**Echotron** is designed to be as similar to the official [Telegram API](https://core.telegram.org/bots/api) as possible, but there are some things to take into account before starting to work with this library.
+
+- The methods have the exact same name, but with a capital first letter, since in Go methods have to start with a capital letter to be exported.
+_Example: `sendMessage` becomes `SendMessage`_
+- The order of the parameters in some methods is different than in the official Telegram API, so refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for the correct one.
+- The only `chat_id` (or, in this case, `chatID`) type supported is `int64`, instead of the "Integer or String" requirement of the official API. That's because numeric IDs can't change in any way, which isn't the case with text-based usernames.
+- In some methods, you might find a `InputFile` type parameter. [`InputFile`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#InputFile) is a struct with unexported fields, since only three combination of fields are valid, which can be obtained through the methods [`NewInputFileID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileID), [`NewInputFilePath`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFilePath) and [`NewInputFileBytes`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileBytes).
+- In some methods, you might find a `MessageIDOptions` type parameter. [`MessageIDOptions`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#MessageIDOptions) is another struct with unexported fields, since only two combination of field are valid, which can be obtained through the methods [`NewMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewMessageID) and [`NewInlineMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInlineMessageID).
+- Optional parameters can be added by passing the correct struct to each method that might request optional parameters. If you don't want to pass any optional parameter, `nil` is more than enough. Refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) to check for each method's optional parameters struct: it's the type of the `opts` parameter.
+- Some parameters are hardcoded to avoid putting random stuff which isn't recognized by the Telegram API. Some notable examples are [`ParseMode`](https://github.com/NicoNex/echotron/blob/master/options.go#L21), [`ChatAction`](https://github.com/NicoNex/echotron/blob/master/options.go#L54) and [`InlineQueryType`](https://github.com/NicoNex/echotron/blob/master/inline.go#L27). For a full list of custom hardcoded parameters, refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for each custom type: by clicking on the type's name, you'll get the source which contains the possible values for that type.
+
+## Additional examples
+### Functional approach to state management
+```golang
+package main
+
+import (
+ "log"
+ "strings"
+
+ "github.com/NicoNex/echotron/v3"
+)
+
+// Recursive type definition of the bot state function.
+type stateFn func(*echotron.Update) stateFn
+
+type bot struct {
+ chatID int64
+ state stateFn
+ name string
+ echotron.API
+}
+
+const token = "MY TELEGRAM TOKEN"
+
+func newBot(chatID int64) echotron.Bot {
+ bot := &bot{
+ chatID: chatID,
+ API: echotron.NewAPI(token),
+ }
+ // We set the default state to the bot.handleMessage method.
+ bot.state = bot.handleMessage
+ return bot
+}
+
+func (b *bot) Update(update *echotron.Update) {
+ // Here we execute the current state and set the next one.
+ b.state = b.state(update)
+}
+
+func (b *bot) handleMessage(update *echotron.Update) stateFn {
+ if strings.HasPrefix(update.Message.Text, "/set_name") {
+ b.SendMessage("Send me my new name!", b.chatID, nil)
+ // Here we return b.handleName since next time we receive a message it
+ // will be the new name.
+ return b.handleName
+ }
+ return b.handleMessage
+}
+
+func (b *bot) handleName(update *echotron.Update) stateFn {
+ b.name = update.Message.Text
+ b.SendMessage(fmt.Sprintf("My new name is %q", b.name), b.chatID, nil)
+ // Here we return b.handleMessage since the next time we receive a message
+ // it will be handled in the default way.
+ return b.handleMessage
+}
+
+func main() {
+ dsp := echotron.NewDispatcher(token, newBot)
+ log.Println(dsp.Poll())
+}
+```
+
+### Self destruction for lower memory footprint
+```golang
+package main
+
+import (
+ "log"
+ "time"
+
+ "github.com/NicoNex/echotron/v3"
+)
+
+type bot struct {
+ chatID int64
+ echotron.API
+}
+
+const token = "MY TELEGRAM TOKEN"
+
+var dsp *echotron.Dispatcher
+
+func newBot(chatID int64) echotron.Bot {
+ bot := &bot{
+ chatID,
+ echotron.NewAPI(token),
+ }
+ go bot.selfDestruct(time.After(time.Hour))
+ return bot
+}
+
+func (b *bot) selfDestruct(timech <-chan time.Time) {
+ <-timech
+ b.SendMessage("goodbye", b.chatID, nil)
+ dsp.DelSession(b.chatID)
+}
+
+func (b *bot) Update(update *echotron.Update) {
+ if update.Message.Text == "/start" {
+ b.SendMessage("Hello world", b.chatID, nil)
+ }
+}
+
+func main() {
+ dsp = echotron.NewDispatcher(token, newBot)
+ log.Println(dsp.Poll())
+}
+```
+
+### Webhook
+
+```golang
+package main
+
+import "github.com/NicoNex/echotron/v3"
+
+type bot struct {
+ chatID int64
+ echotron.API
+}
+
+const token = "MY TELEGRAM TOKEN"
+
+func newBot(chatID int64) echotron.Bot {
+ return &bot{
+ chatID,
+ echotron.NewAPI(token),
+ }
+}
+
+func (b *bot) Update(update *echotron.Update) {
+ if update.Message.Text == "/start" {
+ b.SendMessage("Hello world", b.chatID, nil)
+ }
+}
+
+func main() {
+ dsp := echotron.NewDispatcher(token, newBot)
+ dsp.ListenWebhook("https://example.com:443/my_bot_token")
+}
+```
+
+
+### Webhook with a custom http.Server
+
+This is an example for a custom http.Server which handles your own specified routes
+and also the webhook route which is specified by ListenWebhook.
+
+```golang
+package main
+
+import (
+ "github.com/NicoNex/echotron/v3"
+
+ "context"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+)
+
+type bot struct {
+ chatID int64
+ echotron.API
+}
+
+const token = "MY TELEGRAM TOKEN"
+
+func newBot(chatID int64) echotron.Bot {
+ return &bot{
+ chatID,
+ echotron.NewAPI(token),
+ }
+}
+
+func (b *bot) Update(update *echotron.Update) {
+ if update.Message.Text == "/start" {
+ b.SendMessage("Hello world", b.chatID, nil)
+ }
+}
+
+func main() {
+ termChan := make(chan os.Signal, 1) // Channel for terminating the app via os.Interrupt signal
+ signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
+ // Handle user login
+ })
+ mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
+ // Handle user logout
+ })
+ mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
+ // Tell something about your awesome telegram bot
+ })
+
+ // Set custom http.Server
+ server := &http.Server{Addr: ":8080", Handler: mux}
+
+ go func() {
+ <-termChan
+ // Perform some cleanup..
+ if err := server.Shutdown(context.Background()); err != nil {
+ log.Print(err)
+ }
+ }()
+
+ // Capture the interrupt signal for app termination handling
+ dsp := echotron.NewDispatcher(token, newBot)
+ dsp.SetHTTPServer(server)
+ // Start your custom http.Server with a registered /my_bot_token handler.
+ log.Println(dsp.ListenWebhook("https://example.com/my_bot_token"))
+}
+```
diff --git a/shared/echotron/api.go b/shared/echotron/api.go
new file mode 100644
index 0000000..6e1f4ea
--- /dev/null
+++ b/shared/echotron/api.go
@@ -0,0 +1,1097 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+)
+
+// API is the object that contains all the functions that wrap those of the Telegram Bot API.
+type API struct {
+ token string
+ base string
+}
+
+// NewAPI returns a new API object.
+func NewAPI(token string) API {
+ return API{
+ token: token,
+ base: fmt.Sprintf("https://api.telegram.org/bot%s/", token),
+ }
+}
+
+// NewLocalAPI is like NewAPI but allows to use a local API server.
+func NewLocalAPI(url, token string) API {
+ return API{
+ token: token,
+ base: url,
+ }
+}
+
+// GetUpdates is used to receive incoming updates using long polling.
+func (a API) GetUpdates(opts *UpdateOptions) (res APIResponseUpdate, err error) {
+ return res, client.get(a.base, "getUpdates", urlValues(opts), &res)
+}
+
+// SetWebhook is used to specify a url and receive incoming updates via an outgoing webhook.
+func (a API) SetWebhook(webhookURL string, dropPendingUpdates bool, opts *WebhookOptions) (res APIResponseBase, err error) {
+ var (
+ vals = make(url.Values)
+ keyVal = map[string]string{"url": webhookURL}
+ )
+
+ url, err := url.JoinPath(a.base, "setWebhook")
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("drop_pending_updates", btoa(dropPendingUpdates))
+ addValues(vals, opts)
+ url = fmt.Sprintf("%s?%s", strings.TrimSuffix(url, "/"), vals.Encode())
+
+ cnt, err := client.doPostForm(url, keyVal)
+ if err != nil {
+ return
+ }
+
+ if err = json.Unmarshal(cnt, &res); err != nil {
+ return
+ }
+
+ err = check(res)
+ return
+}
+
+// DeleteWebhook is used to remove webhook integration if you decide to switch back to GetUpdates.
+func (a API) DeleteWebhook(dropPendingUpdates bool) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+ vals.Set("drop_pending_updates", btoa(dropPendingUpdates))
+
+ return res, client.get(a.base, "deleteWebhook", vals, &res)
+}
+
+// GetWebhookInfo is used to get current webhook status.
+func (a API) GetWebhookInfo() (res APIResponseWebhook, err error) {
+ return res, client.get(a.base, "getWebhookInfo", nil, &res)
+}
+
+// GetMe is a simple method for testing your bot's auth token.
+func (a API) GetMe() (res APIResponseUser, err error) {
+ return res, client.get(a.base, "getMe", nil, &res)
+}
+
+// LogOut is used to log out from the cloud Bot API server before launching the bot locally.
+// You MUST log out the bot before running it locally, otherwise there is no guarantee that the bot will receive updates.
+// After a successful call, you can immediately log in on a local server,
+// but will not be able to log in back to the cloud Bot API server for 10 minutes.
+func (a API) LogOut() (res APIResponseBool, err error) {
+ return res, client.get(a.base, "logOut", nil, &res)
+}
+
+// Close is used to close the bot instance before moving it from one local server to another.
+// You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart.
+// The method will return error 429 in the first 10 minutes after the bot is launched.
+func (a API) Close() (res APIResponseBool, err error) {
+ return res, client.get(a.base, "close", nil, &res)
+}
+
+// SendMessage is used to send text messages.
+func (a API) SendMessage(text string, chatID int64, opts *MessageOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("text", text)
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "sendMessage", addValues(vals, opts), &res)
+}
+
+// ForwardMessage is used to forward messages of any kind.
+// Service messages can't be forwarded.
+func (a API) ForwardMessage(chatID, fromChatID int64, messageID int, opts *ForwardOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("from_chat_id", itoa(fromChatID))
+ vals.Set("message_id", itoa(int64(messageID)))
+ return res, client.get(a.base, "forwardMessage", addValues(vals, opts), &res)
+}
+
+// ForwardMessages is used to forward multiple messages of any kind.
+// If some of the specified messages can't be found or forwarded, they are skipped.
+// Service messages and messages with protected content can't be forwarded.
+// Album grouping is kept for forwarded messages.
+func (a API) ForwardMessages(chatID, fromChatID int64, messageIDs []int, opts *ForwardOptions) (res APIResponseMessageIDs, err error) {
+ var vals = make(url.Values)
+
+ msgIDs, err := json.Marshal(messageIDs)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("from_chat_id", itoa(fromChatID))
+ vals.Set("message_ids", string(msgIDs))
+ return res, client.get(a.base, "forwardMessages", addValues(vals, opts), &res)
+}
+
+// CopyMessage is used to copy messages of any kind.
+// Service messages, paid media mesages, giveaway messages, giveaway winners messages, and invoice messages can't be copied.
+// The method is analogous to the method ForwardMessage,
+// but the copied message doesn't have a link to the original message.
+func (a API) CopyMessage(chatID, fromChatID int64, messageID int, opts *CopyOptions) (res APIResponseMessageID, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("from_chat_id", itoa(fromChatID))
+ vals.Set("message_id", itoa(int64(messageID)))
+ return res, client.get(a.base, "copyMessage", addValues(vals, opts), &res)
+}
+
+// CopyMessages is used to copy messages of any kind.
+// If some of the specified messages can't be found or copied, they are skipped.
+// Service messages, paid media mesages, giveaway messages, giveaway winners messages, and invoice messages can't be copied.
+// A quiz poll can be copied only if the value of the field correct_option_id is known to the bot.
+// The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message.
+// Album grouping is kept for copied messages.
+func (a API) CopyMessages(chatID, fromChatID int64, messageIDs []int, opts *CopyMessagesOptions) (res APIResponseMessageIDs, err error) {
+ var vals = make(url.Values)
+
+ msgIDs, err := json.Marshal(messageIDs)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("from_chat_id", itoa(fromChatID))
+ vals.Set("message_ids", string(msgIDs))
+ return res, client.get(a.base, "copyMessages", addValues(vals, opts), &res)
+}
+
+// SendPhoto is used to send photos.
+func (a API) SendPhoto(file InputFile, chatID int64, opts *PhotoOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendPhoto", "photo", file, InputFile{}, addValues(vals, opts), &res)
+}
+
+// SendAudio is used to send audio files,
+// if you want Telegram clients to display them in the music player.
+// Your audio must be in the .MP3 or .M4A format.
+func (a API) SendAudio(file InputFile, chatID int64, opts *AudioOptions) (res APIResponseMessage, err error) {
+ var (
+ thumbnail InputFile
+ vals = make(url.Values)
+ )
+
+ if opts != nil {
+ thumbnail = opts.Thumbnail
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendAudio", "audio", file, thumbnail, addValues(vals, opts), &res)
+}
+
+// SendDocument is used to send general files.
+func (a API) SendDocument(file InputFile, chatID int64, opts *DocumentOptions) (res APIResponseMessage, err error) {
+ var (
+ thumbnail InputFile
+ vals = make(url.Values)
+ )
+
+ if opts != nil {
+ thumbnail = opts.Thumbnail
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendDocument", "document", file, thumbnail, addValues(vals, opts), &res)
+}
+
+// SendVideo is used to send video files.
+// Telegram clients support mp4 videos (other formats may be sent with SendDocument).
+func (a API) SendVideo(file InputFile, chatID int64, opts *VideoOptions) (res APIResponseMessage, err error) {
+ var (
+ thumbnail InputFile
+ vals = make(url.Values)
+ )
+
+ if opts != nil {
+ thumbnail = opts.Thumbnail
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendVideo", "video", file, thumbnail, addValues(vals, opts), &res)
+}
+
+// SendAnimation is used to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
+func (a API) SendAnimation(file InputFile, chatID int64, opts *AnimationOptions) (res APIResponseMessage, err error) {
+ var (
+ thumbnail InputFile
+ vals = make(url.Values)
+ )
+
+ if opts != nil {
+ thumbnail = opts.Thumbnail
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendAnimation", "animation", file, thumbnail, addValues(vals, opts), &res)
+}
+
+// SendVoice is used to send audio files, if you want Telegram clients to display the file as a playable voice message.
+// For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as Audio or Document).
+func (a API) SendVoice(file InputFile, chatID int64, opts *VoiceOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendVoice", "voice", file, InputFile{}, addValues(vals, opts), &res)
+}
+
+// SendVideoNote is used to send video messages.
+func (a API) SendVideoNote(file InputFile, chatID int64, opts *VideoNoteOptions) (res APIResponseMessage, err error) {
+ var (
+ thumbnail InputFile
+ vals = make(url.Values)
+ )
+
+ if opts != nil {
+ thumbnail = opts.Thumbnail
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "sendVideoNote", "video_note", file, thumbnail, addValues(vals, opts), &res)
+}
+
+// SendPaidMedia is used to send paid media to channel chats.
+func (a API) SendPaidMedia(chatID int64, starCount int64, media []GroupableInputMedia, opts *PaidMediaOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("star_count", itoa(starCount))
+ return res, client.postMedia(a.base, "sendPaidMedia", false, addValues(vals, opts), &res, toInputMedia(media)...)
+}
+
+// SendMediaGroup is used to send a group of photos, videos, documents or audios as an album.
+// Documents and audio files can be only grouped in an album with messages of the same type.
+func (a API) SendMediaGroup(chatID int64, media []GroupableInputMedia, opts *MediaGroupOptions) (res APIResponseMessageArray, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postMedia(a.base, "sendMediaGroup", false, addValues(vals, opts), &res, toInputMedia(media)...)
+}
+
+// SendLocation is used to send point on the map.
+func (a API) SendLocation(chatID int64, latitude, longitude float64, opts *LocationOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("latitude", ftoa(latitude))
+ vals.Set("longitude", ftoa(longitude))
+ return res, client.get(a.base, "sendLocation", addValues(vals, opts), &res)
+}
+
+// EditMessageLiveLocation is used to edit live location messages.
+// A location can be edited until its `LivePeriod` expires or editing is explicitly disabled by a call to `StopMessageLiveLocation`.
+func (a API) EditMessageLiveLocation(msg MessageIDOptions, latitude, longitude float64, opts *EditLocationOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("latitude", ftoa(latitude))
+ vals.Set("longitude", ftoa(longitude))
+ return res, client.get(a.base, "editMessageLiveLocation", addValues(addValues(vals, msg), opts), &res)
+}
+
+// StopMessageLiveLocation is used to stop updating a live location message before `LivePeriod` expires.
+func (a API) StopMessageLiveLocation(msg MessageIDOptions, opts *StopLocationOptions) (res APIResponseMessage, err error) {
+ return res, client.get(a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts), &res)
+}
+
+// SendVenue is used to send information about a venue.
+func (a API) SendVenue(chatID int64, latitude, longitude float64, title, address string, opts *VenueOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("latitude", ftoa(latitude))
+ vals.Set("longitude", ftoa(longitude))
+ vals.Set("title", title)
+ vals.Set("address", address)
+ return res, client.get(a.base, "sendVenue", addValues(vals, opts), &res)
+}
+
+// SendContact is used to send phone contacts.
+func (a API) SendContact(phoneNumber, firstName string, chatID int64, opts *ContactOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("phone_number", phoneNumber)
+ vals.Set("first_name", firstName)
+ return res, client.get(a.base, "sendContact", addValues(vals, opts), &res)
+}
+
+// SendPoll is used to send a native poll.
+func (a API) SendPoll(chatID int64, question string, options []InputPollOption, opts *PollOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ pollOpts, err := json.Marshal(options)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("question", question)
+ vals.Set("options", string(pollOpts))
+ return res, client.get(a.base, "sendPoll", addValues(vals, opts), &res)
+}
+
+// SendDice is used to send an animated emoji that will display a random value.
+func (a API) SendDice(chatID int64, emoji DiceEmoji, opts *BaseOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("emoji", string(emoji))
+ return res, client.get(a.base, "sendDice", addValues(vals, opts), &res)
+}
+
+// SendChatAction is used to tell the user that something is happening on the bot's side.
+// The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status).
+func (a API) SendChatAction(action ChatAction, chatID int64, opts *ChatActionOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("action", string(action))
+ return res, client.get(a.base, "sendChatAction", addValues(vals, opts), &res)
+}
+
+// SetMessageReaction is used to change the chosen reactions on a message.
+// Service messages can't be reacted to.
+// Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel.
+// In albums, bots must react to the first message.
+func (a API) SetMessageReaction(chatID int64, messageID int, opts *MessageReactionOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_id", itoa(int64(messageID)))
+ return res, client.get(a.base, "setMessageReaction", addValues(vals, opts), &res)
+}
+
+// GetUserProfilePhotos is used to get a list of profile pictures for a user.
+func (a API) GetUserProfilePhotos(userID int64, opts *UserProfileOptions) (res APIResponseUserProfile, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "getUserProfilePhotos", addValues(vals, opts), &res)
+}
+
+// GetUserProfileAudios is used to get the list of profile audios for a user.
+func (a API) GetUserProfileAudios(userID int64, opts *UserProfileAudioOptions) (res APIResponseUserProfileAudios, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "getUserProfileAudios", addValues(vals, opts), &res)
+}
+
+// SetUserEmojiStatus
+func (a API) SetUserEmojiStatus(userID int64, opts *UserEmojiStatusOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "setUserEmojiStatus", addValues(vals, opts), &res)
+}
+
+// GetFile returns the basic info about a file and prepares it for downloading.
+// For the moment, bots can download files of up to 20MB in size.
+// The file can then be downloaded with DownloadFile where filePath is taken from the response.
+// It is guaranteed that the file will be downloadable for at least 1 hour.
+// When the download file expires, a new one can be requested by calling GetFile again.
+func (a API) GetFile(fileID string) (res APIResponseFile, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("file_id", fileID)
+ return res, client.get(a.base, "getFile", vals, &res)
+}
+
+// DownloadFile returns the bytes of the file corresponding to the given filePath.
+// This function is callable for at least 1 hour since the call to GetFile.
+// When the download expires a new one can be requested by calling GetFile again.
+func (a API) DownloadFile(filePath string) ([]byte, error) {
+ return client.doGet(fmt.Sprintf(
+ "https://api.telegram.org/file/bot%s/%s",
+ a.token,
+ filePath,
+ ))
+}
+
+// BanChatMember is used to ban a user in a group, a supergroup or a channel.
+// In the case of supergroups or channels, the user will not be able to return to the chat
+// on their own using invite links, etc., unless unbanned first (through the UnbanChatMember method).
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+func (a API) BanChatMember(chatID, userID int64, opts *BanOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "banChatMember", addValues(vals, opts), &res)
+}
+
+// UnbanChatMember is used to unban a previously banned user in a supergroup or channel.
+// The user will NOT return to the group or channel automatically, but will be able to join via link, etc.
+// The bot must be an administrator for this to work.
+// By default, this method guarantees that after the call the user is not a member of the chat, but will be able to join it.
+// So if the user is a member of the chat they will also be REMOVED from the chat.
+// If you don't want this, use the parameter `OnlyIfBanned`.
+func (a API) UnbanChatMember(chatID, userID int64, opts *UnbanOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "unbanChatMember", addValues(vals, opts), &res)
+}
+
+// RestrictChatMember is used to restrict a user in a supergroup.
+// The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
+func (a API) RestrictChatMember(chatID, userID int64, permissions ChatPermissions, opts *RestrictOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ perm, err := json.Marshal(permissions)
+ if err != nil {
+ return
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ vals.Set("permissions", string(perm))
+ return res, client.get(a.base, "restrictChatMember", addValues(vals, opts), &res)
+}
+
+// PromoteChatMember is used to promote or demote a user in a supergroup or a channel.
+// The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
+func (a API) PromoteChatMember(chatID, userID int64, opts *PromoteOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "promoteChatMember", addValues(vals, opts), &res)
+}
+
+// SetChatAdministratorCustomTitle is used to set a custom title for an administrator in a supergroup promoted by the bot.
+func (a API) SetChatAdministratorCustomTitle(chatID, userID int64, customTitle string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ vals.Set("custom_title", customTitle)
+ return res, client.get(a.base, "setChatAdministratorCustomTitle", vals, &res)
+}
+
+// BanChatSenderChat is used to ban a channel chat in a supergroup or a channel.
+// The owner of the chat will not be able to send messages and join live streams on behalf of the chat, unless it is unbanned first.
+// The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights.
+func (a API) BanChatSenderChat(chatID, senderChatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("sender_chat_id", itoa(senderChatID))
+ return res, client.get(a.base, "banChatSenderChat", vals, &res)
+}
+
+// UnbanChatSenderChat is used to unban a previously channel chat in a supergroup or channel.
+// The bot must be an administrator for this to work and must have the appropriate administrator rights.
+func (a API) UnbanChatSenderChat(chatID, senderChatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("sender_chat_id", itoa(senderChatID))
+ return res, client.get(a.base, "unbanChatSenderChat", vals, &res)
+}
+
+// SetChatPermissions is used to set default chat permissions for all members.
+// The bot must be an administrator in the supergroup for this to work and must have the can_restrict_members admin rights.
+func (a API) SetChatPermissions(chatID int64, permissions ChatPermissions, opts *ChatPermissionsOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ perm, err := json.Marshal(permissions)
+ if err != nil {
+ return
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("permissions", string(perm))
+ return res, client.get(a.base, "setChatPermissions", addValues(vals, opts), &res)
+}
+
+// ExportChatInviteLink is used to generate a new primary invite link for a chat;
+// any previously generated primary link is revoked.
+// The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
+func (a API) ExportChatInviteLink(chatID int64) (res APIResponseString, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "exportChatInviteLink", vals, &res)
+}
+
+// CreateChatInviteLink is used to create an additional invite link for a chat.
+// The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
+// The link can be revoked using the method RevokeChatInviteLink.
+func (a API) CreateChatInviteLink(chatID int64, opts *InviteLinkOptions) (res APIResponseInviteLink, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "createChatInviteLink", addValues(vals, opts), &res)
+}
+
+// EditChatInviteLink is used to edit a non-primary invite link created by the bot.
+// The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
+func (a API) EditChatInviteLink(chatID int64, inviteLink string, opts *InviteLinkOptions) (res APIResponseInviteLink, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("invite_link", inviteLink)
+ return res, client.get(a.base, "editChatInviteLink", addValues(vals, opts), &res)
+}
+
+// CreateChatSubscriptionInviteLink is used to create a subscription invite link for a channel chat.
+// The bot must have the can_invite_users administrator rights.
+// The link can be edited using the method editChatSubscriptionInviteLink or revoked using the method revokeChatInviteLink.
+func (a API) CreateChatSubscriptionInviteLink(chatID int64, subscriptionPeriod, subscriptionPrice int, opts *ChatSubscriptionInviteOptions) (res APIResponseInviteLink, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("subscription_period", itoa(int64(subscriptionPeriod)))
+ vals.Set("subscription_price", itoa(int64(subscriptionPrice)))
+ return res, client.get(a.base, "createChatSubscriptionInviteLink", addValues(vals, opts), &res)
+}
+
+// EditChatSubscriptionInviteLink is used to creeditate a subscription invite link for a channel chat.
+// The bot must have the can_invite_users administrator rights.
+func (a API) EditChatSubscriptionInviteLink(chatID int64, inviteLink string, opts *ChatSubscriptionInviteOptions) (res APIResponseInviteLink, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("invite_link", inviteLink)
+ return res, client.get(a.base, "editChatSubscriptionInviteLink", addValues(vals, opts), &res)
+}
+
+// RevokeChatInviteLink is used to revoke an invite link created by the bot.
+// If the primary link is revoked, a new link is automatically generated.
+// The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
+func (a API) RevokeChatInviteLink(chatID int64, inviteLink string) (res APIResponseInviteLink, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("invite_link", inviteLink)
+ return res, client.get(a.base, "editChatInviteLink", vals, &res)
+}
+
+// ApproveChatJoinRequest is used to approve a chat join request.
+// The bot must be an administrator in the chat for this to work and must have the CanInviteUsers administrator right.
+func (a API) ApproveChatJoinRequest(chatID, userID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "approveChatJoinRequest", vals, &res)
+}
+
+// DeclineChatJoinRequest is used to decline a chat join request.
+// The bot must be an administrator in the chat for this to work and must have the CanInviteUsers administrator right.
+func (a API) DeclineChatJoinRequest(chatID, userID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "declineChatJoinRequest", vals, &res)
+}
+
+// SetChatPhoto is used to set a new profile photo for the chat.
+// Photos can't be changed for private chats.
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+func (a API) SetChatPhoto(file InputFile, chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.postFile(a.base, "setChatPhoto", "photo", file, InputFile{}, vals, &res)
+}
+
+// DeleteChatPhoto is used to delete a chat photo.
+// Photos can't be changed for private chats.
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+func (a API) DeleteChatPhoto(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "deleteChatPhoto", vals, &res)
+}
+
+// SetChatTitle is used to change the title of a chat.
+// Titles can't be changed for private chats.
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+func (a API) SetChatTitle(chatID int64, title string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("title", title)
+ return res, client.get(a.base, "setChatTitle", vals, &res)
+}
+
+// SetChatDescription is used to change the description of a group, a supergroup or a channel.
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+func (a API) SetChatDescription(chatID int64, description string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("description", description)
+ return res, client.get(a.base, "setChatDescription", vals, &res)
+}
+
+// PinChatMessage is used to add a message to the list of pinned messages in the chat.
+// If the chat is not a private chat, the bot must be an administrator in the chat for this to work
+// and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel.
+func (a API) PinChatMessage(chatID int64, messageID int, opts *PinMessageOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_id", itoa(int64(messageID)))
+ return res, client.get(a.base, "pinChatMessage", addValues(vals, opts), &res)
+}
+
+// UnpinChatMessage is used to remove a message from the list of pinned messages in the chat.
+// If the chat is not a private chat, the bot must be an administrator in the chat for this to work
+// and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel.
+func (a API) UnpinChatMessage(chatID int64, opts *UnpinMessageOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "unpinChatMessage", addValues(vals, opts), &res)
+}
+
+// UnpinAllChatMessages is used to clear the list of pinned messages in a chat.
+// If the chat is not a private chat, the bot must be an administrator in the chat for this to work
+// and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel.
+func (a API) UnpinAllChatMessages(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "unpinAllChatMessages", vals, &res)
+}
+
+// LeaveChat is used to make the bot leave a group, supergroup or channel.
+func (a API) LeaveChat(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "leaveChat", vals, &res)
+}
+
+// GetChat is used to get up to date information about the chat.
+// (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.)
+func (a API) GetChat(chatID int64) (res APIResponseChat, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "getChat", vals, &res)
+}
+
+// GetChatAdministrators is used to get a list of administrators in a chat.
+func (a API) GetChatAdministrators(chatID int64) (res APIResponseAdministrators, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "getChatAdministrators", vals, &res)
+}
+
+// GetChatMemberCount is used to get the number of members in a chat.
+func (a API) GetChatMemberCount(chatID int64) (res APIResponseInteger, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "getChatMemberCount", vals, &res)
+}
+
+// GetChatMember is used to get information about a member of a chat.
+func (a API) GetChatMember(chatID, userID int64) (res APIResponseChatMember, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "getChatMember", vals, &res)
+}
+
+// SetChatStickerSet is used to set a new group sticker set for a supergroup.
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+// Use the field `CanSetStickerSet` optionally returned in GetChat requests to check if the bot can use this method.
+func (a API) SetChatStickerSet(chatID int64, stickerSetName string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("sticker_set_name", stickerSetName)
+ return res, client.get(a.base, "setChatStickerSet", vals, &res)
+}
+
+// DeleteChatStickerSet is used to delete a group sticker set for a supergroup.
+// The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+// Use the field `CanSetStickerSet` optionally returned in GetChat requests to check if the bot can use this method.
+func (a API) DeleteChatStickerSet(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "deleteChatStickerSet", vals, &res)
+}
+
+// CreateForumTopic is used to create a topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights.
+func (a API) CreateForumTopic(chatID int64, name string, opts *CreateTopicOptions) (res APIResponseForumTopic, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("name", name)
+ return res, client.get(a.base, "createForumTopic", addValues(vals, opts), &res)
+}
+
+// EditForumTopic is used to edit name and icon of a topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights.
+func (a API) EditForumTopic(chatID, messageThreadID int64, opts *EditTopicOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_thread_id", itoa(messageThreadID))
+ return res, client.get(a.base, "editForumTopic", addValues(vals, opts), &res)
+}
+
+// CloseForumTopic is used to close an open topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights.
+func (a API) CloseForumTopic(chatID, messageThreadID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_thread_id", itoa(messageThreadID))
+ return res, client.get(a.base, "closeForumTopic", vals, &res)
+}
+
+// ReopenForumTopic is used to reopen a closed topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights.
+func (a API) ReopenForumTopic(chatID, messageThreadID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_thread_id", itoa(messageThreadID))
+ return res, client.get(a.base, "reopenForumTopic", vals, &res)
+}
+
+// DeleteForumTopic is used to delete a forum topic along with all its messages in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights.
+func (a API) DeleteForumTopic(chatID, messageThreadID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_thread_id", itoa(messageThreadID))
+ return res, client.get(a.base, "deleteForumTopic", vals, &res)
+}
+
+// UnpinAllForumTopicMessages is used to clear the list of pinned messages in a forum topic.
+// The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights.
+func (a API) UnpinAllForumTopicMessages(chatID, messageThreadID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_thread_id", itoa(messageThreadID))
+ return res, client.get(a.base, "unpinAllForumTopicMessages", vals, &res)
+}
+
+// EditGeneralForumTopic is used to edit the name of the 'General' topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights.
+func (a API) EditGeneralForumTopic(chatID int64, name string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("name", name)
+ return res, client.get(a.base, "editGeneralForumTopic", vals, &res)
+}
+
+// CloseGeneralForumTopic is used to close an open 'General' topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights.
+func (a API) CloseGeneralForumTopic(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "closeGeneralForumTopic", vals, &res)
+}
+
+// ReopenGeneralForumTopic is used to reopen a closed 'General' topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights.
+// The topic will be automatically unhidden if it was hidden.
+func (a API) ReopenGeneralForumTopic(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "reopenGeneralForumTopic", vals, &res)
+}
+
+// HideGeneralForumTopic is used to hide the 'General' topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights.
+// The topic will be automatically closed if it was open.
+func (a API) HideGeneralForumTopic(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "hideGeneralForumTopic", vals, &res)
+}
+
+// UnhideGeneralForumTopic is used to unhide the 'General' topic in a forum supergroup chat.
+// The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights.
+func (a API) UnhideGeneralForumTopic(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "unhideGeneralForumTopic", vals, &res)
+}
+
+// UnpinAllGeneralForumTopicMessages is used to clear the list of pinned messages in a General forum topic.
+// The bot must be an administrator in the chat for this to work and must have can_pin_messages administrator right in the supergroup.
+func (a API) UnpinAllGeneralForumTopicMessages(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "unpinAllGeneralForumTopicMessages", vals, &res)
+}
+
+// AnswerCallbackQuery is used to send answers to callback queries sent from inline keyboards.
+// The answer will be displayed to the user as a notification at the top of the chat screen or as an alert.
+func (a API) AnswerCallbackQuery(callbackID string, opts *CallbackQueryOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("callback_query_id", callbackID)
+ return res, client.get(a.base, "answerCallbackQuery", addValues(vals, opts), &res)
+}
+
+// GetUserChatBoosts is used to get the list of boosts added to a chat by a user.
+// Requires administrator rights in the chat.
+func (a API) GetUserChatBoosts(chatID, userID int64) (res APIResponseUserChatBoosts, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "getUserChatBoosts", vals, &res)
+}
+
+// GetBusinessConnection is used to get information about the connection of the bot with a business account.
+func (a API) GetBusinessConnection(business_connection_id string) (res APIResponseBusinessConnection, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("business_connection_id", business_connection_id)
+ return res, client.get(a.base, "getBusinessConnection", vals, &res)
+}
+
+// SetMyCommands is used to change the list of the bot's commands for the given scope and user language.
+func (a API) SetMyCommands(opts *CommandOptions, commands ...BotCommand) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(commands)
+ vals.Set("commands", string(jsn))
+ return res, client.get(a.base, "setMyCommands", addValues(vals, opts), &res)
+}
+
+// DeleteMyCommands is used to delete the list of the bot's commands for the given scope and user language.
+func (a API) DeleteMyCommands(opts *CommandOptions) (res APIResponseBool, err error) {
+ return res, client.get(a.base, "deleteMyCommands", urlValues(opts), &res)
+}
+
+// GetMyCommands is used to get the current list of the bot's commands for the given scope and user language.
+func (a API) GetMyCommands(opts *CommandOptions) (res APIResponseCommands, err error) {
+ return res, client.get(a.base, "getMyCommands", urlValues(opts), &res)
+}
+
+// SetMyName is used to change the bot's name.
+func (a API) SetMyName(name, languageCode string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("name", name)
+ vals.Set("language_code", languageCode)
+ return res, client.get(a.base, "setMyName", vals, &res)
+}
+
+// GetMyName is used to get the current bot name for the given user language.
+func (a API) GetMyName(languageCode string) (res APIResponseBotName, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("language_code", languageCode)
+ return res, client.get(a.base, "getMyName", vals, &res)
+}
+
+// SetMyDescription is used to to change the bot's description, which is shown in the chat with the bot if the chat is empty.
+func (a API) SetMyDescription(description, languageCode string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("description", description)
+ vals.Set("language_code", languageCode)
+ return res, client.get(a.base, "setMyDescription", vals, &res)
+}
+
+// GetMyDescription is used to get the current bot description for the given user language.
+func (a API) GetMyDescription(languageCode string) (res APIResponseBotDescription, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("language_code", languageCode)
+ return res, client.get(a.base, "getMyDescription", vals, &res)
+}
+
+// SetMyShortDescription is used to to change the bot's short description,
+// which is shown on the bot's profile page and is sent together with the link when users share the bot.
+func (a API) SetMyShortDescription(shortDescription, languageCode string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("short_description", shortDescription)
+ vals.Set("language_code", languageCode)
+ return res, client.get(a.base, "setMyShortDescription", vals, &res)
+}
+
+// GetMyShortDescription is used to get the current bot short description for the given user language.
+func (a API) GetMyShortDescription(languageCode string) (res APIResponseBotShortDescription, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("language_code", languageCode)
+ return res, client.get(a.base, "getMyDescription", vals, &res)
+}
+
+// SetMyProfilePhoto is used to change the profile photo of the bot.
+func (a API) SetMyProfilePhoto(profilePhoto InputProfilePhoto) (res APIResponseBool, err error) {
+ return res, client.postProfilePhoto(a.base, "setMyProfilePhoto", nil, &res, profilePhoto)
+}
+
+// RemoveMyProfilePhoto is used to remove one of the bot profile photos.
+func (a API) RemoveMyProfilePhoto(photoID string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("photo_id", photoID)
+ return res, client.get(a.base, "removeMyProfilePhoto", vals, &res)
+}
+
+// EditMessageText is used to edit text and game messages.
+func (a API) EditMessageText(text string, msg MessageIDOptions, opts *MessageTextOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("text", text)
+ return res, client.get(a.base, "editMessageText", addValues(addValues(vals, msg), opts), &res)
+}
+
+// EditMessageCaption is used to edit captions of messages.
+func (a API) EditMessageCaption(msg MessageIDOptions, opts *MessageCaptionOptions) (res APIResponseMessage, err error) {
+ return res, client.get(a.base, "editMessageCaption", addValues(urlValues(msg), opts), &res)
+}
+
+// EditMessageMedia is used to edit animation, audio, document, photo or video messages, or to add media to text messages.
+// If a message is part of a message album, then it can be edited only to an audio for audio albums,
+// only to a document for document albums and to a photo or a video otherwise.
+// When an inline message is edited, a new file can't be uploaded;
+// Use a previously uploaded file via its file_id or specify a URL.
+func (a API) EditMessageMedia(msg MessageIDOptions, media InputMedia, opts *MessageMediaOptions) (res APIResponseMessage, err error) {
+ return res, client.postMedia(a.base, "editMessageMedia", true, addValues(urlValues(msg), opts), &res, media)
+}
+
+// EditMessageReplyMarkup is used to edit only the reply markup of messages.
+func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkupOptions) (res APIResponseMessage, err error) {
+ return res, client.get(a.base, "editMessageReplyMarkup", addValues(urlValues(msg), opts), &res)
+}
+
+// StopPoll is used to stop a poll which was sent by the bot.
+func (a API) StopPoll(chatID int64, messageID int, opts *StopPollOptions) (res APIResponsePoll, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_id", itoa(int64(messageID)))
+ return res, client.get(a.base, "stopPoll", addValues(vals, opts), &res)
+}
+
+// DeleteMessage is used to delete a message, including service messages, with the following limitations:
+// - A message can only be deleted if it was sent less than 48 hours ago.
+// - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago.
+// - Bots can delete outgoing messages in private chats, groups, and supergroups.
+// - Bots can delete incoming messages in private chats.
+// - Bots granted can_post_messages permissions can delete outgoing messages in channels.
+// - If the bot is an administrator of a group, it can delete any message there.
+// - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there.
+func (a API) DeleteMessage(chatID int64, messageID int) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_id", itoa(int64(messageID)))
+ return res, client.get(a.base, "deleteMessage", vals, &res)
+}
+
+// DeleteMessages is used to delete multiple messages simultaneously.
+// If some of the specified messages can't be found, they are skipped.
+func (a API) DeleteMessages(chatID int64, messageIDs []int) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ msgIDs, err := json.Marshal(messageIDs)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("message_ids", string(msgIDs))
+ return res, client.get(a.base, "deleteMessages", vals, &res)
+}
+
+// GetAvailableGifts returns the list of gifts that can be sent by the bot to users.
+func (a API) GetAvailableGifts() (res APIResponseGifts, err error) {
+ return res, client.get(a.base, "getAvailableGifts", nil, &res)
+}
+
+// SendGift sends a gift to the given user.
+// The gift can't be converted to Telegram Stars by the user.
+func (a API) SendGift(userID int64, giftID string, opts *GiftOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("gift_id", giftID)
+ return res, client.get(a.base, "sendGift", addValues(vals, opts), &res)
+}
+
+// VerifyUser verifies a user on behalf of the organization which is represented by the bot.
+func (a API) VerifyUser(userID int64, opts *VerifyOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "verifyUser", addValues(vals, opts), &res)
+}
+
+// VerifyChat verifies a chat on behalf of the organization which is represented by the bot.
+func (a API) VerifyChat(chatID int64, opts *VerifyOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "verifyChat", addValues(vals, opts), &res)
+}
+
+// RemoveUserVerification removes verification from a user who is currently verified on behalf of the organization represented by the bot.
+func (a API) RemoveUserVerification(userID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "verifyUser", vals, &res)
+}
+
+// RemoveChatVerification removes verification from a chat who is currently verified on behalf of the organization represented by the bot.
+func (a API) RemoveChatVerification(chatID int64) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "verifyChat", vals, &res)
+}
diff --git a/shared/echotron/api_test.go b/shared/echotron/api_test.go
new file mode 100644
index 0000000..cb1456b
--- /dev/null
+++ b/shared/echotron/api_test.go
@@ -0,0 +1,2174 @@
+package echotron
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+ "testing"
+ "time"
+)
+
+var (
+ msgTmp *Message
+ animationTmp *Message
+ pollTmp *Message
+ locationTmp *Message
+ inviteTmp *ChatInviteLink
+ chatSubInviteTmp *ChatInviteLink
+ filePath string
+ currentBotDesc string
+ currentBotShortDesc string
+ msgThreadID int64
+ currentBotName = "bot name unset"
+ api = NewAPI("1713461126:AAEV5sgVo513Vz4PT33mpp0ZykJqrnSluzM")
+ chatID = int64(14870908)
+ banUserID = int64(41876271)
+ channelID = int64(-1001563144067)
+ groupID = int64(-1001265771214)
+ pinMsgID = int(11)
+ photoID = "AgACAgQAAxkDAAMrYFtODxV2LL6-kR_6qSbG9n8dIOIAAti1MRug29lSkNq_9o8PC5uMd7EnXQADAQADAgADbQADeooGAAEeBA"
+ animationID = "CgACAgQAAxkDAAICQGBcoGs7GFJ-tR5AkbRRLFTbvdxXAAJ1CAAC1zHgUu-ciZqanytIHgQ"
+ audioID = "CQACAgQAAxkDAAIBCmBbamz_DqKk2GmrzmoM0SrzRN6wAAK9CAACoNvZUgPyk-87OM_YHgQ"
+ documentID = "BQACAgQAAxkDAANmYFtSXcF5kTtwgHeqVUngyuuJMx4AAnQIAAKg29lSb4HP4x-qMT8eBA"
+ paidVideoID = "BAACAgQAAx0EXSuvgwADGGa7vNTyDxfQiyICxWhnLUfhJphkAAIvFAACHozgUVOTwR-Bak97NQQ"
+ videoID = "BAACAgQAAxkDAANxYFtaxF1kfc7nVY_Mtfba3u5dMooAAoYIAAKg29lSpwABJrcveXZlHgQ"
+ videoNoteID = "DQACAgQAAxkDAAIBumBbfT5jPC_cvyEcr0_8DpmFDz2PAALVCgACOX7hUjGZ_MmnZVVeHgQ"
+ voiceID = "AwACAgQAAxkDAAPXYFtmoFriwJFVGDgPPpfUBljgnYAAAq8IAAKg29lStEWfrNMMAxgeBA"
+ photoURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/echotron_test.png"
+ animationURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/animation.mp4"
+ audioURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/audio.mp3"
+ documentURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/document.pdf"
+ logoInvURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/echotron_thumb_inv.jpg"
+ videoURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/video.webm"
+ videoNoteURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/video_note.mp4"
+ voiceURL = "https://github.com/NicoNex/echotron/raw/master/assets/tests/audio.mp3"
+
+ commands = []BotCommand{
+ {Command: "test1", Description: "Test command 1"},
+ {Command: "test2", Description: "Test command 2"},
+ {Command: "test3", Description: "Test command 3"},
+ }
+
+ keyboard = ReplyKeyboardMarkup{
+ Keyboard: [][]KeyboardButton{
+ {
+ {Text: "test 1"},
+ {Text: "test 2"},
+ },
+ {
+ {Text: "test 3"},
+ {Text: "test 4"},
+ },
+ },
+ ResizeKeyboard: true,
+ }
+
+ inlineKeyboard = InlineKeyboardMarkup{
+ InlineKeyboard: [][]InlineKeyboardButton{
+ {
+ {Text: "test1", CallbackData: "test1"},
+ {Text: "test2", CallbackData: "test2"},
+ },
+ {
+ {Text: "test3", CallbackData: "test3"},
+ },
+ },
+ }
+
+ inlineKeyboardEdit = InlineKeyboardMarkup{
+ InlineKeyboard: [][]InlineKeyboardButton{
+ {
+ {Text: "test1", CallbackData: "test1"},
+ {Text: "test2", CallbackData: "test2"},
+ },
+ {
+ {Text: "test3", CallbackData: "test3"},
+ {Text: "edit", CallbackData: "edit"},
+ },
+ },
+ }
+)
+
+func openBytes(path string) (data []byte, err error) {
+ file, err := os.Open(path)
+
+ if err != nil {
+ return
+ }
+
+ data, err = io.ReadAll(file)
+
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+func TestGetUpdates(t *testing.T) {
+ _, err := api.GetUpdates(
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetWebhook(t *testing.T) {
+ _, err := api.SetWebhook(
+ "example.com",
+ false,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetWebhookWrongURL(t *testing.T) {
+ _, err := api.SetWebhook(
+ "example.com_",
+ false,
+ nil,
+ )
+
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+}
+
+func TestDeleteWebhook(t *testing.T) {
+ _, err := api.DeleteWebhook(
+ false,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetWebhookInfo(t *testing.T) {
+ _, err := api.GetWebhookInfo()
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetMe(t *testing.T) {
+ _, err := api.GetMe()
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendMessage(t *testing.T) {
+ res, err := api.SendMessage(
+ "TestSendMessage *bold* _italic_ `monospace`",
+ chatID,
+ &MessageOptions{
+ ParseMode: MarkdownV2,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ msgTmp = res.Result
+}
+
+func TestSetMessageReaction(t *testing.T) {
+ _, err := api.SetMessageReaction(
+ chatID,
+ msgTmp.ID,
+ &MessageReactionOptions{
+ Reaction: []ReactionType{
+ ReactionType{
+ Type: "emoji",
+ Emoji: "👍",
+ },
+ },
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestForwardMessage(t *testing.T) {
+ _, err := api.ForwardMessage(
+ chatID,
+ chatID, // fromChatID
+ msgTmp.ID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestForwardMessages(t *testing.T) {
+ msg, _ := api.SendMessage(
+ "TestForwardMessages",
+ chatID,
+ nil,
+ )
+
+ _, err := api.ForwardMessages(
+ chatID,
+ chatID, // fromChatID
+ []int{msgTmp.ID, msg.Result.ID},
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestForwardMessagesWrongMsgID(t *testing.T) {
+ _, err := api.ForwardMessages(
+ chatID,
+ chatID, // fromChatID
+ []int{},
+ nil,
+ )
+
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+}
+
+func TestCopyMessage(t *testing.T) {
+ _, err := api.CopyMessage(
+ chatID,
+ chatID, // fromChatID
+ msgTmp.ID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCopyMessages(t *testing.T) {
+ msg, _ := api.SendMessage(
+ "TestCopyMessages",
+ chatID,
+ nil,
+ )
+
+ _, err := api.CopyMessages(
+ chatID,
+ chatID, // fromChatID
+ []int{msgTmp.ID, msg.Result.ID},
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCopyMessagesWrongMsgID(t *testing.T) {
+ _, err := api.CopyMessages(
+ chatID,
+ chatID, // fromChatID
+ []int{},
+ nil,
+ )
+
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+}
+
+func TestSendMessageReply(t *testing.T) {
+ _, err := api.SendMessage(
+ "TestSendMessageReply",
+ chatID,
+ &MessageOptions{
+ ReplyParameters: ReplyParameters{
+ MessageID: msgTmp.ID,
+ },
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendMessageWithKeyboard(t *testing.T) {
+ _, err := api.SendMessage(
+ "TestSendMessageWithKeyboard",
+ chatID,
+ &MessageOptions{
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPhoto(t *testing.T) {
+ _, err := api.SendPhoto(
+ NewInputFilePath("assets/tests/echotron_test.png"),
+ chatID,
+ &PhotoOptions{
+ Caption: "TestSendPhoto",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPhotoByID(t *testing.T) {
+ _, err := api.SendPhoto(
+ NewInputFileID(photoID),
+ chatID,
+ &PhotoOptions{
+ Caption: "TestSendPhotoByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPhotoURL(t *testing.T) {
+ _, err := api.SendPhoto(
+ NewInputFileURL(photoURL),
+ chatID,
+ &PhotoOptions{
+ Caption: "TestSendPhotoURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPhotoBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/echotron_test.png")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendPhoto(
+ NewInputFileBytes("echotron_test.png", data),
+ chatID,
+ &PhotoOptions{
+ Caption: "TestSendPhotoBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPhotoWithKeyboard(t *testing.T) {
+ _, err := api.SendPhoto(
+ NewInputFilePath("assets/tests/echotron_test.png"),
+ chatID,
+ &PhotoOptions{
+ Caption: "TestSendPhotoWithKeyboard",
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAudio(t *testing.T) {
+ _, err := api.SendAudio(
+ NewInputFilePath("assets/tests/audio.mp3"),
+ chatID,
+ &AudioOptions{
+ Caption: "TestSendAudio",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAudioByID(t *testing.T) {
+ _, err := api.SendAudio(
+ NewInputFileID(audioID),
+ chatID,
+ &AudioOptions{
+ Caption: "TestSendAudioByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAudioURL(t *testing.T) {
+ _, err := api.SendAudio(
+ NewInputFileURL(audioURL),
+ chatID,
+ &AudioOptions{
+ Caption: "TestSendAudioURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAudioWithKeyboard(t *testing.T) {
+ _, err := api.SendAudio(
+ NewInputFilePath("assets/tests/audio.mp3"),
+ chatID,
+ &AudioOptions{
+ Caption: "TestSendAudioWithKeyboard",
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAudioBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/audio.mp3")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendAudio(
+ NewInputFileBytes("audio.mp3", data),
+ chatID,
+ &AudioOptions{
+ Caption: "TestSendAudioBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAudioThumbnail(t *testing.T) {
+ _, err := api.SendAudio(
+ NewInputFilePath("assets/tests/audio.mp3"),
+ chatID,
+ &AudioOptions{
+ Caption: "TestSendAudio",
+ Thumbnail: NewInputFilePath("assets/tests/echotron_thumb.jpg"),
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendDocument(t *testing.T) {
+ _, err := api.SendDocument(
+ NewInputFilePath("assets/tests/document.pdf"),
+ chatID,
+ &DocumentOptions{
+ Caption: "TestSendDocument",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendDocumentByID(t *testing.T) {
+ _, err := api.SendDocument(
+ NewInputFileID(documentID),
+ chatID,
+ &DocumentOptions{
+ Caption: "TestSendDocumentByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendDocumentURL(t *testing.T) {
+ _, err := api.SendDocument(
+ NewInputFileURL(documentURL),
+ chatID,
+ &DocumentOptions{
+ Caption: "TestSendDocumentURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendDocumentWithKeyboard(t *testing.T) {
+ _, err := api.SendDocument(
+ NewInputFilePath("assets/tests/document.pdf"),
+ chatID,
+ &DocumentOptions{
+ Caption: "TestSendDocumentWithKeyboard",
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendDocumentBytes(t *testing.T) {
+ file, err := os.Open("assets/tests/document.pdf")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data, err := io.ReadAll(file)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendDocument(
+ NewInputFileBytes("document.pdf", data),
+ chatID,
+ &DocumentOptions{
+ Caption: "TestSendDocumentBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideo(t *testing.T) {
+ _, err := api.SendVideo(
+ NewInputFilePath("assets/tests/video.webm"),
+ chatID,
+ &VideoOptions{
+ Caption: "TestSendVideo",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoByID(t *testing.T) {
+ _, err := api.SendVideo(
+ NewInputFileID(videoID),
+ chatID,
+ &VideoOptions{
+ Caption: "TestSendVideoByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoURL(t *testing.T) {
+ _, err := api.SendVideo(
+ NewInputFileURL(videoURL),
+ chatID,
+ &VideoOptions{
+ Caption: "TestSendVideoURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoWithKeyboard(t *testing.T) {
+ _, err := api.SendVideo(
+ NewInputFilePath("assets/tests/video.webm"),
+ chatID,
+ &VideoOptions{
+ Caption: "TestSendVideoWithKeyboard",
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/video.webm")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendVideo(
+ NewInputFileBytes("video.webm", data),
+ chatID,
+ &VideoOptions{
+ Caption: "TestSendVideoBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAnimation(t *testing.T) {
+ res, err := api.SendAnimation(
+ NewInputFilePath("assets/tests/animation.mp4"),
+ chatID,
+ &AnimationOptions{
+ Caption: "TestSendAnimation",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ animationTmp = res.Result
+}
+
+func TestSendAnimationByID(t *testing.T) {
+ _, err := api.SendAnimation(
+ NewInputFileID(animationID),
+ chatID,
+ &AnimationOptions{
+ Caption: "TestSendAnimationByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAnimationURL(t *testing.T) {
+ res, err := api.SendAnimation(
+ NewInputFileURL(animationURL),
+ chatID,
+ &AnimationOptions{
+ Caption: "TestSendAnimationURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ animationTmp = res.Result
+}
+
+func TestSendAnimationWithKeyboard(t *testing.T) {
+ _, err := api.SendAnimation(
+ NewInputFilePath("assets/tests/animation.mp4"),
+ chatID,
+ &AnimationOptions{
+ Caption: "TestSendAnimationWithKeyboard",
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendAnimationBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/animation.mp4")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendAnimation(
+ NewInputFileBytes("animation.mp4", data),
+ chatID,
+ &AnimationOptions{
+ Caption: "TestSendAnimationBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVoice(t *testing.T) {
+ _, err := api.SendVoice(
+ NewInputFilePath("assets/tests/audio.mp3"),
+ chatID,
+ &VoiceOptions{
+ Caption: "TestSendVoice",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVoiceByID(t *testing.T) {
+ _, err := api.SendVoice(
+ NewInputFileID(voiceID),
+ chatID,
+ &VoiceOptions{
+ Caption: "TestSendVoiceByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVoiceURL(t *testing.T) {
+ _, err := api.SendVoice(
+ NewInputFileURL(voiceURL),
+ chatID,
+ &VoiceOptions{
+ Caption: "TestSendVoiceURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVoiceWithKeyboard(t *testing.T) {
+ _, err := api.SendVoice(
+ NewInputFilePath("assets/tests/audio.mp3"),
+ chatID,
+ &VoiceOptions{
+ Caption: "TestSendVoiceWithKeyboard",
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVoiceBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/audio.mp3")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendVoice(
+ NewInputFileBytes("audio.mp3", data),
+ chatID,
+ &VoiceOptions{
+ Caption: "TestSendVoiceBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoNote(t *testing.T) {
+ _, err := api.SendVideoNote(
+ NewInputFilePath("assets/tests/video_note.mp4"),
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoNoteURL(t *testing.T) {
+ _, err := api.SendVideoNote(
+ NewInputFileURL(videoNoteURL),
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoNoteByID(t *testing.T) {
+ _, err := api.SendVideoNote(
+ NewInputFileID(videoNoteID),
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoNoteWithKeyboard(t *testing.T) {
+ _, err := api.SendVideoNote(
+ NewInputFilePath("assets/tests/video_note.mp4"),
+ chatID,
+ &VideoNoteOptions{
+ ReplyMarkup: keyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVideoNoteBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/video_note.mp4")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendVideoNote(
+ NewInputFileBytes("video_note.mp4", data),
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaPhoto(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFilePath("assets/logo.png"),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaPhoto",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaPhotoByID(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFileID(photoID),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaPhotoByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaPhotoURL(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFileURL(photoURL),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaPhotoURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaPhotoBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/echotron_test.png")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFileBytes("echotron_test.png", data),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaPhotoBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaPhotoWithKeyboard(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFilePath("assets/logo.png"),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaPhotoWithKeyboard",
+ ReplyMarkup: inlineKeyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaVideo(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFilePath("assets/tests/video_note.mp4"),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaVideo",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaVideoByID(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFileID(paidVideoID),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaVideoByID",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaVideoURL(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFileURL(videoNoteURL),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaVideoURL",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaVideoBytes(t *testing.T) {
+ data, err := openBytes("assets/tests/video_note.mp4")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFileBytes("video_note.mp4", data),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaVideoBytes",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaVideoWithKeyboard(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFilePath("assets/tests/video_note.mp4"),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaVideoWithKeyboard",
+ ReplyMarkup: inlineKeyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPaidMediaGroup(t *testing.T) {
+ _, err := api.SendPaidMedia(
+ channelID,
+ 1,
+ []GroupableInputMedia{
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFilePath("assets/logo.png"),
+ },
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFileID(photoID),
+ },
+ InputPaidMediaPhoto{
+ Type: InputPaidMediaTypePhoto,
+ Media: NewInputFileURL(logoInvURL),
+ },
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFilePath("assets/tests/video_note.mp4"),
+ },
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFileID(paidVideoID),
+ },
+ InputPaidMediaVideo{
+ Type: InputPaidMediaTypeVideo,
+ Media: NewInputFileURL(videoNoteURL),
+ },
+ },
+ &PaidMediaOptions{
+ Caption: "TestSendPaidMediaGroup",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendMediaGroupPhoto(t *testing.T) {
+ _, err := api.SendMediaGroup(
+ chatID,
+ []GroupableInputMedia{
+ InputMediaPhoto{
+ Type: MediaTypePhoto,
+ Media: NewInputFileID(photoID),
+ Caption: "TestSendMediaGroup1",
+ },
+ InputMediaPhoto{
+ Type: MediaTypePhoto,
+ Media: NewInputFilePath("assets/logo.png"),
+ Caption: "TestSendMediaGroup2",
+ },
+ InputMediaPhoto{
+ Type: MediaTypePhoto,
+ Media: NewInputFileURL(logoInvURL),
+ Caption: "TestSendMediaGroup3",
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendMediaGroupVideo(t *testing.T) {
+ _, err := api.SendMediaGroup(
+ chatID,
+ []GroupableInputMedia{
+ InputMediaVideo{
+ Type: MediaTypeVideo,
+ Media: NewInputFileID(videoID),
+ Caption: "TestSendMediaGroup1",
+ },
+ InputMediaVideo{
+ Type: MediaTypeVideo,
+ Media: NewInputFilePath("assets/tests/video.webm"),
+ Caption: "TestSendMediaGroup2",
+ },
+ InputMediaVideo{
+ Type: MediaTypeVideo,
+ Media: NewInputFileURL(videoURL),
+ Caption: "TestSendMediaGroup3",
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendMediaGroupDocument(t *testing.T) {
+ _, err := api.SendMediaGroup(
+ chatID,
+ []GroupableInputMedia{
+ InputMediaDocument{
+ Type: MediaTypeDocument,
+ Media: NewInputFileID(documentID),
+ Caption: "TestSendMediaGroup1",
+ },
+ InputMediaDocument{
+ Type: MediaTypeDocument,
+ Media: NewInputFilePath("assets/tests/document.pdf"),
+ Caption: "TestSendMediaGroup2",
+ },
+ InputMediaDocument{
+ Type: MediaTypeDocument,
+ Media: NewInputFileURL(documentURL),
+ Caption: "TestSendMediaGroup3",
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendMediaGroupThumbnail(t *testing.T) {
+ _, err := api.SendMediaGroup(
+ chatID,
+ []GroupableInputMedia{
+ InputMediaAudio{
+ Type: MediaTypeAudio,
+ Media: NewInputFilePath("assets/tests/audio_inv.mp3"),
+ Thumbnail: NewInputFilePath("assets/tests/echotron_thumb_inv.jpg"),
+ Caption: "TestSendMediaGroupThumbnail1",
+ },
+ InputMediaAudio{
+ Type: MediaTypeAudio,
+ Media: NewInputFilePath("assets/tests/audio.mp3"),
+ Thumbnail: NewInputFilePath("assets/tests/echotron_thumb.jpg"),
+ Caption: "TestSendMediaGroupThumbnail2",
+ },
+ InputMediaAudio{
+ Type: MediaTypeAudio,
+ Media: NewInputFileURL(audioURL),
+ Thumbnail: NewInputFileURL(logoInvURL),
+ Caption: "TestSendMediaGroupThumbnail3",
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendLocation(t *testing.T) {
+ res, err := api.SendLocation(
+ chatID,
+ 0.0,
+ 0.0,
+ &LocationOptions{
+ LivePeriod: 60,
+ HorizontalAccuracy: 50,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ locationTmp = res.Result
+}
+
+func TestEditMessageLiveLocation(t *testing.T) {
+ _, err := api.EditMessageLiveLocation(
+ NewMessageID(chatID, locationTmp.ID),
+ 0.0,
+ 0.0,
+ &EditLocationOptions{
+ HorizontalAccuracy: 50,
+ ReplyMarkup: inlineKeyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestStopMessageLiveLocation(t *testing.T) {
+ _, err := api.StopMessageLiveLocation(
+ NewMessageID(chatID, locationTmp.ID),
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendVenue(t *testing.T) {
+ _, err := api.SendVenue(
+ chatID,
+ 0.0,
+ 0.0,
+ "TestSendVenue",
+ "TestSendVenueAddress",
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendContact(t *testing.T) {
+ _, err := api.SendContact(
+ "1234567890",
+ "Name",
+ chatID,
+ &ContactOptions{
+ LastName: "Surname",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendPoll(t *testing.T) {
+ res, err := api.SendPoll(
+ chatID,
+ "TestSendPoll",
+ []InputPollOption{
+ {Text: "Option 1"},
+ {Text: "Option 2"},
+ {Text: "Option 3"},
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pollTmp = res.Result
+}
+
+func TestSendPollWrongOptions(t *testing.T) {
+ _, err := api.SendPoll(
+ chatID,
+ "TestSendPoll",
+ []InputPollOption{},
+ nil,
+ )
+
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+}
+
+func TestSendDice(t *testing.T) {
+ _, err := api.SendDice(
+ chatID,
+ Die,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendChatAction(t *testing.T) {
+ _, err := api.SendChatAction(
+ Typing,
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetUserProfilePhotos(t *testing.T) {
+ _, err := api.GetUserProfilePhotos(
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetFile(t *testing.T) {
+ res, err := api.GetFile(
+ photoID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ filePath = res.Result.FilePath
+}
+
+func TestDownloadFile(t *testing.T) {
+ res, err := api.DownloadFile(
+ filePath,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(res) == 0 {
+ t.Fatal("empty file received")
+ }
+}
+
+func TestBanChatMember(t *testing.T) {
+ _, err := api.BanChatMember(
+ channelID,
+ banUserID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnbanChatMember(t *testing.T) {
+ _, err := api.UnbanChatMember(
+ channelID,
+ banUserID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRestrictChatMember(t *testing.T) {
+ _, err := api.RestrictChatMember(
+ groupID,
+ banUserID,
+ ChatPermissions{
+ CanSendMessages: true,
+ },
+ &RestrictOptions{
+ UseIndependentChatPermissions: true,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPromoteChatMember(t *testing.T) {
+ _, err := api.PromoteChatMember(
+ groupID,
+ banUserID,
+ &PromoteOptions{
+ CanManageChat: true,
+ CanPostMessages: true,
+ CanEditMessages: true,
+ CanDeleteMessages: true,
+ CanManageVideoChats: true,
+ CanRestrictMembers: true,
+ CanPromoteMembers: true,
+ CanChangeInfo: true,
+ CanInviteUsers: true,
+ CanPinMessages: true,
+ CanPostStories: true,
+ CanEditStories: true,
+ CanDeleteStories: true,
+ CanManageTopics: true,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBanChatSenderChat(t *testing.T) {
+ _, err := api.BanChatSenderChat(
+ channelID,
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnbanChatSenderChat(t *testing.T) {
+ _, err := api.UnbanChatSenderChat(
+ channelID,
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetChatPermissions(t *testing.T) {
+ _, err := api.SetChatPermissions(
+ groupID,
+ ChatPermissions{
+ CanSendMessages: true,
+ CanSendAudios: true,
+ CanSendDocuments: true,
+ CanSendPhotos: true,
+ CanSendVideos: true,
+ CanSendVideoNotes: true,
+ CanSendVoiceNotes: true,
+ CanSendPolls: true,
+ CanSendOtherMessages: true,
+ CanAddWebPagePreviews: true,
+ CanChangeInfo: true,
+ CanInviteUsers: true,
+ CanPinMessages: true,
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestExportChatInviteLink(t *testing.T) {
+ _, err := api.ExportChatInviteLink(
+ channelID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCreateChatInviteLink(t *testing.T) {
+ res, err := api.CreateChatInviteLink(
+ channelID, nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ inviteTmp = res.Result
+}
+
+func TestEditChatInviteLink(t *testing.T) {
+ _, err := api.EditChatInviteLink(
+ channelID,
+ inviteTmp.InviteLink,
+ &InviteLinkOptions{
+ ExpireDate: time.Now().Unix() + 300,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCreateChatSubscriptionInviteLink(t *testing.T) {
+ res, err := api.CreateChatSubscriptionInviteLink(
+ channelID,
+ 2592000,
+ 1,
+ &ChatSubscriptionInviteOptions{
+ Name: "TestCreateChatSubscriptionInviteLink",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ chatSubInviteTmp = res.Result
+}
+
+func TestEditChatSubscriptionInviteLink(t *testing.T) {
+ _, err := api.EditChatSubscriptionInviteLink(
+ channelID,
+ chatSubInviteTmp.InviteLink,
+ &ChatSubscriptionInviteOptions{
+ Name: "TestEditChatSubscriptionInviteLink",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRevokeChatInviteLink(t *testing.T) {
+ _, err := api.RevokeChatInviteLink(
+ channelID,
+ inviteTmp.InviteLink,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetChatPhoto(t *testing.T) {
+ _, err := api.SetChatPhoto(
+ NewInputFilePath("assets/tests/echotron_test.png"),
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteChatPhoto(t *testing.T) {
+ _, err := api.DeleteChatPhoto(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetChatTitle(t *testing.T) {
+ _, err := api.SetChatTitle(
+ groupID,
+ "Echotron Coverage Supergroup",
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetChatDescription(t *testing.T) {
+ _, err := api.SetChatDescription(
+ groupID,
+ fmt.Sprintf(
+ "This supergroup is used to test some of the methods of the Echotron library for Telegram bots.\n\nLast changed: %d",
+ time.Now().Unix(),
+ ),
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPinChatMessage(t *testing.T) {
+ _, err := api.PinChatMessage(
+ groupID,
+ pinMsgID,
+ &PinMessageOptions{
+ DisableNotification: true,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnpinChatMessage(t *testing.T) {
+ _, err := api.UnpinChatMessage(
+ groupID,
+ &UnpinMessageOptions{
+ MessageID: pinMsgID,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnpinAllChatMessages(t *testing.T) {
+ _, err := api.UnpinAllChatMessages(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetChat(t *testing.T) {
+ res, err := api.GetChat(
+ chatID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.Result.Type != "private" && res.Result.Type != "group" &&
+ res.Result.Type != "supergroup" && res.Result.Type != "channel" {
+
+ t.Fatal("wrong chat type, got:", res.Result.Type)
+ }
+}
+
+func TestGetChatAdministrators(t *testing.T) {
+ _, err := api.GetChatAdministrators(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetChatMemberCount(t *testing.T) {
+ _, err := api.GetChatMemberCount(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetChatMember(t *testing.T) {
+ _, err := api.GetChatMember(
+ groupID,
+ chatID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCreateForumTopic(t *testing.T) {
+ res, err := api.CreateForumTopic(
+ groupID,
+ "Test Topic",
+ &CreateTopicOptions{
+ IconColor: Green,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ msgThreadID = res.Result.MessageThreadID
+}
+
+func TestEditForumTopic(t *testing.T) {
+ _, err := api.EditForumTopic(
+ groupID,
+ msgThreadID,
+ &EditTopicOptions{
+ Name: "Testing Topic",
+ IconCustomEmojiID: "5411138633765757782",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCloseForumTopic(t *testing.T) {
+ _, err := api.CloseForumTopic(
+ groupID,
+ msgThreadID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestReopenForumTopic(t *testing.T) {
+ _, err := api.ReopenForumTopic(
+ groupID,
+ msgThreadID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnpinAllForumTopicMessages(t *testing.T) {
+ res, err := api.SendMessage(
+ "Test",
+ groupID,
+ &MessageOptions{
+ MessageThreadID: msgThreadID,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.PinChatMessage(
+ groupID,
+ res.Result.ID,
+ &PinMessageOptions{
+ DisableNotification: true,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = api.UnpinAllForumTopicMessages(
+ groupID,
+ msgThreadID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteForumTopic(t *testing.T) {
+ _, err := api.DeleteForumTopic(
+ groupID,
+ msgThreadID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditGeneralForumTopic(t *testing.T) {
+ _, err := api.EditGeneralForumTopic(
+ groupID,
+ fmt.Sprintf(
+ "General | %d",
+ time.Now().Unix(),
+ ),
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCloseGeneralForumTopic(t *testing.T) {
+ _, err := api.CloseGeneralForumTopic(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestHideGeneralForumTopic(t *testing.T) {
+ _, err := api.HideGeneralForumTopic(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnhideGeneralForumTopic(t *testing.T) {
+ _, err := api.UnhideGeneralForumTopic(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestReopenGeneralForumTopic(t *testing.T) {
+ _, err := api.ReopenGeneralForumTopic(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestUnpinAllGeneralForumTopicMessages(t *testing.T) {
+ _, err := api.UnpinAllGeneralForumTopicMessages(
+ groupID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetUserChatBoosts(t *testing.T) {
+ _, err := api.GetUserChatBoosts(
+ channelID,
+ chatID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetMyCommands(t *testing.T) {
+ opts := &CommandOptions{
+ LanguageCode: "it",
+ Scope: BotCommandScope{Type: BCSTChat, ChatID: chatID},
+ }
+
+ _, err := api.SetMyCommands(
+ opts,
+ commands...,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetMyCommands(t *testing.T) {
+ res, err := api.GetMyCommands(
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, cmd := range res.Result {
+ if !reflect.DeepEqual(*cmd, commands[i]) {
+ t.Logf("expected command in %d: %+v", i, commands[i])
+ t.Logf("command in %d from API: %+v", i, cmd)
+ t.Fatal("error: commands mismatch")
+ }
+ }
+}
+
+func TestDeleteMyCommands(t *testing.T) {
+ _, err := api.DeleteMyCommands(
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetMyName(t *testing.T) {
+ currentBotName = fmt.Sprintf(
+ "Echotron Coverage Bot - %d",
+ time.Now().Unix(),
+ )
+
+ _, err := api.SetMyName(currentBotName, "")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetMyName(t *testing.T) {
+ res, err := api.GetMyName("")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.Result.Name != currentBotName {
+ t.Logf("expected bot name [\"%s\"]\n", currentBotName)
+ t.Logf("got bot name [\"%s\"]\n", res.Result.Name)
+ t.Fatal("error: bot name mismatch")
+ }
+}
+
+func TestSetMyDescription(t *testing.T) {
+ currentBotDesc = fmt.Sprintf(
+ "Echotron Coverage Bot - %d",
+ time.Now().Unix(),
+ )
+
+ _, err := api.SetMyDescription(currentBotDesc, "")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetMyDescription(t *testing.T) {
+ res, err := api.GetMyDescription("")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.Result.Description != currentBotDesc {
+ t.Logf("expected bot description [\"%s\"]\n", currentBotDesc)
+ t.Logf("got bot description [\"%s\"]\n", res.Result.Description)
+ t.Fatal("error: bot description mismatch")
+ }
+}
+
+func TestSetMyShortDescription(t *testing.T) {
+ currentBotShortDesc = fmt.Sprintf(
+ "Echotron Coverage Bot - %d",
+ time.Now().Unix(),
+ )
+
+ _, err := api.SetMyShortDescription(currentBotShortDesc, "")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageText(t *testing.T) {
+ _, err := api.EditMessageText(
+ "edited message",
+ NewMessageID(chatID, msgTmp.ID),
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageTextWithKeyboard(t *testing.T) {
+ _, err := api.EditMessageText(
+ "edited message with keyboard",
+ NewMessageID(chatID, msgTmp.ID),
+ &MessageTextOptions{
+ ReplyMarkup: inlineKeyboard,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageCaption(t *testing.T) {
+ _, err := api.EditMessageCaption(
+ NewMessageID(chatID, animationTmp.ID),
+ &MessageCaptionOptions{
+ Caption: "TestEditMessageCaption",
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageMedia(t *testing.T) {
+ _, err := api.EditMessageMedia(
+ NewMessageID(chatID, animationTmp.ID),
+ InputMediaAnimation{
+ Type: MediaTypeAnimation,
+ Media: NewInputFileID(animationID),
+ Caption: "TestEditMessageMedia",
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageMediaBytes(t *testing.T) {
+ _, err := api.EditMessageMedia(
+ NewMessageID(chatID, animationTmp.ID),
+ InputMediaAnimation{
+ Type: MediaTypeAnimation,
+ Media: NewInputFilePath("assets/tests/animation.mp4"),
+ Caption: "TestEditMessageMediaBytes",
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageMediaURL(t *testing.T) {
+ _, err := api.EditMessageMedia(
+ NewMessageID(chatID, animationTmp.ID),
+ InputMediaAnimation{
+ Type: MediaTypeAnimation,
+ Media: NewInputFileURL(animationURL),
+ Caption: "TestEditMessageMediaURL",
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditMessageReplyMarkup(t *testing.T) {
+ _, err := api.EditMessageReplyMarkup(
+ NewMessageID(chatID, msgTmp.ID),
+ &MessageReplyMarkupOptions{
+ ReplyMarkup: inlineKeyboardEdit,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestStopPoll(t *testing.T) {
+ _, err := api.StopPoll(
+ chatID,
+ pollTmp.ID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteMessage(t *testing.T) {
+ _, err := api.DeleteMessage(
+ chatID,
+ msgTmp.ID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteMessages(t *testing.T) {
+ msg, _ := api.SendMessage(
+ "TestDeleteMessages",
+ chatID,
+ nil,
+ )
+
+ _, err := api.DeleteMessages(
+ chatID,
+ []int{msgTmp.ID, msg.Result.ID},
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteMessagesWrongMsgIDs(t *testing.T) {
+ _, err := api.DeleteMessages(
+ chatID,
+ []int{},
+ )
+
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+}
diff --git a/shared/echotron/apierror.go b/shared/echotron/apierror.go
new file mode 100644
index 0000000..7974046
--- /dev/null
+++ b/shared/echotron/apierror.go
@@ -0,0 +1,42 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import "fmt"
+
+// APIError represents an error returned by the Telegram API.
+type APIError struct {
+ desc string
+ code int
+}
+
+// ErrorCode returns the error code received from the Telegram API.
+func (a *APIError) ErrorCode() int {
+ return a.code
+}
+
+// Description returns the error description received from the Telegram API.
+func (a *APIError) Description() string {
+ return a.desc
+}
+
+// Error returns the error string.
+func (a *APIError) Error() string {
+ return fmt.Sprintf("API error: %d %s", a.code, a.desc)
+}
diff --git a/shared/echotron/apierror_test.go b/shared/echotron/apierror_test.go
new file mode 100644
index 0000000..0f07f9f
--- /dev/null
+++ b/shared/echotron/apierror_test.go
@@ -0,0 +1,17 @@
+package echotron
+
+import "testing"
+
+var a APIError
+
+func TestErrorCode(_ *testing.T) {
+ a.ErrorCode()
+}
+
+func TestDescription(_ *testing.T) {
+ a.Description()
+}
+
+func TestError(_ *testing.T) {
+ _ = a.Error()
+}
diff --git a/shared/echotron/assets/banner.jpg b/shared/echotron/assets/banner.jpg
new file mode 100644
index 0000000..bf38ecb
Binary files /dev/null and b/shared/echotron/assets/banner.jpg differ
diff --git a/shared/echotron/assets/logo.png b/shared/echotron/assets/logo.png
new file mode 100644
index 0000000..fd0ddfd
Binary files /dev/null and b/shared/echotron/assets/logo.png differ
diff --git a/shared/echotron/assets/readme_banner.png b/shared/echotron/assets/readme_banner.png
new file mode 100644
index 0000000..0766314
Binary files /dev/null and b/shared/echotron/assets/readme_banner.png differ
diff --git a/shared/echotron/assets/tests/animation.mp4 b/shared/echotron/assets/tests/animation.mp4
new file mode 100644
index 0000000..1f645e0
Binary files /dev/null and b/shared/echotron/assets/tests/animation.mp4 differ
diff --git a/shared/echotron/assets/tests/audio.mp3 b/shared/echotron/assets/tests/audio.mp3
new file mode 100644
index 0000000..90c09c2
Binary files /dev/null and b/shared/echotron/assets/tests/audio.mp3 differ
diff --git a/shared/echotron/assets/tests/audio_inv.mp3 b/shared/echotron/assets/tests/audio_inv.mp3
new file mode 100644
index 0000000..75a701f
Binary files /dev/null and b/shared/echotron/assets/tests/audio_inv.mp3 differ
diff --git a/shared/echotron/assets/tests/document.pdf b/shared/echotron/assets/tests/document.pdf
new file mode 100644
index 0000000..e25081e
Binary files /dev/null and b/shared/echotron/assets/tests/document.pdf differ
diff --git a/shared/echotron/assets/tests/echotron_sticker.png b/shared/echotron/assets/tests/echotron_sticker.png
new file mode 100644
index 0000000..7034472
Binary files /dev/null and b/shared/echotron/assets/tests/echotron_sticker.png differ
diff --git a/shared/echotron/assets/tests/echotron_test.png b/shared/echotron/assets/tests/echotron_test.png
new file mode 100644
index 0000000..7034472
Binary files /dev/null and b/shared/echotron/assets/tests/echotron_test.png differ
diff --git a/shared/echotron/assets/tests/echotron_thumb.jpg b/shared/echotron/assets/tests/echotron_thumb.jpg
new file mode 100644
index 0000000..4d0faa3
Binary files /dev/null and b/shared/echotron/assets/tests/echotron_thumb.jpg differ
diff --git a/shared/echotron/assets/tests/echotron_thumb.png b/shared/echotron/assets/tests/echotron_thumb.png
new file mode 100644
index 0000000..50db086
Binary files /dev/null and b/shared/echotron/assets/tests/echotron_thumb.png differ
diff --git a/shared/echotron/assets/tests/echotron_thumb_inv.jpg b/shared/echotron/assets/tests/echotron_thumb_inv.jpg
new file mode 100644
index 0000000..c0d2d47
Binary files /dev/null and b/shared/echotron/assets/tests/echotron_thumb_inv.jpg differ
diff --git a/shared/echotron/assets/tests/video.webm b/shared/echotron/assets/tests/video.webm
new file mode 100644
index 0000000..72b90e8
Binary files /dev/null and b/shared/echotron/assets/tests/video.webm differ
diff --git a/shared/echotron/assets/tests/video_note.mp4 b/shared/echotron/assets/tests/video_note.mp4
new file mode 100644
index 0000000..d1ef9f3
Binary files /dev/null and b/shared/echotron/assets/tests/video_note.mp4 differ
diff --git a/shared/echotron/chatadminrights.go b/shared/echotron/chatadminrights.go
new file mode 100644
index 0000000..4597290
--- /dev/null
+++ b/shared/echotron/chatadminrights.go
@@ -0,0 +1,64 @@
+/*
+ * Echotron
+ * Copyright (C) 2022 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+// ChatAdministratorRights represents the rights of an administrator in a chat.
+type ChatAdministratorRights struct {
+ IsAnonymous bool `json:"is_anonymous"`
+ CanManageChat bool `json:"can_manage_chat"`
+ CanDeleteMessages bool `json:"can_delete_messages"`
+ CanManageVideoChats bool `json:"can_manage_video_chats"`
+ CanRestrictMembers bool `json:"can_restrict_members"`
+ CanPromoteMembers bool `json:"can_promote_members"`
+ CanChangeInfo bool `json:"can_change_info"`
+ CanInviteUsers bool `json:"can_invite_users"`
+ CanPostStories bool `json:"can_post_stories"`
+ CanEditStories bool `json:"can_edit_stories"`
+ CanDeleteStories bool `json:"can_delete_stories"`
+ CanPostMessages bool `json:"can_post_messages,omitempty"`
+ CanEditMessages bool `json:"can_edit_messages,omitempty"`
+ CanPinMessages bool `json:"can_pin_messages,omitempty"`
+ CanManageTopics bool `json:"can_manage_topics,omitempty"`
+}
+
+// SetMyDefaultAdministratorRightsOptions contains the optional parameters used by
+// the SetMyDefaultAdministratorRights method.
+type SetMyDefaultAdministratorRightsOptions struct {
+ Rights ChatAdministratorRights `query:"rights"`
+ ForChannels bool `query:"for_channels"`
+}
+
+// GetMyDefaultAdministratorRightsOptions contains the optional parameters used by
+// the GetMyDefaultAdministratorRights method.
+type GetMyDefaultAdministratorRightsOptions struct {
+ ForChannels bool `query:"for_channels"`
+}
+
+// SetMyDefaultAdministratorRights is used to change the default administrator rights
+// requested by the bot when it's added as an administrator to groups or channels.
+// These rights will be suggested to users, but they are are free to modify the list
+// before adding the bot.
+func (a API) SetMyDefaultAdministratorRights(opts *SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
+ return res, client.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
+}
+
+// GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot.
+func (a API) GetMyDefaultAdministratorRights(opts *GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
+ return res, client.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
+}
diff --git a/shared/echotron/chatadminrights_test.go b/shared/echotron/chatadminrights_test.go
new file mode 100644
index 0000000..c3a0d43
--- /dev/null
+++ b/shared/echotron/chatadminrights_test.go
@@ -0,0 +1,48 @@
+package echotron
+
+import (
+ "reflect"
+ "testing"
+)
+
+var (
+ rights = ChatAdministratorRights{
+ IsAnonymous: true,
+ CanManageChat: true,
+ CanDeleteMessages: true,
+ CanManageVideoChats: true,
+ CanRestrictMembers: true,
+ CanPromoteMembers: true,
+ CanChangeInfo: true,
+ CanInviteUsers: true,
+ CanPostStories: true,
+ CanEditStories: true,
+ CanDeleteStories: true,
+ }
+)
+
+func TestSetMyDefaultAdministratorRights(t *testing.T) {
+ _, err := api.SetMyDefaultAdministratorRights(
+ &SetMyDefaultAdministratorRightsOptions{
+ Rights: rights,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetMyDefaultAdministratorRights(t *testing.T) {
+ res, err := api.GetMyDefaultAdministratorRights(nil)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !reflect.DeepEqual(*res.Result, rights) {
+ t.Logf("expected: %+v", rights)
+ t.Logf("got: %+v", res.Result)
+ t.Fatal("error: chat administrator rights mismatch")
+ }
+}
diff --git a/shared/echotron/dispatcher.go b/shared/echotron/dispatcher.go
new file mode 100644
index 0000000..74b3af6
--- /dev/null
+++ b/shared/echotron/dispatcher.go
@@ -0,0 +1,226 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "compress/gzip"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "sync"
+)
+
+type smap sync.Map
+
+func (s *smap) load(id int64) (Bot, bool) {
+ bot, ok := (*sync.Map)(s).Load(id)
+ if ok {
+ return bot.(Bot), ok
+ }
+ return nil, ok
+}
+
+func (s *smap) store(id int64, bot Bot) {
+ (*sync.Map)(s).Store(id, bot)
+}
+
+func (s *smap) delete(id int64) {
+ (*sync.Map)(s).Delete(id)
+}
+
+// Bot is the interface that must be implemented by your definition of
+// the struct thus it represent each open session with a user on Telegram.
+type Bot interface {
+ // Update will be called upon receiving any update from Telegram.
+ Update(*Update)
+}
+
+// NewBotFn is called every time echotron receives an update with a chat ID never
+// encountered before.
+type NewBotFn func(chatId int64) Bot
+
+// The Dispatcher passes the updates from the Telegram Bot API to the Bot instance
+// associated with each chatID. When a new chat ID is found, the provided function
+// of type NewBotFn will be called.
+type Dispatcher struct {
+ sessions smap
+ newBot NewBotFn
+ updates chan *Update
+ httpServer *http.Server
+ api API
+}
+
+// NewDispatcher returns a new instance of the Dispatcher object.
+// Calls the Update function of the bot associated with each chat ID.
+// If a new chat ID is found, newBotFn will be called first.
+func NewDispatcher(token string, newBotFn NewBotFn) *Dispatcher {
+ d := &Dispatcher{
+ api: NewAPI(token),
+ newBot: newBotFn,
+ updates: make(chan *Update),
+ }
+ go d.listen()
+ return d
+}
+
+// DelSession deletes the Bot instance, seen as a session, from the
+// map with all of them.
+func (d *Dispatcher) DelSession(chatID int64) {
+ d.sessions.delete(chatID)
+}
+
+// AddSession allows to arbitrarily create a new Bot instance.
+func (d *Dispatcher) AddSession(chatID int64) {
+ d.sessions.store(chatID, d.newBot(chatID))
+}
+
+// Poll is a wrapper function for PollOptions.
+func (d *Dispatcher) Poll() error {
+ return d.PollOptions(true, UpdateOptions{Timeout: 120})
+}
+
+// PollOptions starts the polling loop so that the dispatcher calls the function Update
+// upon receiving any update from Telegram.
+func (d *Dispatcher) PollOptions(dropPendingUpdates bool, opts UpdateOptions) error {
+ var (
+ timeout = opts.Timeout
+ isFirstRun = true
+ )
+
+ // deletes webhook if present to run in long polling mode
+ if _, err := d.api.DeleteWebhook(dropPendingUpdates); err != nil {
+ return err
+ }
+
+ for {
+ if isFirstRun {
+ opts.Timeout = 0
+ }
+
+ response, err := d.api.GetUpdates(&opts)
+ if err != nil {
+ return err
+ }
+
+ if !dropPendingUpdates || !isFirstRun {
+ for _, u := range response.Result {
+ d.updates <- u
+ }
+ }
+
+ if l := len(response.Result); l > 0 {
+ opts.Offset = response.Result[l-1].ID + 1
+ }
+
+ if isFirstRun {
+ isFirstRun = false
+ opts.Timeout = timeout
+ }
+ }
+}
+
+func (d *Dispatcher) instance(chatID int64) Bot {
+ bot, ok := d.sessions.load(chatID)
+ if !ok {
+ bot = d.newBot(chatID)
+ d.sessions.store(chatID, bot)
+ }
+ return bot
+}
+
+func (d *Dispatcher) listen() {
+ for update := range d.updates {
+ bot := d.instance(update.ChatID())
+ go bot.Update(update)
+ }
+}
+
+// ListenWebhook is a wrapper function for ListenWebhookOptions.
+func (d *Dispatcher) ListenWebhook(webhookURL string) error {
+ return d.ListenWebhookOptions(webhookURL, false, nil)
+}
+
+// ListenWebhookOptions sets a webhook and listens for incoming updates.
+// The webhookUrl should be provided in the following format: ':/',
+// eg: 'https://example.com:443/bot_token'.
+// ListenWebhook will then proceed to communicate the webhook url '/' to Telegram
+// and run a webserver that listens to ':' and handles the path.
+func (d *Dispatcher) ListenWebhookOptions(webhookURL string, dropPendingUpdates bool, opts *WebhookOptions) error {
+ u, err := url.Parse(webhookURL)
+ if err != nil {
+ return err
+ }
+
+ whURL := fmt.Sprintf("%s%s", u.Hostname(), u.EscapedPath())
+ if _, err = d.api.SetWebhook(whURL, dropPendingUpdates, opts); err != nil {
+ return err
+ }
+
+ if d.httpServer != nil {
+ mux := http.NewServeMux()
+ mux.Handle("/", d.httpServer.Handler)
+ mux.HandleFunc(u.EscapedPath(), d.HandleWebhook)
+ d.httpServer.Handler = mux
+ return d.httpServer.ListenAndServe()
+ }
+ http.HandleFunc(u.EscapedPath(), d.HandleWebhook)
+ return http.ListenAndServe(fmt.Sprintf(":%s", u.Port()), nil)
+}
+
+// SetHTTPServer allows to set a custom http.Server for ListenWebhook and ListenWebhookOptions.
+func (d *Dispatcher) SetHTTPServer(s *http.Server) {
+ d.httpServer = s
+}
+
+// HandleWebhook is the http.HandlerFunc for the webhook URL.
+// Useful if you've already a http server running and want to handle the request yourself.
+func (d *Dispatcher) HandleWebhook(w http.ResponseWriter, r *http.Request) {
+ var update Update
+
+ jsn, err := readRequest(r)
+ if err != nil {
+ log.Println("echotron.Dispatcher", "HandleWebhook", err)
+ return
+ }
+
+ if err := json.Unmarshal(jsn, &update); err != nil {
+ log.Println("echotron.Dispatcher", "HandleWebhook", err)
+ return
+ }
+
+ d.updates <- &update
+}
+
+func readRequest(r *http.Request) ([]byte, error) {
+ switch r.Header.Get("Content-Encoding") {
+ case "gzip":
+ reader, err := gzip.NewReader(r.Body)
+ if err != nil {
+ return []byte{}, err
+ }
+ defer reader.Close()
+ return io.ReadAll(reader)
+
+ default:
+ return io.ReadAll(r.Body)
+ }
+}
diff --git a/shared/echotron/dispatcher_test.go b/shared/echotron/dispatcher_test.go
new file mode 100644
index 0000000..0d27eee
--- /dev/null
+++ b/shared/echotron/dispatcher_test.go
@@ -0,0 +1,175 @@
+package echotron
+
+import (
+ "testing"
+ "time"
+)
+
+type test struct{}
+
+func (t test) Update(_ *Update) {}
+
+var dsp *Dispatcher
+
+func TestNewDispatcher(t *testing.T) {
+ if dsp = NewDispatcher("token", func(_ int64) Bot { return test{} }); dsp == nil {
+ t.Fatal("dispatcher is nil")
+ }
+}
+
+func TestAddSession(t *testing.T) {
+ dsp.AddSession(0)
+
+ if _, ok := dsp.sessions.load(0); !ok {
+ t.Fatal("could not add session")
+ }
+}
+
+func TestDelSession(t *testing.T) {
+ dsp.DelSession(0)
+
+ if _, ok := dsp.sessions.load(0); ok {
+ t.Fatal("could not delete session")
+ }
+}
+
+func TestListenWebhook(_ *testing.T) {
+ dsp.ListenWebhook("http://example.com:8443/test")
+ time.Sleep(time.Second)
+}
+
+func TestPoll(_ *testing.T) {
+ dsp.Poll()
+
+ dsp.updates <- &Update{}
+
+ dsp.updates <- &Update{
+ ChatJoinRequest: &ChatJoinRequest{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ ChatBoost: &ChatBoostUpdated{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ RemovedChatBoost: &ChatBoostRemoved{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ Message: &Message{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ EditedMessage: &Message{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ ChannelPost: &Message{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ EditedChannelPost: &Message{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ BusinessConnection: &BusinessConnection{
+ User: User{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ BusinessMessage: &Message{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ EditedBusinessMessage: &Message{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ DeletedBusinessMessages: &BusinessMessagesDeleted{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ MessageReaction: &MessageReactionUpdated{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ MessageReactionCount: &MessageReactionCountUpdated{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ InlineQuery: &InlineQuery{
+ From: &User{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ ChosenInlineResult: &ChosenInlineResult{
+ From: &User{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ CallbackQuery: &CallbackQuery{
+ Message: &Message{
+ Chat: Chat{ID: 0},
+ },
+ },
+ }
+
+ dsp.updates <- &Update{
+ ShippingQuery: &ShippingQuery{
+ From: User{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ PreCheckoutQuery: &PreCheckoutQuery{
+ From: User{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ PollAnswer: &PollAnswer{
+ User: &User{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ MyChatMember: &ChatMemberUpdated{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ dsp.updates <- &Update{
+ ChatMember: &ChatMemberUpdated{
+ Chat: Chat{ID: 0},
+ },
+ }
+
+ time.Sleep(time.Second)
+}
diff --git a/shared/echotron/games.go b/shared/echotron/games.go
new file mode 100644
index 0000000..9addcc2
--- /dev/null
+++ b/shared/echotron/games.go
@@ -0,0 +1,73 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import "net/url"
+
+// Game represents a game.
+type Game struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Photo []PhotoSize `json:"photo"`
+ Text string `json:"text,omitempty"`
+ TextEntities []MessageEntity `json:"text_entities,omitempty"`
+ Animation Animation `json:"animation,omitempty"`
+}
+
+// CallbackGame is a placeholder, currently holds no information.
+type CallbackGame struct{}
+
+// GameHighScore represents one row of the high scores table for a game.
+type GameHighScore struct {
+ User User `json:"user"`
+ Position int `json:"position"`
+ Score int `json:"score"`
+}
+
+// GameScoreOptions contains the optional parameters used in SetGameScore method.
+type GameScoreOptions struct {
+ Force bool `query:"force"`
+ DisableEditMessage bool `query:"disable_edit_message"`
+}
+
+// SendGame is used to send a Game.
+func (a API) SendGame(gameShortName string, chatID int64, opts *BaseOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("game_short_name", gameShortName)
+ return res, client.get(a.base, "sendGame", addValues(vals, opts), &res)
+}
+
+// SetGameScore is used to set the score of the specified user in a game.
+func (a API) SetGameScore(userID int64, score int, msgID MessageIDOptions, opts *GameScoreOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("score", itoa(int64(score)))
+ return res, client.get(a.base, "setGameScore", addValues(addValues(vals, msgID), opts), &res)
+}
+
+// GetGameHighScores is used to get data for high score tables.
+func (a API) GetGameHighScores(userID int64, opts MessageIDOptions) (res APIResponseGameHighScore, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ return res, client.get(a.base, "getGameHighScores", addValues(vals, opts), &res)
+}
diff --git a/shared/echotron/games_test.go b/shared/echotron/games_test.go
new file mode 100644
index 0000000..79b1aa1
--- /dev/null
+++ b/shared/echotron/games_test.go
@@ -0,0 +1,74 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import "testing"
+
+var (
+ gameMsgTmp *Message
+ highScores []*GameHighScore
+)
+
+func TestSendGame(t *testing.T) {
+ resp, err := api.SendGame(
+ "echotron_coverage_game",
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ gameMsgTmp = resp.Result
+}
+
+func TestGameHighScores(t *testing.T) {
+ resp, err := api.GetGameHighScores(
+ chatID,
+ NewMessageID(chatID, gameMsgTmp.ID),
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ highScores = resp.Result
+}
+
+func TestSetGameScore(t *testing.T) {
+ var score int
+
+ if len(highScores) > 0 {
+ score = highScores[0].Score + 1
+ }
+
+ _, err := api.SetGameScore(
+ chatID,
+ score,
+ NewMessageID(chatID, gameMsgTmp.ID),
+ &GameScoreOptions{
+ Force: true,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/shared/echotron/go.mod b/shared/echotron/go.mod
new file mode 100644
index 0000000..fb5c529
--- /dev/null
+++ b/shared/echotron/go.mod
@@ -0,0 +1,5 @@
+module github.com/NicoNex/echotron/v3
+
+go 1.19
+
+require golang.org/x/time v0.5.0
diff --git a/shared/echotron/go.sum b/shared/echotron/go.sum
new file mode 100644
index 0000000..a2652c5
--- /dev/null
+++ b/shared/echotron/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
diff --git a/shared/echotron/helpers.go b/shared/echotron/helpers.go
new file mode 100644
index 0000000..aa462aa
--- /dev/null
+++ b/shared/echotron/helpers.go
@@ -0,0 +1,164 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+)
+
+// content contains a file's name, its type and its data.
+type content struct {
+ fname string
+ ftype string
+ fdata []byte
+}
+
+func check(r APIResponse) error {
+ if b := r.Base(); !b.Ok {
+ return &APIError{code: b.ErrorCode, desc: b.Description}
+ }
+ return nil
+}
+
+func processMedia(media, thumbnail InputFile) (im mediaEnvelope, cnt []content, err error) {
+ switch {
+ case media.id != "":
+ im = mediaEnvelope{
+ media: media.id,
+ thumbnail: "",
+ }
+
+ case media.url != "":
+ im = mediaEnvelope{
+ media: media.url,
+ thumbnail: "",
+ }
+
+ case media.path != "" && len(media.content) == 0:
+ if media.content, media.path, err = readFile(media); err != nil {
+ return
+ }
+ fallthrough
+
+ case media.path != "" && len(media.content) > 0:
+ cnt = append(cnt, content{media.path, media.path, media.content})
+ im = mediaEnvelope{
+ media: fmt.Sprintf("attach://%s", media.path),
+ thumbnail: "",
+ }
+ }
+
+ switch {
+ case thumbnail.path != "" && len(thumbnail.content) == 0:
+ if thumbnail.content, thumbnail.path, err = readFile(thumbnail); err != nil {
+ return
+ }
+ fallthrough
+
+ case thumbnail.path != "" && len(thumbnail.content) > 0:
+ cnt = append(cnt, content{thumbnail.path, thumbnail.path, thumbnail.content})
+ im.thumbnail = fmt.Sprintf("attach://%s", thumbnail.path)
+ }
+
+ return
+}
+
+func processSticker(sticker InputFile) (se stickerEnvelope, cnt []content, err error) {
+ switch {
+ case sticker.id != "":
+ se.Sticker = sticker.id
+
+ case sticker.url != "":
+ se.Sticker = sticker.url
+
+ case sticker.path != "" && len(sticker.content) == 0:
+ if sticker.content, sticker.path, err = readFile(sticker); err != nil {
+ return
+ }
+ fallthrough
+
+ case sticker.path != "" && len(sticker.content) > 0:
+ cnt = append(cnt, content{sticker.path, sticker.path, sticker.content})
+ se.Sticker = fmt.Sprintf("attach://%s", sticker.path)
+ }
+
+ return
+}
+
+func readFile(im InputFile) (content []byte, path string, err error) {
+ content, err = os.ReadFile(im.path)
+ if err != nil {
+ return
+ }
+ path = filepath.Base(im.path)
+
+ return
+}
+
+func toContent(ftype string, f InputFile) (content, error) {
+ if f.path != "" && len(f.content) == 0 {
+ var err error
+ if f.content, f.path, err = readFile(f); err != nil {
+ return content{}, err
+ }
+ }
+
+ return content{f.path, ftype, f.content}, nil
+}
+
+func toInputMedia(media []GroupableInputMedia) (ret []InputMedia) {
+ ret = make([]InputMedia, len(media))
+
+ for i, v := range media {
+ ret[i] = v
+ }
+
+ return ret
+}
+
+func joinURL(base, endpoint string, vals url.Values) (addr string, err error) {
+ addr, err = url.JoinPath(base, endpoint)
+ if err != nil {
+ return
+ }
+
+ if vals != nil {
+ if queries := vals.Encode(); queries != "" {
+ addr = fmt.Sprintf("%s?%s", addr, queries)
+ }
+ }
+
+ return
+}
+
+func itoa(i int64) string {
+ return strconv.FormatInt(i, 10)
+}
+
+func ftoa(f float64) string {
+ return strconv.FormatFloat(f, 'f', -1, 64)
+}
+
+func btoa(b bool) string {
+ return strconv.FormatBool(b)
+}
diff --git a/shared/echotron/inline.go b/shared/echotron/inline.go
new file mode 100644
index 0000000..1d545c0
--- /dev/null
+++ b/shared/echotron/inline.go
@@ -0,0 +1,601 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "net/url"
+)
+
+// InlineQueryType is a custom type for the various InlineQueryResult*'s Type field.
+type InlineQueryType string
+
+// These are all the possible types for the various InlineQueryResult*'s Type field.
+const (
+ InlineArticle InlineQueryType = "article"
+ InlinePhoto = "photo"
+ InlineGIF = "gif"
+ InlineMPEG4GIF = "mpeg4_gif"
+ InlineVideo = "video"
+ InlineAudio = "audio"
+ InlineVoice = "voice"
+ InlineDocument = "document"
+ InlineLocation = "location"
+ InlineVenue = "venue"
+ InlineContact = "contact"
+ InlineGame = "game"
+ InlineSticker = "sticker"
+)
+
+// InlineQuery represents an incoming inline query.
+// When the user sends an empty query, your bot could return some default or trending results.
+type InlineQuery struct {
+ From *User `json:"from"`
+ Location *Location `json:"location,omitempty"`
+ ID string `json:"id"`
+ Query string `json:"query"`
+ Offset string `json:"offset"`
+ ChatType string `json:"chat_type,omitempty"`
+}
+
+// ChosenInlineResult represents a result of an inline query that was chosen by the user and sent to their chat partner.
+type ChosenInlineResult struct {
+ ResultID string `json:"result_id"`
+ From *User `json:"from"`
+ Location *Location `json:"location,omitempty"`
+ InlineMessageID string `json:"inline_message_id,omitempty"`
+ Query string `json:"query"`
+}
+
+// InlineQueryResult represents an interface that implements all the various InlineQueryResult* types.
+type InlineQueryResult interface {
+ ImplementsInlineQueryResult()
+}
+
+// InlineQueryResultArticle represents a link to an article or web page.
+type InlineQueryResultArticle struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Description string `json:"description,omitempty"`
+ ThumbnailURL string `json:"thumbnail_url,omitempty"`
+ URL string `json:"url,omitempty"`
+ ThumbnailWidth int `json:"thumbnail_width,omitempty"`
+ ThumbnailHeight int `json:"thumbnail_height,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultArticle) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultPhoto represents a link to a photo.
+// By default, this photo will be sent by the user with optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
+type InlineQueryResultPhoto struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Title string `json:"title,omitempty"`
+ ThumbnailURL string `json:"thumbnail_url"`
+ PhotoURL string `json:"photo_url"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ ID string `json:"id"`
+ Description string `json:"description,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ Type InlineQueryType `json:"type"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ PhotoHeight int `json:"photo_height,omitempty"`
+ PhotoWidth int `json:"photo_width,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultPhoto) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultGif represents a link to an animated GIF file.
+// By default, this animated GIF file will be sent by the user with optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
+type InlineQueryResultGif struct {
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ Title string `json:"title,omitempty"`
+ GifURL string `json:"gif_url"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ThumbnailURL string `json:"thumbnail_url"`
+ ID string `json:"id"`
+ ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
+ Type InlineQueryType `json:"type"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ GifDuration int `json:"gif_duration,omitempty"`
+ GifHeight int `json:"gif_height,omitempty"`
+ GifWidth int `json:"gif_width,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultGif) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultMpeg4Gif represents a link to a video animation (H.264/MPEG-4 AVC video without sound).
+// By default, this animated MPEG-4 file will be sent by the user with optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
+type InlineQueryResultMpeg4Gif struct {
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ Title string `json:"title,omitempty"`
+ Mpeg4URL string `json:"mpeg4_url"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ThumbnailURL string `json:"thumbnail_url"`
+ ID string `json:"id"`
+ ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
+ Type InlineQueryType `json:"type"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ Mpeg4Duration int `json:"mpeg4_duration,omitempty"`
+ Mpeg4Height int `json:"mpeg4_height,omitempty"`
+ Mpeg4Width int `json:"mpeg4_width,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultMpeg4Gif) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultVideo represents a link to a page containing an embedded video player or a video file.
+// By default, this video file will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
+type InlineQueryResultVideo struct {
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ Description string `json:"description,omitempty"`
+ MimeType string `json:"mime_type"`
+ ThumbnailURL string `json:"thumbnail_url"`
+ Title string `json:"title"`
+ Caption string `json:"caption,omitempty"`
+ ID string `json:"id"`
+ VideoURL string `json:"video_url"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Type InlineQueryType `json:"type"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ VideoHeight int `json:"video_height,omitempty"`
+ VideoDuration int `json:"video_duration,omitempty"`
+ VideoWidth int `json:"video_width,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultVideo) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultAudio represents a link to an MP3 audio file.
+// By default, this audio file will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the audio.
+type InlineQueryResultAudio struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ AudioURL string `json:"audio_url"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Performer string `json:"performer,omitempty"`
+ Title string `json:"title"`
+ Caption string `json:"caption,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ AudioDuration int `json:"audio_duration,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultAudio) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultVoice represents a link to a voice recording in an .OGG container encoded with OPUS.
+// By default, this voice recording will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the the voice message.
+type InlineQueryResultVoice struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ VoiceURL string `json:"voice_url"`
+ Title string `json:"title"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ VoiceDuration int `json:"voice_duration,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultVoice) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultDocument represents a link to a file.
+// By default, this file will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the file.
+// Currently, only .PDF and .ZIP files can be sent using this method.
+type InlineQueryResultDocument struct {
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ MimeType string `json:"mime_type"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ ThumbnailURL string `json:"thumbnail_url,omitempty"`
+ DocumentURL string `json:"document_url"`
+ Title string `json:"title"`
+ Description string `json:"description,omitempty"`
+ ID string `json:"id"`
+ Type InlineQueryType `json:"type"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ ThumbnailWidth int `json:"thumbnail_width,omitempty"`
+ ThumbnailHeight int `json:"thumbnail_height,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultDocument) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultLocation represents a location on a map.
+// By default, the location will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the location.
+type InlineQueryResultLocation struct {
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ ID string `json:"id"`
+ ThumbnailURL string `json:"thumbnail_url,omitempty"`
+ Title string `json:"title"`
+ Type InlineQueryType `json:"type"`
+ LivePeriod int `json:"live_period,omitempty"`
+ HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"`
+ ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"`
+ Longitude float64 `json:"longitude"`
+ Latitude float64 `json:"latitude"`
+ ThumbnailWidth int `json:"thumbnail_width,omitempty"`
+ ThumbnailHeight int `json:"thumbnail_height,omitempty"`
+ Heading int `json:"heading,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultLocation) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultVenue represents a venue.
+// By default, the venue will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the venue.
+type InlineQueryResultVenue struct {
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ GooglePlaceType string `json:"google_place_type,omitempty"`
+ ThumbnailURL string `json:"thumbnail_url,omitempty"`
+ Title string `json:"title"`
+ Address string `json:"address"`
+ FoursquareID string `json:"foursquare_id,omitempty"`
+ ID string `json:"id"`
+ GooglePlaceID string `json:"google_place_id,omitempty"`
+ FoursquareType string `json:"foursquare_type,omitempty"`
+ Type InlineQueryType `json:"type"`
+ Longitude float64 `json:"longitude"`
+ Latitude float64 `json:"latitude"`
+ ThumbnailWidth int `json:"thumbnail_width,omitempty"`
+ ThumbnailHeight int `json:"thumbnail_height,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultVenue) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultContact represents a contact with a phone number.
+// By default, this contact will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the contact.
+type InlineQueryResultContact struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ ID string `json:"id"`
+ PhoneNumber string `json:"phone_number"`
+ FirstName string `json:"first_name"`
+ VCard string `json:"vcard,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ThumbnailURL string `json:"thumbnail_url,omitempty"`
+ LastName string `json:"last_name,omitempty"`
+ ThumbnailWidth int `json:"thumbnail_width,omitempty"`
+ ThumbnailHeight int `json:"thumbnail_height,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultContact) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultGame represents a Game.
+type InlineQueryResultGame struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ GameShortName string `json:"game_short_name"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultGame) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedPhoto represents a link to a photo stored on the Telegram servers.
+// By default, this photo will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
+type InlineQueryResultCachedPhoto struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ Description string `json:"description,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ PhotoFileID string `json:"photo_file_id"`
+ Title string `json:"title,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedPhoto) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedGif represents a link to an animated GIF file stored on the Telegram servers.
+// By default, this animated GIF file will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with specified content instead of the animation.
+type InlineQueryResultCachedGif struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ Title string `json:"title,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ ID string `json:"id"`
+ GifFileID string `json:"gif_file_id"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedGif) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedMpeg4Gif represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers.
+// By default, this animated MPEG-4 file will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
+type InlineQueryResultCachedMpeg4Gif struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ Title string `json:"title,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ ID string `json:"id"`
+ Mpeg4FileID string `json:"mpeg4_file_id"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedMpeg4Gif) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedSticker represents a link to a sticker stored on the Telegram servers.
+// By default, this sticker will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the sticker.
+type InlineQueryResultCachedSticker struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ StickerFileID string `json:"sticker_file_id"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedSticker) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedDocument represents a link to a file stored on the Telegram servers.
+// By default, this file will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the file.
+type InlineQueryResultCachedDocument struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ Description string `json:"description,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Title string `json:"title"`
+ DocumentFileID string `json:"document_file_id"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedDocument) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedVideo represents a link to a video file stored on the Telegram servers.
+// By default, this video file will be sent by the user with an optional caption.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
+type InlineQueryResultCachedVideo struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ Description string `json:"description,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ VideoFileID string `json:"video_file_id"`
+ Title string `json:"title"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedVideo) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedVoice represents a link to a voice message stored on the Telegram servers.
+// By default, this voice message will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the voice message.
+type InlineQueryResultCachedVoice struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ Type InlineQueryType `json:"type"`
+ Title string `json:"title"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ ID string `json:"id"`
+ VoiceFileID string `json:"voice_file_id"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedVoice) ImplementsInlineQueryResult() {}
+
+// InlineQueryResultCachedAudio represents a link to an MP3 audio file stored on the Telegram servers.
+// By default, this audio file will be sent by the user.
+// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the audio.
+type InlineQueryResultCachedAudio struct {
+ ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
+ InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
+ AudioFileID string `json:"audio_file_id"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Type InlineQueryType `json:"type"`
+ ID string `json:"id"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+}
+
+// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
+func (i InlineQueryResultCachedAudio) ImplementsInlineQueryResult() {}
+
+// InputMessageContent represents an interface that implements all the various Input*MessageContent types.
+type InputMessageContent interface {
+ ImplementsInputMessageContent()
+}
+
+// InputTextMessageContent represents the content of a text message to be sent as the result of an inline query.
+type InputTextMessageContent struct {
+ LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
+ MessageText string `json:"message_text"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ Entities []*MessageEntity `json:"entities,omitempty"`
+}
+
+// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
+func (i InputTextMessageContent) ImplementsInputMessageContent() {}
+
+// InputLocationMessageContent represents the content of a location message to be sent as the result of an inline query.
+type InputLocationMessageContent struct {
+ Latitude float64 `json:"latitude"`
+ Longitude float64 `json:"longitude"`
+ HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"`
+ LivePeriod int `json:"live_period,omitempty"`
+ Heading int `json:"heading,omitempty"`
+ ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"`
+}
+
+// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
+func (i InputLocationMessageContent) ImplementsInputMessageContent() {}
+
+// InputVenueMessageContent represents the content of a venue message to be sent as the result of an inline query.
+type InputVenueMessageContent struct {
+ GooglePlaceID string `json:"google_place_id,omitempty"`
+ GooglePlaceType string `json:"google_place_type,omitempty"`
+ Title string `json:"title"`
+ Address string `json:"address"`
+ FoursquareID string `json:"foursquare_id,omitempty"`
+ FoursquareType string `json:"foursquare_type,omitempty"`
+ Latitude float64 `json:"latitude"`
+ Longitude float64 `json:"longitude"`
+}
+
+// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
+func (i InputVenueMessageContent) ImplementsInputMessageContent() {}
+
+// InputContactMessageContent represents the content of a contact message to be sent as the result of an inline query.
+type InputContactMessageContent struct {
+ PhoneNumber string `json:"phone_number"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name,omitempty"`
+ VCard string `json:"vcard,omitempty"`
+}
+
+// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
+func (i InputContactMessageContent) ImplementsInputMessageContent() {}
+
+// InputInvoiceMessageContent represents the content of an invoice message to be sent as the result of an inline query.
+type InputInvoiceMessageContent struct {
+ SuggestedTipAmounts *[]int `json:"suggested_tip_amounts,omitempty"`
+ PhotoURL string `json:"photo_url,omitempty"`
+ Description string `json:"description"`
+ Payload string `json:"string"`
+ ProviderToken string `json:"provider_token,omitempty"`
+ Currency string `json:"currency"`
+ Title string `json:"title"`
+ ProviderData string `json:"provider_data,omitempty"`
+ Prices []LabeledPrice `json:"prices"`
+ PhotoSize int `json:"photo_size,omitempty"`
+ MaxTipAmount int `json:"max_tip_amount,omitempty"`
+ PhotoWidth int `json:"photo_width,omitempty"`
+ PhotoHeight int `json:"photo_height,omitempty"`
+ NeedName bool `json:"need_name,omitempty"`
+ NeedPhoneNumber bool `json:"need_phone_number,omitempty"`
+ NeedEmail bool `json:"need_email,omitempty"`
+ NeedShippingAddress bool `json:"need_shipping_address,omitempty"`
+ SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"`
+ SendEmailToProvider bool `json:"send_email_to_provider,omitempty"`
+ IsFlexible bool `json:"is_flexible,omitempty"`
+}
+
+// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
+func (i InputInvoiceMessageContent) ImplementsInputMessageContent() {}
+
+// InlineQueryResultsButton represents a button to be shown above inline query results.
+// You MUST use exactly one of the fields.
+type InlineQueryResultsButton struct {
+ WebApp WebAppInfo `json:"web_app,omitempty"`
+ StartParameter string `json:"start_parameter,omitempty"`
+ Text string `json:"text"`
+}
+
+// PreparedInlineMessage describes an inline message to be sent by a user of a Mini App.
+type PreparedInlineMessage struct {
+ ID string `json:"id"`
+ ExpirationDate int `json:"expiration_date"`
+}
+
+// InlineQueryOptions is a custom type which contains the various options required by the AnswerInlineQuery method.
+type InlineQueryOptions struct {
+ Button InlineQueryResultsButton `query:"button"`
+ NextOffset string `query:"next_offset"`
+ CacheTime int `query:"cache_time"`
+ IsPersonal bool `query:"is_personal"`
+}
+
+// PreparedInlineMessageOptions is a custom type which contains the various options required by the SavePreparedInlineMessage method.
+type PreparedInlineMessageOptions struct {
+ AllowUserChats bool `query:"allow_user_chats"`
+ AllowBotChats bool `query:"allow_bot_chats"`
+ AllowGroupChats bool `query:"allow_group_chats"`
+ AllowChannelChats bool `query:"allow_channel_chats"`
+}
+
+// AnswerInlineQuery is used to send answers to an inline query.
+func (a API) AnswerInlineQuery(inlineQueryID string, results []InlineQueryResult, opts *InlineQueryOptions) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(results)
+ vals.Set("inline_query_id", inlineQueryID)
+ vals.Set("results", string(jsn))
+ return res, client.get(a.base, "answerInlineQuery", addValues(vals, opts), &res)
+}
+
+// SavePreparedInlineMessage stores a message that can be sent by a user of a Mini App.
+func (a API) SavePreparedInlineMessage(userID int64, result InlineQueryResult, opts *PreparedInlineMessageOptions) (res APIResponsePreparedInlineMessage, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(result)
+ vals.Set("user_id", itoa(userID))
+ vals.Set("result", string(jsn))
+ return res, client.get(a.base, "savePreparedInlineMessage", addValues(vals, opts), &res)
+}
diff --git a/shared/echotron/inline_test.go b/shared/echotron/inline_test.go
new file mode 100644
index 0000000..21f1390
--- /dev/null
+++ b/shared/echotron/inline_test.go
@@ -0,0 +1,128 @@
+package echotron
+
+import "testing"
+
+func TestInlineQueryResultArticle(_ *testing.T) {
+ i := InlineQueryResultArticle{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultPhoto(_ *testing.T) {
+ i := InlineQueryResultPhoto{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultGif(_ *testing.T) {
+ i := InlineQueryResultGif{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultMpeg4Gif(_ *testing.T) {
+ i := InlineQueryResultMpeg4Gif{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultVideo(_ *testing.T) {
+ i := InlineQueryResultVideo{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultAudio(_ *testing.T) {
+ i := InlineQueryResultAudio{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultVoice(_ *testing.T) {
+ i := InlineQueryResultVoice{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultDocument(_ *testing.T) {
+ i := InlineQueryResultDocument{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultLocation(_ *testing.T) {
+ i := InlineQueryResultLocation{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultVenue(_ *testing.T) {
+ i := InlineQueryResultVenue{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultContact(_ *testing.T) {
+ i := InlineQueryResultContact{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultGame(_ *testing.T) {
+ i := InlineQueryResultGame{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedPhoto(_ *testing.T) {
+ i := InlineQueryResultCachedPhoto{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedGif(_ *testing.T) {
+ i := InlineQueryResultCachedGif{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedMpeg4Gif(_ *testing.T) {
+ i := InlineQueryResultCachedMpeg4Gif{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedSticker(_ *testing.T) {
+ i := InlineQueryResultCachedSticker{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedDocument(_ *testing.T) {
+ i := InlineQueryResultCachedDocument{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedVideo(_ *testing.T) {
+ i := InlineQueryResultCachedVideo{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedVoice(_ *testing.T) {
+ i := InlineQueryResultCachedVoice{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInlineQueryResultCachedAudio(_ *testing.T) {
+ i := InlineQueryResultCachedAudio{}
+ i.ImplementsInlineQueryResult()
+}
+
+func TestInputTextMessageContent(_ *testing.T) {
+ i := InputTextMessageContent{}
+ i.ImplementsInputMessageContent()
+}
+
+func TestInputLocationMessageContent(_ *testing.T) {
+ i := InputLocationMessageContent{}
+ i.ImplementsInputMessageContent()
+}
+
+func TestInputVenueMessageContent(_ *testing.T) {
+ i := InputVenueMessageContent{}
+ i.ImplementsInputMessageContent()
+}
+
+func TestInputContactMessageContent(_ *testing.T) {
+ i := InputContactMessageContent{}
+ i.ImplementsInputMessageContent()
+}
+
+func TestInputInvoiceMessageContent(_ *testing.T) {
+ i := InputInvoiceMessageContent{}
+ i.ImplementsInputMessageContent()
+}
diff --git a/shared/echotron/menubutton.go b/shared/echotron/menubutton.go
new file mode 100644
index 0000000..e166704
--- /dev/null
+++ b/shared/echotron/menubutton.go
@@ -0,0 +1,57 @@
+/*
+ * Echotron
+ * Copyright (C) 2022 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+// MenuButtonType is a custom type for the various MenuButton*'s Type field.
+type MenuButtonType string
+
+// These are all the possible types for the various MenuButton*'s Type field.
+const (
+ MenuButtonTypeCommands MenuButtonType = "commands"
+ MenuButtonTypeWebApp = "web_app"
+ MenuButtonTypeDefault = "default"
+)
+
+// MenuButton is a unique type for MenuButtonCommands, MenuButtonWebApp and MenuButtonDefault
+type MenuButton struct {
+ WebApp *WebAppInfo `json:"web_app,omitempty"`
+ Type MenuButtonType `json:"type"`
+ Text string `json:"text,omitempty"`
+}
+
+// SetChatMenuButtonOptions contains the optional parameters used by the SetChatMenuButton method.
+type SetChatMenuButtonOptions struct {
+ MenuButton MenuButton `query:"menu_button"`
+ ChatID int64 `query:"chat_id"`
+}
+
+// GetChatMenuButtonOptions contains the optional parameters used by the GetChatMenuButton method.
+type GetChatMenuButtonOptions struct {
+ ChatID int64 `query:"chat_id"`
+}
+
+// SetChatMenuButton is used to change the bot's menu button in a private chat, or the default menu button.
+func (a API) SetChatMenuButton(opts *SetChatMenuButtonOptions) (res APIResponseBool, err error) {
+ return res, client.get(a.base, "setChatMenuButton", urlValues(opts), &res)
+}
+
+// GetChatMenuButton is used to get the current value of the bot's menu button in a private chat, or the default menu button.
+func (a API) GetChatMenuButton(opts *GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
+ return res, client.get(a.base, "getChatMenuButton", urlValues(opts), &res)
+}
diff --git a/shared/echotron/menubutton_test.go b/shared/echotron/menubutton_test.go
new file mode 100644
index 0000000..e2316ec
--- /dev/null
+++ b/shared/echotron/menubutton_test.go
@@ -0,0 +1,38 @@
+package echotron
+
+import (
+ "reflect"
+ "testing"
+)
+
+var (
+ menuBtn = MenuButton{
+ Type: MenuButtonTypeCommands,
+ }
+)
+
+func TestSetChatMenuButton(t *testing.T) {
+ _, err := api.SetChatMenuButton(
+ &SetChatMenuButtonOptions{
+ MenuButton: menuBtn,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetChatMenuButton(t *testing.T) {
+ res, err := api.GetChatMenuButton(nil)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !reflect.DeepEqual(*res.Result, menuBtn) {
+ t.Logf("expected menu button: %+v", menuBtn)
+ t.Logf("got menu button: %+v", res.Result)
+ t.Fatal("error: menu buttons mismatch")
+ }
+}
diff --git a/shared/echotron/network.go b/shared/echotron/network.go
new file mode 100644
index 0000000..8079f11
--- /dev/null
+++ b/shared/echotron/network.go
@@ -0,0 +1,437 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/time/rate"
+)
+
+type lclient struct {
+ *http.Client
+ *sync.RWMutex
+ cl map[string]*rate.Limiter // chat based limiter
+ gl *rate.Limiter // global limiter
+ climiter func() *rate.Limiter
+}
+
+var client = &lclient{
+ Client: new(http.Client),
+ RWMutex: new(sync.RWMutex),
+ cl: make(map[string]*rate.Limiter),
+ gl: rate.NewLimiter(rate.Every(time.Second/30), 30),
+ climiter: func() *rate.Limiter {
+ return rate.NewLimiter(rate.Every(time.Minute/20), 20)
+ },
+}
+
+// SetGlobalRequestLimit sets the global rate limit for requests to the Telegram API.
+// An interval of 0 disables the rate limiter, allowing unlimited requests.
+// By default the interval of this limiter is set to time.Second/30 and the
+// burstSize is set to 30.
+func SetGlobalRequestLimit(interval time.Duration, burstSize int) {
+ client.Lock()
+ client.gl = rate.NewLimiter(rate.Every(interval), burstSize)
+ client.Unlock()
+}
+
+// SetChatRequestLimit sets the per-chat rate limit for requests to the Telegram API.
+// An interval of 0 disables the rate limiter, allowing unlimited requests.
+// By default the interval of this limiter is set to time.Minute/20 and the
+// burstSize is set to 20.
+func SetChatRequestLimit(interval time.Duration, burstSize int) {
+ client.Lock()
+ client.cl = make(map[string]*rate.Limiter)
+ client.climiter = func() *rate.Limiter {
+ return rate.NewLimiter(rate.Every(interval), burstSize)
+ }
+ client.Unlock()
+}
+
+func (c lclient) wait(chatID string) error {
+ c.RLock()
+ defer c.RUnlock()
+
+ ctx := context.Background()
+ // If the chatID is empty, it's a general API call like GetUpdates, GetMe
+ // and similar, so skip the per-chat request limit wait.
+ if chatID != "" {
+ // If no limiter exists for a chat, create one.
+ l, ok := c.cl[chatID]
+ if !ok {
+ l = c.climiter()
+ c.cl[chatID] = l
+ }
+
+ // Make sure to respect the single chat limit of requests.
+ if err := l.Wait(ctx); err != nil {
+ return err
+ }
+ }
+
+ // Make sure to respect the global limit of requests.
+ return c.gl.Wait(ctx)
+}
+
+func (c lclient) doGet(reqURL string) ([]byte, error) {
+ resp, err := c.Get(reqURL)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+func (c lclient) doPost(reqURL string, files ...content) ([]byte, error) {
+ var (
+ buf = new(bytes.Buffer)
+ w = multipart.NewWriter(buf)
+ )
+
+ for _, f := range files {
+ part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname))
+ if err != nil {
+ return nil, err
+ }
+ part.Write(f.fdata)
+ }
+ w.Close()
+
+ req, err := http.NewRequest(http.MethodPost, reqURL, buf)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Add("Content-Type", w.FormDataContentType())
+
+ res, err := c.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+ return io.ReadAll(res.Body)
+}
+
+func (c lclient) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
+ var form = make(url.Values)
+
+ for k, v := range keyVals {
+ form.Add(k, v)
+ }
+
+ req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(form.Encode()))
+ if err != nil {
+ return nil, err
+ }
+ req.PostForm = form
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+ res, err := c.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+ return io.ReadAll(res.Body)
+}
+
+func (c lclient) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
+ var cnt []content
+
+ if file.id != "" {
+ url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
+ } else if file.url != "" {
+ url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
+ } else if c, e := toContent(fileType, file); e == nil {
+ cnt = append(cnt, c)
+ } else {
+ err = e
+ }
+
+ if c, e := toContent("thumbnail", thumbnail); e == nil {
+ cnt = append(cnt, c)
+ } else {
+ err = e
+ }
+
+ if len(cnt) > 0 {
+ res, err = c.doPost(url, cnt...)
+ } else {
+ res, err = c.doGet(url)
+ }
+ return
+}
+
+func (c lclient) get(base, endpoint string, vals url.Values, v APIResponse) error {
+ url, err := url.JoinPath(base, endpoint)
+ if err != nil {
+ return err
+ }
+
+ if vals != nil {
+ if queries := vals.Encode(); queries != "" {
+ url = fmt.Sprintf("%s?%s", url, queries)
+ }
+ }
+
+ if err := c.wait(vals.Get("chat_id")); err != nil {
+ return err
+ }
+
+ cnt, err := c.doGet(url)
+ if err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal(cnt, v); err != nil {
+ return err
+ }
+ return check(v)
+}
+
+func (c lclient) postFile(base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values, v APIResponse) error {
+ url, err := joinURL(base, endpoint, vals)
+ if err != nil {
+ return err
+ }
+
+ if err := c.wait(vals.Get("chat_id")); err != nil {
+ return err
+ }
+
+ cnt, err := c.sendFile(file, thumbnail, url, fileType)
+ if err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal(cnt, v); err != nil {
+ return err
+ }
+ return check(v)
+}
+
+func (c lclient) postMedia(base, endpoint string, editSingle bool, vals url.Values, v APIResponse, files ...InputMedia) error {
+ url, err := joinURL(base, endpoint, vals)
+ if err != nil {
+ return err
+ }
+
+ if err := c.wait(vals.Get("chat_id")); err != nil {
+ return err
+ }
+
+ cnt, err := c.sendMediaFiles(url, editSingle, files...)
+ if err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal(cnt, v); err != nil {
+ return err
+ }
+ return check(v)
+}
+
+func (c lclient) postStickers(base, endpoint string, vals url.Values, v APIResponse, stickers ...InputSticker) error {
+ url, err := joinURL(base, endpoint, vals)
+ if err != nil {
+ return err
+ }
+
+ if err := c.wait(vals.Get("chat_id")); err != nil {
+ return err
+ }
+
+ cnt, err := c.sendStickers(url, stickers...)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(cnt, v); err != nil {
+ return err
+ }
+ return check(v)
+}
+
+func (c lclient) postProfilePhoto(base, endpoint string, vals url.Values, v APIResponse, profilePhoto InputProfilePhoto) error {
+ url, err := joinURL(base, endpoint, vals)
+ if err != nil {
+ return err
+ }
+
+ if err := c.wait(""); err != nil {
+ return err
+ }
+
+ cnt, err := c.sendProfilePhoto(url, profilePhoto)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(cnt, v); err != nil {
+ return err
+ }
+ return check(v)
+}
+
+func (c lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
+ var (
+ med []mediaEnvelope
+ cnt []content
+ jsn []byte
+ )
+
+ for _, file := range files {
+ var im mediaEnvelope
+ var cntArr []content
+
+ media := file.media()
+ thumbnail := file.thumbnail()
+
+ im, cntArr, err = processMedia(media, thumbnail)
+ if err != nil {
+ return
+ }
+
+ im.InputMedia = file
+
+ med = append(med, im)
+ cnt = append(cnt, cntArr...)
+ }
+
+ if editSingle {
+ jsn, err = json.Marshal(med[0])
+ } else {
+ jsn, err = json.Marshal(med)
+ }
+
+ if err != nil {
+ return
+ }
+
+ url = fmt.Sprintf("%s&media=%s", url, jsn)
+
+ if len(cnt) > 0 {
+ return c.doPost(url, cnt...)
+ }
+ return c.doGet(url)
+}
+
+func (c lclient) sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
+ var (
+ sti []stickerEnvelope
+ cnt []content
+ jsn []byte
+ )
+
+ for _, s := range stickers {
+ var se stickerEnvelope
+ var cntArr []content
+
+ se, cntArr, err = processSticker(s.Sticker)
+ if err != nil {
+ return
+ }
+
+ se.InputSticker = s
+
+ sti = append(sti, se)
+ cnt = append(cnt, cntArr...)
+ }
+
+ if len(sti) == 1 {
+ jsn, _ = json.Marshal(sti[0])
+ url = fmt.Sprintf("%s&sticker=%s", url, jsn)
+ } else {
+ jsn, _ = json.Marshal(sti)
+ url = fmt.Sprintf("%s&stickers=%s", url, jsn)
+ }
+
+ if len(cnt) > 0 {
+ return c.doPost(url, cnt...)
+ }
+ return c.doGet(url)
+}
+
+func (c lclient) sendProfilePhoto(url string, profilePhoto InputProfilePhoto) (res []byte, err error) {
+ var (
+ pp profilePhotoEnvelope
+ cnt []content
+ jsn []byte
+ )
+
+ file := profilePhoto.file()
+
+ switch file := file; {
+ case file.id != "":
+ pp.ProfilePhoto = file.id
+ pp.Animation = file.id
+
+ case file.url != "":
+ pp.ProfilePhoto = file.url
+ pp.Animation = file.url
+
+ case file.path != "" && len(file.content) == 0:
+ if file.content, file.path, err = readFile(file); err != nil {
+ return
+ }
+ fallthrough
+
+ case file.path != "" && len(file.content) > 0:
+ cnt = append(cnt, content{file.path, file.path, file.content})
+ pp.ProfilePhoto = fmt.Sprintf("attach://%s", file.path)
+ pp.Animation = fmt.Sprintf("attach://%s", file.path)
+ }
+
+ switch profilePhoto.(type) {
+ case InputProfilePhotoStatic:
+ pp.Animation = ""
+ case InputProfilePhotoAnimated:
+ pp.ProfilePhoto = ""
+ }
+
+ pp.InputProfilePhoto = profilePhoto
+ jsn, err = json.Marshal(pp)
+ if err != nil {
+ return
+ }
+
+ sep := "?"
+ if strings.Contains(url, "?") {
+ sep = "&"
+ }
+ url = fmt.Sprintf("%s%sprofile_photo=%s", url, sep, jsn)
+
+ if len(cnt) > 0 {
+ return c.doPost(url, cnt...)
+ }
+ return c.doGet(url)
+}
diff --git a/shared/echotron/options.go b/shared/echotron/options.go
new file mode 100644
index 0000000..349004e
--- /dev/null
+++ b/shared/echotron/options.go
@@ -0,0 +1,861 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+// ParseMode is a custom type for the various frequent options used by some methods of the API.
+type ParseMode string
+
+// These are all the possible options that can be used by some methods.
+const (
+ Markdown ParseMode = "Markdown"
+ MarkdownV2 = "MarkdownV2"
+ HTML = "HTML"
+)
+
+// PollType is a custom type for the various types of poll that can be sent.
+type PollType string
+
+// These are all the possible poll types.
+const (
+ Quiz PollType = "quiz"
+ Regular = "regular"
+ Any = ""
+)
+
+// DiceEmoji is a custom type for the various emojis that can be sent through the SendDice method.
+type DiceEmoji string
+
+// These are all the possible emojis that can be sent through the SendDice method.
+const (
+ Die DiceEmoji = "🎲"
+ Darts = "🎯"
+ Basket = "🏀"
+ Goal = "⚽️"
+ Bowling = "🎳"
+ Slot = "🎰"
+)
+
+// ChatAction is a custom type for the various actions that can be sent through the SendChatAction method.
+type ChatAction string
+
+// These are all the possible actions that can be sent through the SendChatAction method.
+const (
+ Typing ChatAction = "typing"
+ UploadPhoto = "upload_photo"
+ RecordVideo = "record_video"
+ UploadVideo = "upload_video"
+ RecordAudio = "record_audio"
+ UploadAudio = "upload_audio"
+ UploadDocument = "upload_document"
+ FindLocation = "find_location"
+ RecordVideoNote = "record_video_note"
+ UploadVideoNote = "upload_video_note"
+ ChooseSticker = "choose_sticker"
+)
+
+// MessageEntityType is a custom type for the various MessageEntity types used in various methods.
+type MessageEntityType string
+
+// These are all the possible types for MessageEntityType.
+const (
+ MentionEntity MessageEntityType = "mention"
+ HashtagEntity = "hashtag"
+ CashtagEntity = "cashtag"
+ BotCommandEntity = "bot_command"
+ UrlEntity = "url"
+ EmailEntity = "email"
+ PhoneNumberEntity = "phone_number"
+ BoldEntity = "bold"
+ ItalicEntity = "italic"
+ UnderlineEntity = "underline"
+ StrikethroughEntity = "strikethrough"
+ SpoilerEntity = "spoiler"
+ BlockQuoteEntity = "blockquote"
+ ExpandableBlockQuoteEntity = "expandable_blockquote"
+ CodeEntity = "code"
+ PreEntity = "pre"
+ TextLinkEntity = "text_link"
+ TextMentionEntity = "text_mention"
+ CustomEmojiEntity = "custom_emoji"
+)
+
+// UpdateType is a custom type for the various update types that a bot can be subscribed to.
+type UpdateType string
+
+// These are all the possible types that a bot can be subscribed to.
+const (
+ MessageUpdate UpdateType = "message"
+ EditedMessageUpdate = "edited_message"
+ ChannelPostUpdate = "channel_post"
+ EditedChannelPostUpdate = "edited_channel_post"
+ InlineQueryUpdate = "inline_query"
+ ChosenInlineResultUpdate = "chosen_inline_result"
+ CallbackQueryUpdate = "callback_query"
+ ShippingQueryUpdate = "shipping_query"
+ PreCheckoutQueryUpdate = "pre_checkout_query"
+ PollUpdate = "poll"
+ PollAnswerUpdate = "poll_answer"
+ MyChatMemberUpdate = "my_chat_member"
+ ChatMemberUpdate = "chat_member"
+)
+
+// ReplyMarkup is an interface for the various keyboard types.
+type ReplyMarkup interface {
+ ImplementsReplyMarkup()
+}
+
+// KeyboardButton represents a button in a keyboard.
+type KeyboardButton struct {
+ RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"`
+ WebApp *WebAppInfo `json:"web_app,omitempty"`
+ RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"`
+ RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"`
+ IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
+ Style ButtonStyle `json:"style,omitempty"`
+ Text string `json:"text"`
+ RequestContact bool `json:"request_contact,omitempty"`
+ RequestLocation bool `json:"request_location,omitempty"`
+}
+
+// ButtonStyle represents supported button style values.
+type ButtonStyle string
+
+// These are all the possible values for ButtonStyle.
+const (
+ PrimaryButtonStyle ButtonStyle = "primary"
+ DangerButtonStyle = "danger"
+ SuccessButtonStyle = "success"
+)
+
+// KeyboardButtonPollType represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed.
+type KeyboardButtonPollType struct {
+ Type PollType `json:"type"`
+}
+
+// KeyboardButtonRequestUsers defines the criteria used to request suitable users.
+// The identifiers of the selected users will be shared with the bot when the corresponding button is pressed.
+type KeyboardButtonRequestUsers struct {
+ RequestID int `json:"request_id"`
+ MaxQuantity int `json:"max_quantity,omitempty"`
+ UserIsBot bool `json:"user_is_bot,omitempty"`
+ UserIsPremium bool `json:"user_is_premium,omitempty"`
+ RequestName bool `json:"request_name,omitempty"`
+ RequestUsername bool `json:"request_username,omitempty"`
+ RequestPhoto bool `json:"request_photo,omitempty"`
+}
+
+// KeyboardButtonRequestChat defines the criteria used to request a suitable chat.
+// The identifier of the selected chat will be shared with the bot when the corresponding button is pressed.
+type KeyboardButtonRequestChat struct {
+ UserAdministratorRights *ChatAdministratorRights `json:"user_administrator_rights,omitempty"`
+ BotAdministratorRights *ChatAdministratorRights `json:"bot_administrator_rights,omitempty"`
+ RequestID int `json:"request_id"`
+ ChatIsChannel bool `json:"chat_is_channel,omitempty"`
+ ChatIsForum bool `json:"chat_is_forum,omitempty"`
+ ChatHasUsername bool `json:"chat_has_username,omitempty"`
+ ChatIsCreated bool `json:"chat_is_created,omitempty"`
+ BotIsMember bool `json:"bot_is_member,omitempty"`
+ RequestName bool `json:"request_name,omitempty"`
+ RequestUsername bool `json:"request_username,omitempty"`
+ RequestPhoto bool `json:"request_photo,omitempty"`
+}
+
+// ReplyKeyboardMarkup represents a custom keyboard with reply options.
+type ReplyKeyboardMarkup struct {
+ InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"`
+ Keyboard [][]KeyboardButton `json:"keyboard"`
+ IsPersistent bool `json:"is_persistent,omitempty"`
+ ResizeKeyboard bool `json:"resize_keyboard,omitempty"`
+ OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
+ Selective bool `json:"selective,omitempty"`
+}
+
+// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
+func (i ReplyKeyboardMarkup) ImplementsReplyMarkup() {}
+
+// ReplyKeyboardRemove is used to remove the current custom keyboard and display the default letter-keyboard.
+// By default, custom keyboards are displayed until a new keyboard is sent by a bot.
+// An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup).
+// RemoveKeyboard MUST BE true.
+type ReplyKeyboardRemove struct {
+ RemoveKeyboard bool `json:"remove_keyboard"`
+ Selective bool `json:"selective"`
+}
+
+// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
+func (r ReplyKeyboardRemove) ImplementsReplyMarkup() {}
+
+// InlineKeyboardButton represents a button in an inline keyboard.
+type InlineKeyboardButton struct {
+ CopyText *CopyTextButton `json:"copy_text,omitempty"`
+ CallbackGame *CallbackGame `json:"callback_game,omitempty"`
+ WebApp *WebAppInfo `json:"web_app,omitempty"`
+ LoginURL *LoginURL `json:"login_url,omitempty"`
+ SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"`
+ IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
+ Style ButtonStyle `json:"style,omitempty"`
+ Text string `json:"text"`
+ CallbackData string `json:"callback_data,omitempty"`
+ SwitchInlineQuery string `json:"switch_inline_query,omitempty"`
+ SwitchInlineQueryCurrentChat string `json:"switch_inline_query_current_chat,omitempty"`
+ URL string `json:"url,omitempty"`
+ Pay bool `json:"pay,omitempty"`
+}
+
+// CopyTextButton represents an inline keyboard button that copies specified text to the clipboard.
+type CopyTextButton struct {
+ Text string `json:"text"`
+}
+
+// InlineKeyboardMarkup represents an inline keyboard.
+type InlineKeyboardMarkup struct {
+ InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard" query:"inline_keyboard"`
+}
+
+// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
+func (i InlineKeyboardMarkup) ImplementsReplyMarkup() {}
+
+// ForceReply is used to display a reply interface to the user (act as if the user has selected the bot's message and tapped 'Reply').
+// This can be extremely useful if you want to create user-friendly step-by-step interfaces without having to sacrifice privacy mode.
+type ForceReply struct {
+ InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"`
+ ForceReply bool `json:"force_reply"`
+ Selective bool `json:"selective"`
+}
+
+// ImplementsReplyMarkup is a dummy method which exists to implement the interface ReplyMarkup.
+func (f ForceReply) ImplementsReplyMarkup() {}
+
+// UpdateOptions contains the optional parameters used by the GetUpdates method.
+type UpdateOptions struct {
+ AllowedUpdates []UpdateType `query:"allowed_updates"`
+ Offset int `query:"offset"`
+ Limit int `query:"limit"`
+ Timeout int `query:"timeout"`
+}
+
+// WebhookOptions contains the optional parameters used by the SetWebhook method.
+type WebhookOptions struct {
+ IPAddress string `query:"ip_address"`
+ SecretToken string `query:"secret_token"`
+ Certificate InputFile
+ AllowedUpdates []UpdateType `query:"allowed_updates"`
+ MaxConnections int `query:"max_connections"`
+}
+
+// BaseOptions contains the optional parameters used frequently in some Telegram API methods.
+type BaseOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// MessageOptions contains the optional parameters used by some Telegram API methods.
+type MessageOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
+ Entities []MessageEntity `query:"entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int64 `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// PinMessageOptions contains the optional parameters used by the PinChatMember method.
+type PinMessageOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ DisableNotification bool `query:"disable_notification"`
+}
+
+// UnpinMessageOptions contains the optional parameters used by the UnpinChatMember method.
+type UnpinMessageOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageID int `query:"message_id"`
+}
+
+// ForwardOptions contains the optional parameters used by the ForwardMessage method.
+type ForwardOptions struct {
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ VideoStartTimestamp int `query:"video_start_timestamp"`
+}
+
+// CopyOptions contains the optional parameters used by the CopyMessage method.
+type CopyOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Caption string `query:"caption"`
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// CopyMessagesOptions contains the optional parameters used by the CopyMessages methods.
+type CopyMessagesOptions struct {
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ RemoveCaption bool `query:"remove_caption"`
+ VideoStartTimestamp int `query:"video_start_timestamp"`
+}
+
+// StickerOptions contains the optional parameters used by the SendSticker method.
+type StickerOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ Emoji string `query:"emoji"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// InputFile is a struct which contains data about a file to be sent.
+type InputFile struct {
+ id string
+ path string
+ url string
+ content []byte
+}
+
+// NewInputFileID is a wrapper for InputFile which only fills the id field.
+func NewInputFileID(ID string) InputFile {
+ return InputFile{id: ID}
+}
+
+// NewInputFilePath is a wrapper for InputFile which only fills the path field.
+func NewInputFilePath(filePath string) InputFile {
+ return InputFile{path: filePath}
+}
+
+// NewInputFileURL is a wrapper for InputFile which only fills the url field.
+func NewInputFileURL(url string) InputFile {
+ return InputFile{url: url}
+}
+
+// NewInputFileBytes is a wrapper for InputFile which only fills the path and content fields.
+func NewInputFileBytes(fileName string, content []byte) InputFile {
+ return InputFile{path: fileName, content: content}
+}
+
+// PhotoOptions contains the optional parameters used by the SendPhoto method.
+type PhotoOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Caption string `query:"caption"`
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ HasSpoiler bool `query:"has_spoiler"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// AudioOptions contains the optional parameters used by the SendAudio method.
+type AudioOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ Title string `query:"title"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Caption string `query:"caption"`
+ Performer string `query:"performer"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ Thumbnail InputFile
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ Duration int `query:"duration"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// DocumentOptions contains the optional parameters used by the SendDocument method.
+type DocumentOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Caption string `query:"caption"`
+ Thumbnail InputFile
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ DisableContentTypeDetection bool `query:"disable_content_type_detection"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// VideoOptions contains the optional parameters used by the SendVideo method.
+// TODO: handle the cover correctly.
+type VideoOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ Caption string `query:"caption"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Thumbnail InputFile
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ Duration int `query:"duration"`
+ Width int `query:"width"`
+ Height int `query:"height"`
+ HasSpoiler bool `query:"has_spoiler"`
+ SupportsStreaming bool `query:"supports_streaming"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+ Cover InputFile
+ StartTimestamp int `query:"start_timestamp"`
+}
+
+// AnimationOptions contains the optional parameters used by the SendAnimation method.
+type AnimationOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Caption string `query:"caption"`
+ Thumbnail InputFile
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ Duration int `query:"duration"`
+ Width int `query:"width"`
+ Height int `query:"height"`
+ HasSpoiler bool `query:"has_spoiler"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// VoiceOptions contains the optional parameters used by the SendVoice method.
+type VoiceOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ Caption string `query:"caption"`
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ Duration int `query:"duration"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// VideoNoteOptions contains the optional parameters used by the SendVideoNote method.
+type VideoNoteOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ Thumbnail InputFile
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ Duration int `query:"duration"`
+ Length int `query:"length"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// PaidMediaOptions contains the optional parameters used by the SendPaidMedia method.
+type PaidMediaOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ Caption string `query:"caption"`
+ Payload string `query:"payload"`
+ ParseMode ParseMode `query:"parse_mode"`
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// MediaGroupOptions contains the optional parameters used by the SendMediaGroup method.
+type MediaGroupOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// This is a custom constant to set an infinite live period value in LocationOptions and EditLocationOptions.
+const InfiniteLivePeriod = 0x7FFFFFFF
+
+// LocationOptions contains the optional parameters used by the SendLocation method.
+type LocationOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ HorizontalAccuracy float64 `query:"horizontal_accuracy"`
+ MessageThreadID int `query:"message_thread_id"`
+ LivePeriod int `query:"live_period"`
+ ProximityAlertRadius int `query:"proximity_alert_radius"`
+ Heading int `query:"heading"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// EditLocationOptions contains the optional parameters used by the EditMessageLiveLocation method.
+type EditLocationOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
+ HorizontalAccuracy float64 `query:"horizontal_accuracy"`
+ Heading int `query:"heading"`
+ LivePeriod int `query:"live_period"`
+ ProximityAlertRadius int `query:"proximity_alert_radius"`
+}
+
+// StopLocationOptions contains the optional parameters used by the StopMessageLiveLocation method.
+type StopLocationOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+}
+
+// VenueOptions contains the optional parameters used by the SendVenue method.
+type VenueOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ FoursquareID string `query:"foursquare_id"`
+ FoursquareType string `query:"foursquare_type"`
+ GooglePlaceType string `query:"google_place_type"`
+ GooglePlaceID string `query:"google_place_id"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// ContactOptions contains the optional parameters used by the SendContact method.
+type ContactOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ VCard string `query:"vcard"`
+ LastName string `query:"last_name"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MessageThreadID int `query:"message_thread_id"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// PollOptions contains the optional parameters used by the SendPoll method.
+type PollOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ Explanation string `query:"explanation"`
+ MessageEffectID string `query:"message_effect_id"`
+ ExplanationParseMode ParseMode `query:"explanation_parse_mode"`
+ QuestionParseMode ParseMode `query:"question_parse_mode"`
+ Type PollType `query:"type"`
+ ExplanationEntities []MessageEntity `query:"explanation_entities"`
+ QuestionEntities []MessageEntity `query:"question_entities"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ CorrectOptionID int `query:"correct_option_id"`
+ MessageThreadID int `query:"message_thread_id"`
+ CloseDate int `query:"close_date"`
+ OpenPeriod int `query:"open_period"`
+ IsClosed bool `query:"is_closed"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ AllowsMultipleAnswers bool `query:"allows_multiple_answers"`
+ IsAnonymous bool `query:"is_anonymous"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// StopPollOptions contains the optional parameters used by the StopPoll method.
+type StopPollOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+}
+
+// BanOptions contains the optional parameters used by the BanChatMember method.
+type BanOptions struct {
+ UntilDate int `query:"until_date"`
+ RevokeMessages bool `query:"revoke_messages"`
+}
+
+// UnbanOptions contains the optional parameters used by the UnbanChatMember method.
+type UnbanOptions struct {
+ OnlyIfBanned bool `query:"only_if_banned"`
+}
+
+// RestrictOptions contains the optional parameters used by the RestrictChatMember method.
+type RestrictOptions struct {
+ UseIndependentChatPermissions bool `query:"use_independent_chat_permissions"`
+ UntilDate int `query:"until_date"`
+}
+
+// PromoteOptions contains the optional parameters used by the PromoteChatMember method.
+type PromoteOptions struct {
+ IsAnonymous bool `query:"is_anonymous,omitempty"`
+ CanManageChat bool `query:"can_manage_chat,omitempty"`
+ CanPostMessages bool `query:"can_post_messages,omitempty"`
+ CanEditMessages bool `query:"can_edit_messages,omitempty"`
+ CanDeleteMessages bool `query:"can_delete_messages,omitempty"`
+ CanManageVideoChats bool `query:"can_manage_video_chats,omitempty"`
+ CanRestrictMembers bool `query:"can_restrict_members,omitempty"`
+ CanPromoteMembers bool `query:"can_promote_members,omitempty"`
+ CanChangeInfo bool `query:"can_change_info,omitempty"`
+ CanInviteUsers bool `query:"can_invite_users,omitempty"`
+ CanPinMessages bool `query:"can_pin_messages,omitempty"`
+ CanPostStories bool `json:"can_post_stories,omitempty"`
+ CanEditStories bool `json:"can_edit_stories,omitempty"`
+ CanDeleteStories bool `json:"can_delete_stories,omitempty"`
+ CanManageTopics bool `query:"can_manage_topics,omitempty"`
+}
+
+// UserProfileOptions contains the optional parameters used by the GetUserProfilePhotos method.
+type UserProfileOptions struct {
+ Offset int `query:"offset"`
+ Limit int `query:"limit"`
+}
+
+// UserProfileAudioOptions contains the optional parameters used by the GetUserProfileAudios method.
+type UserProfileAudioOptions struct {
+ Offset int `query:"offset"`
+ Limit int `query:"limit"`
+}
+
+// UserEmojiStatusOptions contains the optional parameters used by the SetUserEmojiStatus method.
+type UserEmojiStatusOptions struct {
+ EmojiStatusCustomEmojiID string `query:"emoji_status_custom_emoji_id"`
+ EmojiStatusExpirationDate string `query:"emoji_status_expiration_date"`
+}
+
+// ChatPermissionsOptions contains the optional parameters used by the SetChatPermissions method.
+type ChatPermissionsOptions struct {
+ UseIndependentChatPermissions bool `query:"use_independent_chat_permissions"`
+}
+
+// InviteLinkOptions contains the optional parameters used by the CreateChatInviteLink and EditChatInviteLink methods.
+type InviteLinkOptions struct {
+ Name string `query:"name"`
+ ExpireDate int64 `query:"expire_date"`
+ MemberLimit int `query:"member_limit"`
+ CreatesJoinRequest bool `query:"creates_join_request"`
+}
+
+// ChatSubscriptionInviteOptions contains the optional parameters used by the CreateChatSubscriptionInviteLink and EditChatSubscriptionInviteLink methods.
+type ChatSubscriptionInviteOptions struct {
+ Name string `query:"name"`
+}
+
+// CallbackQueryOptions contains the optional parameters used by the AnswerCallbackQuery method.
+type CallbackQueryOptions struct {
+ Text string `query:"text"`
+ URL string `query:"url"`
+ CacheTime int `query:"cache_time"`
+ ShowAlert bool `query:"show_alert"`
+}
+
+// MessageIDOptions is a struct which contains data about a message to edit.
+type MessageIDOptions struct {
+ inlineMessageID string `query:"inline_message_id"`
+ chatID int64 `query:"chat_id"`
+ messageID int `query:"message_id"`
+}
+
+// NewMessageID is a wrapper for MessageIDOptions which only fills the chatID and messageID fields.
+func NewMessageID(chatID int64, messageID int) MessageIDOptions {
+ return MessageIDOptions{chatID: chatID, messageID: messageID}
+}
+
+// NewInlineMessageID is a wrapper for MessageIDOptions which only fills the inlineMessageID fields.
+func NewInlineMessageID(ID string) MessageIDOptions {
+ return MessageIDOptions{inlineMessageID: ID}
+}
+
+// MessageTextOptions contains the optional parameters used by the EditMessageText method.
+type MessageTextOptions struct {
+ ParseMode ParseMode `query:"parse_mode"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ Entities []MessageEntity `query:"entities"`
+ ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
+ LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
+}
+
+// MessageCaptionOptions contains the optional parameters used by the EditMessageCaption method.
+type MessageCaptionOptions struct {
+ Caption string `query:"caption"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ ParseMode ParseMode `query:"parse_mode"`
+ CaptionEntities []MessageEntity `query:"caption_entities"`
+ ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
+ ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
+}
+
+// MessageMediaOptions contains the optional parameters used by the EditMessageMedia method.
+type MessageMediaOptions struct {
+ ReplyMarkup ReplyMarkup `query:"reply_markup"`
+ BusinessConnectionID string `query:"business_connection_id"`
+}
+
+// MessageReplyMarkupOptions contains the optional parameters used by the EditMessageReplyMarkup method.
+type MessageReplyMarkupOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
+}
+
+// CommandOptions contains the optional parameters used by the SetMyCommands, DeleteMyCommands and GetMyCommands methods.
+type CommandOptions struct {
+ LanguageCode string `query:"language_code"`
+ Scope BotCommandScope `query:"scope"`
+}
+
+// InvoiceOptions contains the optional parameters used by the SendInvoice API method.
+type InvoiceOptions struct {
+ StartParameter string `query:"start_parameter"`
+ ProviderData string `query:"provider_data"`
+ PhotoURL string `query:"photo_url"`
+ ProviderToken string `query:"provider_token"`
+ MessageEffectID string `query:"message_effect_id"`
+ ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
+ SuggestedTipAmount []int `query:"suggested_tip_amounts"`
+ ReplyParameters ReplyParameters `query:"reply_parameters"`
+ MaxTipAmount int `query:"max_tip_amount"`
+ PhotoSize int `query:"photo_size"`
+ PhotoWidth int `query:"photo_width"`
+ PhotoHeight int `query:"photo_height"`
+ MessageThreadID int `query:"message_thread_id"`
+ SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"`
+ NeedShippingAddress bool `query:"need_shipping_address"`
+ NeedPhoneNumber bool `query:"need_phone_number"`
+ SendEmailToProvider bool `query:"send_email_to_provider"`
+ IsFlexible bool `query:"is_flexible"`
+ DisableNotification bool `query:"disable_notification"`
+ ProtectContent bool `query:"protect_content"`
+ NeedName bool `query:"need_name"`
+ NeedEmail bool `query:"need_email"`
+ AllowPaidBroadcast bool `query:"allow_paid_broadcast"`
+}
+
+// CreateInvoiceLinkOptions contains the optional parameters used by the CreateInvoiceLink API method.
+// Currently, SubscriptionPeriod should always be set to 2592000 (30 days) if specified.
+type CreateInvoiceLinkOptions struct {
+ ProviderData string `query:"provider_data"`
+ PhotoURL string `query:"photo_url"`
+ ProviderToken string `query:"provider_token"`
+ BusinessConnectionID string `query:"business_connection_id"`
+ SuggestedTipAmounts []int `query:"suggested_tip_amounts"`
+ PhotoSize int `query:"photo_size"`
+ PhotoWidth int `query:"photo_width"`
+ PhotoHeight int `query:"photo_height"`
+ MaxTipAmount int `query:"max_tip_amount"`
+ SubscriptionPeriod int `query:"subscription_period"`
+ NeedPhoneNumber bool `query:"need_phone_number"`
+ NeepShippingAddress bool `query:"need_shipping_address"`
+ SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"`
+ SendEmailToProvider bool `query:"send_email_to_provider"`
+ IsFlexible bool `query:"is_flexible"`
+ NeedName bool `query:"need_name"`
+ NeedEmail bool `query:"need_email"`
+}
+
+// ShippingOption represents one shipping option.
+type ShippingOption struct {
+ ID string `query:"id"`
+ Title string `query:"title"`
+ Prices []LabeledPrice `query:"prices"`
+}
+
+// ShippingQueryOptions contains the optional parameters used by the AnswerShippingQuery API method.
+type ShippingQueryOptions struct {
+ ErrorMessage string `query:"error_message"`
+ ShippingOptions []ShippingOption `query:"shipping_options"`
+}
+
+// PreCheckoutOptions contains the optional parameters used by the AnswerPreCheckoutQuery API method.
+type PreCheckoutOptions struct {
+ ErrorMessage string `query:"error_message"`
+}
+
+// CreateTopicOptions contains the optional parameters used by the CreateForumTopic API method.
+type CreateTopicOptions struct {
+ IconCustomEmojiID string `query:"icon_custom_emoji_id"`
+ IconColor IconColor `query:"icon_color"`
+}
+
+// EditTopicOptions contains the optional parameters used by the EditForumTopic API method.
+type EditTopicOptions struct {
+ Name string `query:"name"`
+ IconCustomEmojiID string `query:"icon_custom_emoji_id"`
+}
+
+// ChatActionOptions contains the optional parameters used by the SendChatAction API method.
+type ChatActionOptions struct {
+ BusinessConnectionID string `query:"business_connection_id"`
+ MessageThreadID int `query:"message_thread_id"`
+}
+
+// MessageReactionOptions contains the optional parameters used by the SetMessageReaction API method.
+type MessageReactionOptions struct {
+ Reaction []ReactionType `query:"reaction"`
+ IsBig bool `query:"is_big"`
+}
+
+// GiftOptions contains the optional parameters used by the SendGift API method.
+type GiftOptions struct {
+ Text string `query:"text"`
+ TextParseMode string `query:"text_parse_mode"`
+ TextEntities []MessageEntity `query:"text_entities"`
+ ChatID int64 `query:"chat_id"`
+ PayForUpgrade bool `query:"pay_for_upgrade"`
+}
+
+// VerifyOptions contains the optional parameters used by the VerifyUser and VerifyChat API methods.
+type VerifyOptions struct {
+ CustomDescription string `query:"custom_description"`
+}
diff --git a/shared/echotron/options_test.go b/shared/echotron/options_test.go
new file mode 100644
index 0000000..9d4c428
--- /dev/null
+++ b/shared/echotron/options_test.go
@@ -0,0 +1,57 @@
+package echotron
+
+import (
+ "reflect"
+ "testing"
+)
+
+var (
+ msgIDOpts = MessageIDOptions{
+ chatID: 1,
+ messageID: 2,
+ }
+
+ inlineMsgIDOpts = MessageIDOptions{
+ inlineMessageID: "inline",
+ }
+)
+
+func TestNewMessageID(t *testing.T) {
+ new := NewMessageID(1, 2)
+
+ if !reflect.DeepEqual(new, msgIDOpts) {
+ t.Logf("expected MessageIDOptions: %+v", msgIDOpts)
+ t.Logf("got MessageIDOptions: %+v", new)
+ t.Fatal("error: MessageIDOptions mismatch")
+ }
+}
+
+func TestNewInlineMessageID(t *testing.T) {
+ new := NewInlineMessageID("inline")
+
+ if !reflect.DeepEqual(new, inlineMsgIDOpts) {
+ t.Logf("expected MessageIDOptions: %+v", inlineMsgIDOpts)
+ t.Logf("got MessageIDOptions: %+v", new)
+ t.Fatal("error: MessageIDOptions mismatch")
+ }
+}
+
+func TestReplyKeyboardMarkupImplementsReplyMarkup(_ *testing.T) {
+ i := ReplyKeyboardMarkup{}
+ i.ImplementsReplyMarkup()
+}
+
+func TestReplyKeyboardRemoveImplementsReplyMarkup(_ *testing.T) {
+ i := ReplyKeyboardRemove{}
+ i.ImplementsReplyMarkup()
+}
+
+func TestInlineKeyboardMarkupImplementsReplyMarkup(_ *testing.T) {
+ i := InlineKeyboardMarkup{}
+ i.ImplementsReplyMarkup()
+}
+
+func TestForceReplyImplementsReplyMarkup(_ *testing.T) {
+ i := ForceReply{}
+ i.ImplementsReplyMarkup()
+}
diff --git a/shared/echotron/passport.go b/shared/echotron/passport.go
new file mode 100644
index 0000000..afacaad
--- /dev/null
+++ b/shared/echotron/passport.go
@@ -0,0 +1,228 @@
+package echotron
+
+import (
+ "encoding/json"
+ "net/url"
+)
+
+// PassportData contains information about Telegram Passport data shared with the bot by the user.
+type PassportData struct {
+ Credentials EncryptedCredentials `json:"encrypted_credentials"`
+ Data []EncryptedPassportElement `json:"encrypted_passport_element"`
+}
+
+// PassportFile represents a file uploaded to Telegram Passport.
+// Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB.
+type PassportFile struct {
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ FileSize int64 `json:"file_size"`
+ FileDate int64 `json:"file_date"`
+}
+
+// EncryptedPassportElementType is a custom type for the various possible options used as Type in EncryptedPassportElement.
+type EncryptedPassportElementType string
+
+// These are all the possible options that can be used as Type in EncryptedPassportElement.
+const (
+ TypePersonalDetails EncryptedPassportElementType = "personal_details"
+ TypePassport = "passport"
+ TypeDriverLicense = "driver_license"
+ TypeIdentityCard = "identity_card"
+ TypeInternalPassport = "internal_passport"
+ TypeAddress = "address"
+ TypeUtilityBill = "utility_bill"
+ TypeBankStatement = "bank_statement"
+ TypeRentalAgreement = "rental_agreement"
+ TypePassportRegistration = "passport_registration"
+ TypeTemporaryRegistration = "temporary_registration"
+ TypePhoneNumber = "phone_number"
+ TypeEmail = "email"
+)
+
+// EncryptedPassportElement contains information about documents or other Telegram Passport elements shared with the bot by the user.
+type EncryptedPassportElement struct {
+ Type EncryptedPassportElementType `json:"type"`
+ Data string `json:"data,omitempty"`
+ PhoneNumber string `json:"phone_number,omitempty"`
+ Email string `json:"email,omitempty"`
+ Files *[]PassportFile `json:"files,omitempty"`
+ FrontSide *PassportFile `json:"front_side,omitempty"`
+ ReverseSide *PassportFile `json:"reverse_side,omitempty"`
+ Selfie *PassportFile `json:"selfie,omitempty"`
+ Translation *[]PassportFile `json:"translation,omitempty"`
+ Hash string `json:"hash"`
+}
+
+// EncryptedCredentials contains data required for decrypting and authenticating EncryptedPassportElement.
+// See the Telegram Passport Documentation for a complete description of the data decryption and authentication processes.
+// https://core.telegram.org/passport#receiving-information
+type EncryptedCredentials struct {
+ Data string `json:"data"`
+ Hash string `json:"hash"`
+ Secret string `json:"secret"`
+}
+
+// PassportElementErrorSource is a custom type for the various possible options used as Source in PassportElementSource.
+type PassportElementErrorSource string
+
+// These are all the possible options that can be used as Source in PassportElementSource.
+const (
+ SourceData PassportElementErrorSource = "data"
+ SourceFrontSide = "front_side"
+ SourceReverseSide = "reverse_side"
+ SourceSelfie = "selfie"
+ SourceFile = "file"
+ SourceFiles = "files"
+ SourceTranslationFile = "translation_file"
+ SourceTranslationFiles = "translation_files"
+ SourceUnspecified = "unspecified"
+)
+
+// PassportElementError is an interface for the various PassportElementError types.
+type PassportElementError interface {
+ ImplementsPassportElementError()
+}
+
+// PassportElementErrorDataField represents an issue in one of the data fields that was provided by the user.
+// The error is considered resolved when the field's value changes.
+// Source MUST BE SourceData.
+// Type MUST BE one of TypePersonalDetails, TypePassport, TypeDriverLicense, TypeIdentityCard, TypeInternalPassport and TypeAddress.
+type PassportElementErrorDataField struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ FieldName string `json:"field_name"`
+ DataHash string `json:"data_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorDataField) ImplementsPassportElementError() {}
+
+// PassportElementErrorFrontSide represents an issue with the front side of a document.
+// The error is considered resolved when the file with the front side of the document changes.
+// Source MUST BE SourceFrontSide.
+// Type MUST BE one of TypeDriverLicense and TypeIdentityCard.
+type PassportElementErrorFrontSide struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ FileHash string `json:"file_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorFrontSide) ImplementsPassportElementError() {}
+
+// PassportElementErrorReverseSide represents an issue with the reverse side of a document.
+// The error is considered resolved when the file with the reverse side of the document changes.
+// Source MUST BE SourceReverseSide.
+// Type MUST BE one of TypeDriverLicense and TypeIdentityCard.
+type PassportElementErrorReverseSide struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ FileHash string `json:"file_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorReverseSide) ImplementsPassportElementError() {}
+
+// PassportElementErrorSelfie represents an issue with the selfie with a document.
+// The error is considered resolved when the file with the selfie changes.
+// Source MUST BE SourceSelfie.
+// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard and TypeIdentityPassport.
+type PassportElementErrorSelfie struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ FileHash string `json:"file_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorSelfie) ImplementsPassportElementError() {}
+
+// PassportElementErrorFile represents an issue with the document scan.
+// The error is considered resolved when the file with the document scan changes.
+// Source MUST BE SourceFile.
+// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard and TypeIdentityPassport.
+type PassportElementErrorFile struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ FileHash string `json:"file_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorFile) ImplementsPassportElementError() {}
+
+// PassportElementErrorFiles represents an issue with a list of scans.
+// The error is considered resolved when the list of files containing the scans changes.
+// Source MUST BE SourceFiles.
+// Type MUST BE one of TypeUtilityBill, TypeBankStatement, TypeRentalAgreement, TypePassportRegistration and TypeTemporaryRegistration.
+type PassportElementErrorFiles struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ Message string `json:"message"`
+ FileHashes []string `json:"file_hashes"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorFiles) ImplementsPassportElementError() {}
+
+// PassportElementErrorTranslationFile represents an issue with one of the files that constitute the translation of the document.
+// The error is considered resolved when the file changes.
+// Source MUST BE SourceTranslationFile.
+// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard, TypeInternalPassport, TypeUtilityBill, TypeBankStatement,
+// TypeRentalAgreement, TypePassportRegistration and TypeTemporaryRegistration.
+type PassportElementErrorTranslationFile struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ FileHash string `json:"file_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorTranslationFile) ImplementsPassportElementError() {}
+
+// PassportElementErrorTranslationFiles represents an issue with the translated version of a document.
+// The error is considered resolved when a file with the document translation changes.
+// Source MUST BE SourceTranslationFiles.
+// Type MUST BE one of TypePassport, TypeDriverLicense, TypeIdentityCard, TypeInternalPassport, TypeUtilityBill, TypeBankStatement,
+// TypeRentalAgreement, TypePassportRegistration and TypeTemporaryRegistration.
+type PassportElementErrorTranslationFiles struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ Message string `json:"message"`
+ FileHashes []string `json:"file_hashes"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorTranslationFiles) ImplementsPassportElementError() {}
+
+// PassportElementErrorUnspecified represents an issue in an unspecified place.
+// The error is considered resolved when new data is added.
+type PassportElementErrorUnspecified struct {
+ Source PassportElementErrorSource `json:"source"`
+ Type EncryptedPassportElementType `json:"type"`
+ ElementHash string `json:"element_hash"`
+ Message string `json:"message"`
+}
+
+// ImplementsPassportElementError us a dummy method which exists to implement the interface PassportElementError.
+func (p PassportElementErrorUnspecified) ImplementsPassportElementError() {}
+
+// SetPassportDataErrors Informs a user that some of the Telegram Passport elements they provided contains errors.
+// The user will not be able to re-submit their Passport to you until the errors are fixed.
+// The contents of the field for which you returned the error must change.
+func (a API) SetPassportDataErrors(userID int64, errors []PassportElementError) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ errorsArr, err := json.Marshal(errors)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("errors", string(errorsArr))
+ return res, client.get(a.base, "setPassportDataErrors", vals, &res)
+}
diff --git a/shared/echotron/passport_test.go b/shared/echotron/passport_test.go
new file mode 100644
index 0000000..6a551db
--- /dev/null
+++ b/shared/echotron/passport_test.go
@@ -0,0 +1,48 @@
+package echotron
+
+import "testing"
+
+func TestPassportElementErrorDataField(_ *testing.T) {
+ p := PassportElementErrorDataField{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorFrontSide(_ *testing.T) {
+ p := PassportElementErrorFrontSide{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorReverseSide(_ *testing.T) {
+ p := PassportElementErrorReverseSide{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorSelfie(_ *testing.T) {
+ p := PassportElementErrorSelfie{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorFile(_ *testing.T) {
+ p := PassportElementErrorFile{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorFiles(_ *testing.T) {
+ p := PassportElementErrorFiles{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorTranslationFile(_ *testing.T) {
+ p := PassportElementErrorTranslationFile{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorTranslationFiles(_ *testing.T) {
+ p := PassportElementErrorTranslationFiles{}
+ p.ImplementsPassportElementError()
+}
+
+func TestPassportElementErrorUnspecified(_ *testing.T) {
+ p := PassportElementErrorUnspecified{}
+ p.ImplementsPassportElementError()
+}
diff --git a/shared/echotron/payments.go b/shared/echotron/payments.go
new file mode 100644
index 0000000..a289fab
--- /dev/null
+++ b/shared/echotron/payments.go
@@ -0,0 +1,335 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "net/url"
+)
+
+// LabeledPrice represents a portion of the price for goods or services.
+type LabeledPrice struct {
+ Label string `json:"label"`
+ // Price of the product in the smallest units of the currency (integer, not float/double).
+ // For example, for a price of US$ 1.45 pass amount = 145.
+ // See the exp parameter in currencies.json, it shows the number of digits
+ // past the decimal point for each currency (2 for the majority of currencies).
+ Amount int `json:"amount"`
+}
+
+// Invoice contains basic information about an invoice.
+type Invoice struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ StartParameter string `json:"start_parameter"`
+ // Three-letter ISO 4217 currency code.
+ Currency string `json:"currency"`
+ // Total amount in the smallest units of the currency (integer, not float/double).
+ // For example, for a price of US$ 1.45 pass amount = 145.
+ // See the exp parameter in currencies.json, it shows the number of digits
+ // past the decimal point for each currency (2 for the majority of currencies).
+ TotalAmount int `json:"total_amount"`
+}
+
+// ShippingAddress represents a shipping address.
+type ShippingAddress struct {
+ // ISO 3166-1 alpha-2 country code.
+ CountryCode string `json:"country_code"`
+ State string `json:"state"`
+ City string `json:"city"`
+ StreetLine1 string `json:"street_line1"`
+ StreetLine2 string `json:"street_line2"`
+ PostCode string `json:"post_code"`
+}
+
+// OrderInfo represents information about an order.
+type OrderInfo struct {
+ Name string `json:"name,omitempty"`
+ PhoneNumber string `json:"phone_number,omitempty"`
+ Email string `json:"email,omitempty"`
+ ShippingAddress ShippingAddress `json:"shipping_address,omitempty"`
+}
+
+// SuccessfulPayment contains basic information about a successful payment.
+type SuccessfulPayment struct {
+ OrderInfo OrderInfo `json:"order_info"`
+ Currency string `json:"currency"`
+ InvoicePayload string `json:"invoice_payload"`
+ ShippingOptionID string `json:"shipping_option_id"`
+ TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
+ ProviderPaymentChargeID string `json:"provider_payment_charge_id"`
+ TotalAmount int `json:"total_amount"`
+ SubscriptionExpirationDate int `json:"subscription_expiration_date,omitempty"`
+ IsRecurring bool `json:"is_recurring,omitempty"`
+ IsFirstRecurring bool `json:"is_first_recurring,omitempty"`
+}
+
+// RefundedPayment contains basic information about a refunded payment.
+type RefundedPayment struct {
+ Currency string `json:"currency"`
+ InvoicePayload string `json:"invoice_payload"`
+ TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
+ ProviderPaymentChargeID string `json:"provider_payment_charge_id,omitempty"`
+ TotalAmount int `json:"total_amount"`
+}
+
+// ShippingQuery contains information about an incoming shipping query.
+type ShippingQuery struct {
+ ShippingAddress ShippingAddress `json:"shipping_address"`
+ ID string `json:"id"`
+ InvoicePayload string `json:"invoice_payload"`
+ From User `json:"from"`
+}
+
+// PreCheckoutQuery contains information about an incoming pre-checkout query.
+type PreCheckoutQuery struct {
+ OrderInfo OrderInfo `json:"order_info,omitempty"`
+ Currency string `json:"currency"`
+ InvoicePayload string `json:"invoice_payload"`
+ ShippingOptionID string `json:"shipping_option_id,omitempty"`
+ ID string `json:"id"`
+ From User `json:"from"`
+ TotalAmount int `json:"total_amount"`
+}
+
+// PaidMediaPurchased contains information about a paid media purchase.
+type PaidMediaPurchased struct {
+ PaidMediaPayload string `json:"paid_media_payload"`
+ From User `json:"from"`
+}
+
+// RevenueWithdrawalState describes the state of a revenue withdrawal operation.
+type RevenueWithdrawalState interface {
+ ImplementsRevenueWithdrawalState()
+}
+
+// RevenueWithdrawalStatePending describes the state of a withdrawal in progress.
+type RevenueWithdrawalStatePending struct {
+ Type string `json:"type"`
+}
+
+// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
+func (r RevenueWithdrawalStatePending) ImplementsRevenueWithdrawalState() {}
+
+// RevenueWithdrawalStateSucceeded describes the state of a succeeded withdrawal.
+type RevenueWithdrawalStateSucceeded struct {
+ Type string `json:"type"`
+ URL string `json:"url"`
+ Date int `json:"date"`
+}
+
+// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
+func (r RevenueWithdrawalStateSucceeded) ImplementsRevenueWithdrawalState() {}
+
+// RevenueWithdrawalStateFailed describes the state of a failed withdrawal, in which the transaction was refunded.
+type RevenueWithdrawalStateFailed struct {
+ Type string `json:"type"`
+}
+
+// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
+func (r RevenueWithdrawalStateFailed) ImplementsRevenueWithdrawalState() {}
+
+// AffiliateInfo
+type AffiliateInfo struct {
+ AffiliateUser *User `json:"affiliate_user,omitempty"`
+ AffiliateChat *Chat `json:"affiliate_chat,omitempty"`
+ CommissionPerMille int `json:"commission_per_mille"`
+ Amount int `json:"amount"`
+ NanostarAmount int `json:"nanostar_amount,omitempty"`
+}
+
+// TransactionPartner describes the source of a transaction, or its recipient for outgoing transactions.
+type TransactionPartner interface {
+ ImplementsTransactionPartner()
+}
+
+// TransactionPartnerAffiliateProgram describes the affiliate program that issued the affiliate commission received via this transaction.
+// Type MUST be "affiliate_program".
+type TransactionPartnerAffiliateProgram struct {
+ SponsorUser *User `json:"sponsor_user,omitempty"`
+ Type string `json:"type"`
+ CommissionPerMille int `json:"commission_per_mille,omitempty"`
+}
+
+// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
+func (t TransactionPartnerAffiliateProgram) ImplementsTransactionPartner() {}
+
+// TransactionPartnerFragment describes a withdrawal transaction with Fragment.
+// Type MUST be "fragment".
+type TransactionPartnerFragment struct {
+ WithdrawalState RevenueWithdrawalState `json:"withdrawal_state"`
+ Type string `json:"type"`
+}
+
+// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
+func (t TransactionPartnerFragment) ImplementsTransactionPartner() {}
+
+// TransactionPartnerUser describes a transaction with a user.
+// Type MUST be "user".
+type TransactionPartnerUser struct {
+ PaidMedia *[]PaidMedia `json:"paid_media,omitempty"`
+ Type string `json:"type"`
+ InvoicePayload string `json:"invoice_payload,omitempty"`
+ PaidMediaPayload string `json:"paid_media_payload,omitempty"`
+ User User `json:"user"`
+ Affiliate *AffiliateInfo `json:"affiliate,omitempty"`
+ Gift Gift `json:"gift,omitempty"`
+ SubscriptionPeriod int `json:"subscription_period,omitempty"`
+}
+
+// TransactionPartnerChat describes a transaction with a chat.
+type TransactionPartnerChat struct {
+ Type string `json:"type"`
+ Chat Chat `json:"chat"`
+ Gift Gift `json:"gift,omitempty"`
+}
+
+// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
+func (t TransactionPartnerUser) ImplementsTransactionPartner() {}
+
+// TransactionPartnerTelegramAds describes a withdrawal transaction to the Telegram Ads platform.
+// Type MUST be "telegram_ads".
+type TransactionPartnerTelegramAds struct {
+ Type string `json:"type"`
+}
+
+// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
+func (t TransactionPartnerTelegramAds) ImplementsTransactionPartner() {}
+
+// TransactionPartnerTelegramApi describes a transaction with payment for paid broadcasting.
+// Type MUST be "telegram_api".
+type TransactionPartnerTelegramApi struct {
+ Type string `json:"type"`
+ RequestCount int `json:"request_count"`
+}
+
+// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
+func (t TransactionPartnerTelegramApi) ImplementsTransactionPartner() {}
+
+// TransactionPartnerOther describes a transaction with an unknown source or recipient.
+// Type MUST be "other".
+type TransactionPartnerOther struct {
+ Type string `json:"type"`
+}
+
+// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
+func (t TransactionPartnerOther) ImplementsTransactionPartner() {}
+
+// StarTransaction describes a Telegram Star transaction.
+type StarTransaction struct {
+ Source TransactionPartner `json:"source"`
+ Receiver TransactionPartner `json:"receiver"`
+ ID string `json:"id"`
+ Amount int `json:"amount"`
+ NanostarAmount int `json:"nanostar_amount,omitempty"`
+ Date int `json:"date"`
+}
+
+// StarTransactions contains a list of Telegram Star transactions.
+type StarTransactions struct {
+ Transaction []StarTransaction `json:"transaction"`
+}
+
+// StarTransactionsOptions contains the optional parameters used by the GetStarTransactions method.
+type StarTransactionsOptions struct {
+ Offset int `query:"offset"`
+ Limit int `query:"limit"`
+}
+
+// SendInvoice is used to send invoices.
+func (a API) SendInvoice(chatID int64, title, description, payload, currency string, prices []LabeledPrice, opts *InvoiceOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ p, err := json.Marshal(prices)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("chat_id", itoa(chatID))
+ vals.Set("title", title)
+ vals.Set("description", description)
+ vals.Set("payload", payload)
+ vals.Set("currency", currency)
+ vals.Set("prices", string(p))
+ return res, client.get(a.base, "sendInvoice", addValues(vals, opts), &res)
+}
+
+// CreateInvoiceLink creates a link for an invoice.
+func (a API) CreateInvoiceLink(title, description, payload, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ p, err := json.Marshal(prices)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("title", title)
+ vals.Set("description", description)
+ vals.Set("payload", payload)
+ vals.Set("currency", currency)
+ vals.Set("prices", string(p))
+ return res, client.get(a.base, "createInvoiceLink", addValues(vals, opts), &res)
+}
+
+// AnswerShippingQuery is used to reply to shipping queries.
+// If you sent an invoice requesting a shipping address and the parameter is_flexible was specified,
+// the Bot API will send an Update with a shipping_query field to the bot.
+func (a API) AnswerShippingQuery(shippingQueryID string, ok bool, opts *ShippingQueryOptions) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("shipping_query_id", shippingQueryID)
+ vals.Set("ok", btoa(ok))
+ return res, client.get(a.base, "answerShippingQuery", addValues(vals, opts), &res)
+}
+
+// AnswerPreCheckoutQuery is used to respond to such pre-checkout queries.
+// Once the user has confirmed their payment and shipping details,
+// the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query.
+// NOTE: The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent.
+func (a API) AnswerPreCheckoutQuery(preCheckoutQueryID string, ok bool, opts *PreCheckoutOptions) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("pre_checkout_query_id", preCheckoutQueryID)
+ vals.Set("ok", btoa(ok))
+ return res, client.get(a.base, "answerPreCheckoutQuery", addValues(vals, opts), &res)
+}
+
+// GetStarTransactions returns the bot's Telegram Star transactions in chronological order.
+func (a API) GetStarTransactions(opts *StarTransactionsOptions) (res APIResponseStarTransactions, err error) {
+ return res, client.get(a.base, "getStarTransactions", urlValues(opts), &res)
+}
+
+// RefundStarPayment refunds a successful payment in Telegram Stars.
+func (a API) RefundStarPayment(userID int64, telegramPaymentChargeID string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
+ return res, client.get(a.base, "refundStarPayment", vals, &res)
+}
+
+// EditUserStarSubscription allows the bot to cancel or re-enable extension of a subscription paid in Telegram Stars.
+func (a API) EditUserStarSubscription(userID int64, telegramPaymentChargeID string, isCanceled bool) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
+ vals.Set("is_canceled", btoa(isCanceled))
+ return res, client.get(a.base, "editUserStarSubscription", vals, &res)
+}
diff --git a/shared/echotron/payments_test.go b/shared/echotron/payments_test.go
new file mode 100644
index 0000000..d8c9ba3
--- /dev/null
+++ b/shared/echotron/payments_test.go
@@ -0,0 +1,87 @@
+package echotron
+
+import "testing"
+
+func TestRevenueWithdrawalStatePending(t *testing.T) {
+ r := RevenueWithdrawalStatePending{}
+ r.ImplementsRevenueWithdrawalState()
+}
+
+func TestRevenueWithdrawalStateSucceeded(t *testing.T) {
+ r := RevenueWithdrawalStateSucceeded{}
+ r.ImplementsRevenueWithdrawalState()
+}
+
+func TestRevenueWithdrawalStateFailed(t *testing.T) {
+ r := RevenueWithdrawalStateFailed{}
+ r.ImplementsRevenueWithdrawalState()
+}
+
+func TestTransactionPartnerFragment(t *testing.T) {
+ r := TransactionPartnerFragment{}
+ r.ImplementsTransactionPartner()
+}
+
+func TestTransactionPartnerUser(t *testing.T) {
+ r := TransactionPartnerUser{}
+ r.ImplementsTransactionPartner()
+}
+func TestTransactionPartnerTelegramAds(t *testing.T) {
+ r := TransactionPartnerTelegramAds{}
+ r.ImplementsTransactionPartner()
+}
+func TestTransactionPartnerOther(t *testing.T) {
+ r := TransactionPartnerOther{}
+ r.ImplementsTransactionPartner()
+}
+
+func TestSendInvoice(t *testing.T) {
+ _, err := api.SendInvoice(
+ chatID,
+ "TestSendInvoice",
+ "TestSendInvoiceDesc",
+ "echotron_test",
+ "XTR",
+ []LabeledPrice{
+ {
+ Label: "Test",
+ Amount: 1,
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCreateInvoiceLink(t *testing.T) {
+ _, err := api.CreateInvoiceLink(
+ "TestCreateInvoiceLink",
+ "TestCreateInvoiceLinkDesc",
+ "echotron_test",
+ "XTR",
+ []LabeledPrice{
+ {
+ Label: "Test",
+ Amount: 1,
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetStarTransactions(t *testing.T) {
+ _, err := api.GetStarTransactions(
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/shared/echotron/querybuilder.go b/shared/echotron/querybuilder.go
new file mode 100644
index 0000000..64ee0a1
--- /dev/null
+++ b/shared/echotron/querybuilder.go
@@ -0,0 +1,88 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "net/url"
+ "reflect"
+ "strconv"
+)
+
+func toString(v reflect.Value) string {
+ switch v.Kind() {
+ case reflect.String:
+ return v.String()
+
+ case reflect.Float64:
+ return strconv.FormatFloat(v.Float(), 'f', -1, 64)
+
+ case reflect.Int, reflect.Int64:
+ return strconv.FormatInt(v.Int(), 10)
+
+ case reflect.Bool:
+ return strconv.FormatBool(v.Bool())
+
+ case reflect.Struct, reflect.Interface, reflect.Slice, reflect.Array:
+ b, _ := json.Marshal(v.Interface())
+ return string(b)
+
+ default:
+ return ""
+ }
+}
+
+func scan(i any, v url.Values) url.Values {
+ e := reflect.ValueOf(i)
+
+ if e.Kind() == reflect.Pointer {
+ e = e.Elem()
+ }
+
+ if e.Kind() == reflect.Invalid {
+ return v
+ }
+
+ for i := 0; i < e.NumField(); i++ {
+ fTag := e.Type().Field(i).Tag
+
+ if name := fTag.Get("query"); name != "" && !e.Field(i).IsZero() {
+ v.Set(name, toString(e.Field(i)))
+ }
+ }
+
+ return v
+}
+
+func urlValues(i any) url.Values {
+ if i == nil {
+ return nil
+ }
+ return scan(i, url.Values{})
+}
+
+func addValues(vals url.Values, i any) url.Values {
+ if i == nil {
+ return vals
+ }
+ if vals == nil {
+ vals = make(url.Values)
+ }
+ return scan(i, vals)
+}
diff --git a/shared/echotron/querybuilder_test.go b/shared/echotron/querybuilder_test.go
new file mode 100644
index 0000000..2eab672
--- /dev/null
+++ b/shared/echotron/querybuilder_test.go
@@ -0,0 +1,72 @@
+package echotron
+
+import (
+ "net/url"
+ "reflect"
+ "testing"
+)
+
+type scanTest struct {
+ i any
+ predefined url.Values
+ expected url.Values
+}
+
+func TestScan(t *testing.T) {
+ tests := []scanTest{
+ {
+ i: CommandOptions{
+ LanguageCode: "it",
+ Scope: BotCommandScope{Type: BCSTChat, ChatID: 33288},
+ },
+ predefined: url.Values{"foo": {"bar"}},
+ expected: url.Values{
+ "foo": {"bar"},
+ "language_code": {"it"},
+ "scope": {`{"type":"chat","chat_id":33288,"user_id":0}`},
+ },
+ },
+ }
+
+ for i, tt := range tests {
+ result := scan(tt.i, tt.predefined)
+ if !reflect.DeepEqual(tt.expected, result) {
+ t.Fatalf("test #%d: result differs from expected value\n", i)
+ }
+ }
+}
+
+func TestToStringDefault(t *testing.T) {
+ ret := toString(reflect.ValueOf(nil))
+
+ if ret != "" {
+ t.Fatalf("expected empty string, got %+v", ret)
+ }
+}
+
+func TestUrlValues(t *testing.T) {
+ ret := urlValues(nil)
+
+ if ret != nil {
+ t.Fatalf("expected nil, got %+v", ret)
+ }
+}
+
+func TestAddValues(t *testing.T) {
+ vals := url.Values{}
+ ret := addValues(vals, nil)
+
+ if !reflect.DeepEqual(vals, ret) {
+ t.Fatalf("expected nil, got %+v", ret)
+ }
+}
+
+func TestAddValuesNil(t *testing.T) {
+ opts := MessageOptions{ParseMode: MarkdownV2}
+ vals := urlValues(opts)
+ ret := addValues(nil, opts)
+
+ if !reflect.DeepEqual(vals, ret) {
+ t.Fatalf("expected %+v, got %+v", vals, ret)
+ }
+}
diff --git a/shared/echotron/simpledsp.go b/shared/echotron/simpledsp.go
new file mode 100644
index 0000000..29b5440
--- /dev/null
+++ b/shared/echotron/simpledsp.go
@@ -0,0 +1,137 @@
+/*
+ * Echotron
+ * Copyright (C) 2023 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+// PollingUpdates is a wrapper function for PollingUpdatesOptions.
+func PollingUpdates(token string) <-chan *Update {
+ return PollingUpdatesOptions(token, true, UpdateOptions{Timeout: 120})
+}
+
+// PollingUpdatesOptions returns a read-only channel of incoming updates from the Telegram API.
+func PollingUpdatesOptions(token string, dropPendingUpdates bool, opts UpdateOptions) <-chan *Update {
+ var updates = make(chan *Update)
+
+ go func() {
+ defer close(updates)
+
+ var (
+ api = NewAPI(token)
+ timeout = opts.Timeout
+ isFirstRun = true
+ )
+
+ // deletes webhook if present to run in long polling mode
+ if _, err := api.DeleteWebhook(dropPendingUpdates); err != nil {
+ log.Println("echotron.PollingUpdates", err)
+ }
+
+ for {
+ if isFirstRun {
+ opts.Timeout = 0
+ }
+
+ response, err := api.GetUpdates(&opts)
+ if err != nil {
+ log.Println("echotron.PollingUpdates", err)
+ time.Sleep(5 * time.Second)
+ continue
+ }
+
+ if !dropPendingUpdates || !isFirstRun {
+ for _, u := range response.Result {
+ updates <- u
+ }
+ }
+
+ if l := len(response.Result); l > 0 {
+ opts.Offset = response.Result[l-1].ID + 1
+ }
+
+ if isFirstRun {
+ isFirstRun = false
+ opts.Timeout = timeout
+ }
+ }
+ }()
+
+ return updates
+}
+
+// WebhookUpdates is a wrapper function for WebhookUpdatesOptions.
+func WebhookUpdates(url, token string) <-chan *Update {
+ return WebhookUpdatesOptions(url, token, false, nil)
+}
+
+// WebhookUpdatesOptions returns a read-only channel of incoming updates from the Telegram API.
+// The webhookUrl should be provided in the following format: ':/',
+// eg: 'https://example.com:443/bot_token'.
+// WebhookUpdatesOptions will then proceed to communicate the webhook url '/'
+// to Telegram and run a webserver that listens to ':' and handles the path.
+func WebhookUpdatesOptions(whURL, token string, dropPendingUpdates bool, opts *WebhookOptions) <-chan *Update {
+ u, err := url.Parse(whURL)
+ if err != nil {
+ panic(err)
+ }
+
+ wURL := u.Hostname() + u.EscapedPath()
+ api := NewAPI(token)
+ if _, err := api.SetWebhook(wURL, dropPendingUpdates, opts); err != nil {
+ panic(err)
+ }
+
+ var updates = make(chan *Update)
+ http.HandleFunc(u.EscapedPath(), func(w http.ResponseWriter, r *http.Request) {
+ var update Update
+
+ jsn, err := readRequest(r)
+ if err != nil {
+ log.Println("echotron.WebhookUpdates", err)
+ return
+ }
+
+ if err := json.Unmarshal(jsn, &update); err != nil {
+ log.Println("echotron.WebhookUpdates", err)
+ return
+ }
+
+ updates <- &update
+ })
+
+ go func() {
+ defer close(updates)
+ port := fmt.Sprintf(":%s", u.Port())
+ for {
+ if err := http.ListenAndServe(port, nil); err != nil {
+ log.Println("echotron.WebhookUpdates", err)
+ time.Sleep(5 * time.Second)
+ }
+ }
+ }()
+
+ return updates
+}
diff --git a/shared/echotron/simpledsp_test.go b/shared/echotron/simpledsp_test.go
new file mode 100644
index 0000000..b83ccec
--- /dev/null
+++ b/shared/echotron/simpledsp_test.go
@@ -0,0 +1,7 @@
+package echotron
+
+import "testing"
+
+func TestPollingUpdates(t *testing.T) {
+ PollingUpdates(api.token)
+}
diff --git a/shared/echotron/stickers.go b/shared/echotron/stickers.go
new file mode 100644
index 0000000..241edcc
--- /dev/null
+++ b/shared/echotron/stickers.go
@@ -0,0 +1,271 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "net/url"
+)
+
+// Sticker represents a sticker.
+type Sticker struct {
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ MaskPosition *MaskPosition `json:"mask_position,omitempty"`
+ Type StickerSetType `json:"type"`
+ FileUniqueID string `json:"file_unique_id"`
+ SetName string `json:"set_name,omitempty"`
+ FileID string `json:"file_id"`
+ Emoji string `json:"emoji,omitempty"`
+ CustomEmojiID string `json:"custom_emoji_id,omitempty"`
+ PremiumAnimation File `json:"premium_animation,omitempty"`
+ FileSize int `json:"file_size,omitempty"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ IsVideo bool `json:"is_video"`
+ IsAnimated bool `json:"is_animated"`
+ NeedsRepainting bool `json:"needs_repainting,omitempty"`
+}
+
+// StickerSet represents a sticker set.
+type StickerSet struct {
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ Title string `json:"title"`
+ Name string `json:"name"`
+ StickerType StickerSetType `json:"sticker_type"`
+ Stickers []Sticker `json:"stickers"`
+}
+
+// StickerSetType represents the type of a sticker or of the entire set
+type StickerSetType string
+
+const (
+ RegularStickerSet StickerSetType = "regular"
+ MaskStickerSet = "mask"
+ CustomEmojiStickerSet = "custom_emoji"
+)
+
+// StickerFormat is a custom type for the various sticker formats.
+type StickerFormat string
+
+// These are all the possible sticker formats.
+const (
+ StaticFormat StickerFormat = "static"
+ AnimatedFormat = "animated"
+ VideoFormat = "video"
+)
+
+// MaskPosition describes the position on faces where a mask should be placed by default.
+type MaskPosition struct {
+ Point MaskPoint `json:"point"`
+ XShift float32 `json:"x_shift"`
+ YShift float32 `json:"y_shift"`
+ Scale float32 `json:"scale"`
+}
+
+// MaskPoint is a custom type for the various part of face where a mask should be placed.
+type MaskPoint string
+
+// These are all the possible parts of the face for a mask.
+const (
+ ForeheadPoint MaskPoint = "forehead"
+ EyesPoint = "eyes"
+ MouthPoint = "mouth"
+ ChinPoint = "chin"
+)
+
+// NewStickerSetOptions contains the optional parameters used in the CreateNewStickerSet method.
+type NewStickerSetOptions struct {
+ StickerType StickerSetType `query:"sticker_type"`
+ NeedsRepainting bool `query:"needs_repainting"`
+}
+
+// InputSticker is a struct which describes a sticker to be added to a sticker set.
+type InputSticker struct {
+ MaskPosition *MaskPosition `json:"mask_position,omitempty"`
+ Keywords *[]string `json:"keywords,omitempty"`
+ Format StickerFormat `json:"format"`
+ Sticker InputFile `json:"-"`
+ EmojiList []string `json:"emoji_list"`
+}
+
+// stickerEnvelope is a generic struct for all the various structs under the InputSticker interface.
+type stickerEnvelope struct {
+ Sticker string `json:"sticker"`
+ InputSticker
+}
+
+// SendSticker is used to send static .WEBP or animated .TGS stickers.
+func (a API) SendSticker(stickerID string, chatID int64, opts *StickerOptions) (res APIResponseMessage, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("sticker", stickerID)
+ vals.Set("chat_id", itoa(chatID))
+ return res, client.get(a.base, "sendSticker", addValues(vals, opts), &res)
+}
+
+// GetStickerSet is used to get a sticker set.
+func (a API) GetStickerSet(name string) (res APIResponseStickerSet, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("name", name)
+ return res, client.get(a.base, "getStickerSet", vals, &res)
+}
+
+// GetCustomEmojiStickers is used to get information about custom emoji stickers by their identifiers.
+func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseStickers, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(customEmojiIDs)
+ vals.Set("custom_emoji_ids", string(jsn))
+ return res, client.get(a.base, "getCustomEmojiStickers", vals, &res)
+}
+
+// UploadStickerFile is used to upload a .PNG file with a sticker for later use in
+// CreateNewStickerSet and AddStickerToSet methods (can be used multiple times).
+func (a API) UploadStickerFile(userID int64, sticker InputFile, format StickerFormat) (res APIResponseFile, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("sticker_format", string(format))
+ return res, client.postFile(a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals, &res)
+}
+
+// CreateNewStickerSet is used to create a new sticker set owned by a user.
+func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []InputSticker, opts *NewStickerSetOptions) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("name", name)
+ vals.Set("title", title)
+ return res, client.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
+}
+
+// AddStickerToSet is used to add a new sticker to a set created by the bot.
+func (a API) AddStickerToSet(userID int64, name string, sticker InputSticker) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("name", name)
+ return res, client.postStickers(a.base, "addStickerToSet", vals, &res, sticker)
+}
+
+// SetStickerPositionInSet is used to move a sticker in a set created by the bot to a specific position.
+func (a API) SetStickerPositionInSet(sticker string, position int) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("sticker", sticker)
+ vals.Set("position", itoa(int64(position)))
+ return res, client.get(a.base, "setStickerPositionInSet", vals, &res)
+}
+
+// DeleteStickerFromSet is used to delete a sticker from a set created by the bot.
+func (a API) DeleteStickerFromSet(sticker string) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("sticker", sticker)
+ return res, client.get(a.base, "deleteStickerFromSet", vals, &res)
+}
+
+// ReplaceStickerInSet is used to replace an existing sticker in a sticker set with a new one.
+// The method is equivalent to calling DeleteStickerFromSet, then AddStickerToSet, then SetStickerPositionInSet.
+func (a API) ReplaceStickerInSet(userID int64, name string, old_sticker string, sticker InputSticker) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("user_id", itoa(userID))
+ vals.Set("name", name)
+ vals.Set("old_sticker", old_sticker)
+ return res, client.postStickers(a.base, "replaceStickerInSet", vals, &res, sticker)
+}
+
+// SetStickerEmojiList is used to change the list of emoji assigned to a regular or custom emoji sticker.
+// The sticker must belong to a sticker set created by the bot.
+func (a API) SetStickerEmojiList(sticker string, emojis []string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(emojis)
+
+ vals.Set("sticker", sticker)
+ vals.Set("emoji_list", string(jsn))
+ return res, client.get(a.base, "setStickerEmojiList", vals, &res)
+}
+
+// SetStickerKeywords is used to change search keywords assigned to a regular or custom emoji sticker.
+// The sticker must belong to a sticker set created by the bot.
+func (a API) SetStickerKeywords(sticker string, keywords []string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(keywords)
+
+ vals.Set("sticker", sticker)
+ vals.Set("keywords", string(jsn))
+ return res, client.get(a.base, "setStickerKeywords", vals, &res)
+}
+
+// SetStickerMaskPosition is used to change the mask position of a mask sticker.
+// The sticker must belong to a sticker set that was created by the bot.
+func (a API) SetStickerMaskPosition(sticker string, mask MaskPosition) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ jsn, _ := json.Marshal(mask)
+
+ vals.Set("sticker", sticker)
+ vals.Set("mask_position", string(jsn))
+ return res, client.get(a.base, "setStickerMaskPosition", vals, &res)
+}
+
+// SetStickerSetTitle is used to set the title of a created sticker set.
+func (a API) SetStickerSetTitle(name, title string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("name", name)
+ vals.Set("title", title)
+ return res, client.get(a.base, "setStickerSetTitle", vals, &res)
+}
+
+// SetStickerSetThumbnail is used to set the thumbnail of a sticker set.
+func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFile, format StickerFormat) (res APIResponseBase, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("name", name)
+ vals.Set("user_id", itoa(userID))
+ vals.Set("format", string(format))
+ return res, client.postFile(a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals, &res)
+}
+
+// SetCustomEmojiStickerSetThumbnail is used to set the thumbnail of a custom emoji sticker set.
+func (a API) SetCustomEmojiStickerSetThumbnail(name, emojiID string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("name", name)
+ vals.Set("custom_emoji_id", emojiID)
+ return res, client.get(a.base, "setCustomEmojiStickerSetThumbnail", vals, &res)
+}
+
+// DeleteStickerSet is used to delete a sticker set that was created by the bot.
+func (a API) DeleteStickerSet(name string) (res APIResponseBool, err error) {
+ var vals = make(url.Values)
+
+ vals.Set("name", name)
+ return res, client.get(a.base, "DeleteStickerSet", vals, &res)
+}
+
+// GetForumTopicIconStickers is used to get custom emoji stickers, which can be used as a forum topic icon by any user.
+func (a API) GetForumTopicIconStickers() (res APIResponseStickers, err error) {
+ return res, client.get(a.base, "getForumTopicIconStickers", nil, &res)
+}
diff --git a/shared/echotron/stickers_test.go b/shared/echotron/stickers_test.go
new file mode 100644
index 0000000..84c649c
--- /dev/null
+++ b/shared/echotron/stickers_test.go
@@ -0,0 +1,229 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "fmt"
+ "testing"
+ "time"
+)
+
+var (
+ stickerFile *File
+ stickerSet *StickerSet
+ stickerSetName = fmt.Sprintf("set%d_by_echotron_coverage_bot", time.Now().Unix())
+)
+
+func TestUploadStickerFile(t *testing.T) {
+ resp, err := api.UploadStickerFile(
+ chatID,
+ NewInputFilePath("assets/tests/echotron_test.png"),
+ StaticFormat,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ stickerFile = resp.Result
+}
+
+func TestCreateNewStickerSet(t *testing.T) {
+ _, err := api.CreateNewStickerSet(
+ chatID,
+ stickerSetName,
+ "Echotron Coverage Pack",
+ []InputSticker{
+ {
+ Sticker: NewInputFileID(stickerFile.FileID),
+ EmojiList: []string{"🤖"},
+ Format: StaticFormat,
+ },
+ {
+ Sticker: NewInputFilePath("assets/tests/echotron_test.png"),
+ EmojiList: []string{"🤖"},
+ Format: StaticFormat,
+ },
+ {
+ Sticker: NewInputFileURL(photoURL),
+ EmojiList: []string{"🤖"},
+ Format: StaticFormat,
+ },
+ },
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestAddStickerToSet(t *testing.T) {
+ _, err := api.AddStickerToSet(
+ chatID,
+ stickerSetName,
+ InputSticker{
+ Sticker: NewInputFilePath("assets/tests/echotron_sticker.png"),
+ EmojiList: []string{"🤖"},
+ Format: StaticFormat,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetCustomEmojiStickers(t *testing.T) {
+ _, err := api.GetCustomEmojiStickers(
+ "5407041870620531251",
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetStickerSet(t *testing.T) {
+ resp, err := api.GetStickerSet(
+ stickerSetName,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ stickerSet = resp.Result
+}
+
+func TestSetStickerPositionInSet(t *testing.T) {
+ _, err := api.SetStickerPositionInSet(
+ stickerSet.Stickers[1].FileID,
+ 0,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetStickerEmojiList(t *testing.T) {
+ _, err := api.SetStickerEmojiList(
+ stickerSet.Stickers[0].FileID,
+ []string{"🤖", "👾"},
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetStickerKeywords(t *testing.T) {
+ _, err := api.SetStickerKeywords(
+ stickerSet.Stickers[0].FileID,
+ []string{"echotron"},
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetStickerSetTitle(t *testing.T) {
+ _, err := api.SetStickerSetTitle(
+ stickerSetName,
+ fmt.Sprintf("new_%s", stickerSetName),
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestReplaceStickerInSet(t *testing.T) {
+ _, err := api.ReplaceStickerInSet(
+ chatID,
+ stickerSetName,
+ stickerSet.Stickers[0].FileID,
+ InputSticker{
+ Sticker: NewInputFileURL(photoURL),
+ EmojiList: []string{"🤖"},
+ Format: StaticFormat,
+ },
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteStickerFromSet(t *testing.T) {
+ _, err := api.DeleteStickerFromSet(
+ stickerSet.Stickers[1].FileID,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSendSticker(t *testing.T) {
+ _, err := api.SendSticker(
+ stickerSet.Stickers[0].FileID,
+ chatID,
+ nil,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetStickerSetThumbnail(t *testing.T) {
+ _, err := api.SetStickerSetThumbnail(
+ stickerSetName,
+ chatID,
+ NewInputFilePath("assets/tests/echotron_thumb.png"),
+ StaticFormat,
+ )
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeleteStickerSet(t *testing.T) {
+ _, err := api.DeleteStickerSet(stickerSetName)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetForumTopicIconStickers(t *testing.T) {
+ res, err := api.GetForumTopicIconStickers()
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(res.Result) == 0 {
+ t.Fatal("error: Telegram returned no forum topic icon stickers")
+ }
+}
diff --git a/shared/echotron/types.go b/shared/echotron/types.go
new file mode 100644
index 0000000..4ff4ced
--- /dev/null
+++ b/shared/echotron/types.go
@@ -0,0 +1,1986 @@
+/*
+ * Echotron
+ * Copyright (C) 2018 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import "encoding/json"
+
+// Update represents an incoming update.
+// At most one of the optional parameters can be present in any given update.
+type Update struct {
+ ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
+ ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"`
+ RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"`
+ Message *Message `json:"message,omitempty"`
+ EditedMessage *Message `json:"edited_message,omitempty"`
+ ChannelPost *Message `json:"channel_post,omitempty"`
+ EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
+ BusinessConnection *BusinessConnection `json:"business_connection,omitempty"`
+ BusinessMessage *Message `json:"business_message,omitempty"`
+ EditedBusinessMessage *Message `json:"edited_business_message,omitempty"`
+ DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"`
+ MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
+ MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
+ InlineQuery *InlineQuery `json:"inline_query,omitempty"`
+ ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"`
+ CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
+ ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
+ PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
+ Poll *Poll `json:"poll,omitempty"`
+ PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
+ MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"`
+ ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"`
+ PurchasedPaidMedia *PaidMediaPurchased `json:"purchased_paid_media,omitempty"`
+ ID int `json:"update_id"`
+}
+
+// ChatID returns the ID of the chat the update is coming from.
+func (u Update) ChatID() int64 {
+ switch {
+ case u.ChatJoinRequest != nil:
+ return u.ChatJoinRequest.Chat.ID
+ case u.ChatBoost != nil:
+ return u.ChatBoost.Chat.ID
+ case u.RemovedChatBoost != nil:
+ return u.RemovedChatBoost.Chat.ID
+ case u.Message != nil:
+ return u.Message.Chat.ID
+ case u.EditedMessage != nil:
+ return u.EditedMessage.Chat.ID
+ case u.ChannelPost != nil:
+ return u.ChannelPost.Chat.ID
+ case u.EditedChannelPost != nil:
+ return u.EditedChannelPost.Chat.ID
+ case u.BusinessConnection != nil:
+ return u.BusinessConnection.User.ID
+ case u.BusinessMessage != nil:
+ return u.BusinessMessage.Chat.ID
+ case u.EditedBusinessMessage != nil:
+ return u.EditedBusinessMessage.Chat.ID
+ case u.DeletedBusinessMessages != nil:
+ return u.DeletedBusinessMessages.Chat.ID
+ case u.MessageReaction != nil:
+ return u.MessageReaction.Chat.ID
+ case u.MessageReactionCount != nil:
+ return u.MessageReactionCount.Chat.ID
+ case u.InlineQuery != nil:
+ return u.InlineQuery.From.ID
+ case u.ChosenInlineResult != nil:
+ return u.ChosenInlineResult.From.ID
+ case u.CallbackQuery != nil:
+ return u.CallbackQuery.Message.Chat.ID
+ case u.ShippingQuery != nil:
+ return u.ShippingQuery.From.ID
+ case u.PreCheckoutQuery != nil:
+ return u.PreCheckoutQuery.From.ID
+ case u.PollAnswer != nil:
+ return u.PollAnswer.User.ID
+ case u.MyChatMember != nil:
+ return u.MyChatMember.Chat.ID
+ case u.ChatMember != nil:
+ return u.ChatMember.Chat.ID
+ default:
+ return 0
+ }
+}
+
+// WebhookInfo contains information about the current status of a webhook.
+type WebhookInfo struct {
+ URL string `json:"url"`
+ IPAddress string `json:"ip_address,omitempty"`
+ LastErrorMessage string `json:"last_error_message,omitempty"`
+ AllowedUpdates []*UpdateType `json:"allowed_updates,omitempty"`
+ MaxConnections int `json:"max_connections,omitempty"`
+ LastErrorDate int64 `json:"last_error_date,omitempty"`
+ LastSynchronizationErrorDate int64 `json:"last_synchronization_error_date,omitempty"`
+ PendingUpdateCount int `json:"pending_update_count"`
+ HasCustomCertificate bool `json:"has_custom_certificate"`
+}
+
+// APIResponse is implemented by all the APIResponse* types.
+type APIResponse interface {
+ // Base returns the object of type APIResponseBase contained in each implemented type.
+ Base() APIResponseBase
+}
+
+// APIResponseBase is a base type that represents the incoming response from Telegram servers.
+// Used by APIResponse* to slim down the implementation.
+type APIResponseBase struct {
+ Description string `json:"description,omitempty"`
+ ErrorCode int `json:"error_code,omitempty"`
+ Ok bool `json:"ok"`
+}
+
+// Base returns the APIResponseBase itself.
+func (a APIResponseBase) Base() APIResponseBase {
+ return a
+}
+
+// APIResponseUpdate represents the incoming response from Telegram servers.
+// Used by all methods that return an array of Update objects on success.
+type APIResponseUpdate struct {
+ Result []*Update `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseUpdate) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseUser represents the incoming response from Telegram servers.
+// Used by all methods that return a User object on success.
+type APIResponseUser struct {
+ Result *User `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseUser) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseMessage represents the incoming response from Telegram servers.
+// Used by all methods that return a Message object on success.
+type APIResponseMessage struct {
+ Result *Message `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseMessage) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseMessageArray represents the incoming response from Telegram servers.
+// Used by all methods that return an array of Message objects on success.
+type APIResponseMessageArray struct {
+ Result []*Message `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseMessageArray) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseMessageID represents the incoming response from Telegram servers.
+// Used by all methods that return a MessageID object on success.
+type APIResponseMessageID struct {
+ Result *MessageID `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseMessageID) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseMessageIDs represents the incoming response from Telegram servers.
+// Used by all methods that return a MessageID object on success.
+type APIResponseMessageIDs struct {
+ Result []*MessageID `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseMessageIDs) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseCommands represents the incoming response from Telegram servers.
+// Used by all methods that return an array of BotCommand objects on success.
+type APIResponseCommands struct {
+ Result []*BotCommand `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseCommands) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseBool represents the incoming response from Telegram servers.
+// Used by all methods that return True on success.
+type APIResponseBool struct {
+ APIResponseBase
+ Result bool `json:"result,omitempty"`
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseBool) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseString represents the incoming response from Telegram servers.
+// Used by all methods that return a string on success.
+type APIResponseString struct {
+ Result string `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseString) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseChat represents the incoming response from Telegram servers.
+// Used by all methods that return a ChatFullInfo object on success.
+type APIResponseChat struct {
+ Result *ChatFullInfo `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseChat) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseInviteLink represents the incoming response from Telegram servers.
+// Used by all methods that return a ChatInviteLink object on success.
+type APIResponseInviteLink struct {
+ Result *ChatInviteLink `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseInviteLink) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseStickers represents the incoming response from Telegram servers.
+// Used by all methods that return an array of Stickers on success.
+type APIResponseStickers struct {
+ Result []*Sticker `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseStickers) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseStickerSet represents the incoming response from Telegram servers.
+// Used by all methods that return a StickerSet object on success.
+type APIResponseStickerSet struct {
+ Result *StickerSet `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseStickerSet) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseUserProfile represents the incoming response from Telegram servers.
+// Used by all methods that return a UserProfilePhotos object on success.
+type APIResponseUserProfile struct {
+ Result *UserProfilePhotos `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseUserProfile) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseUserProfileAudios represents the incoming response from Telegram servers.
+// Used by all methods that return a UserProfileAudios object on success.
+type APIResponseUserProfileAudios struct {
+ Result *UserProfileAudios `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseUserProfileAudios) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseFile represents the incoming response from Telegram servers.
+// Used by all methods that return a File object on success.
+type APIResponseFile struct {
+ Result *File `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseFile) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseAdministrators represents the incoming response from Telegram servers.
+// Used by all methods that return an array of ChatMember objects on success.
+type APIResponseAdministrators struct {
+ Result []*ChatMember `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseAdministrators) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseChatMember represents the incoming response from Telegram servers.
+// Used by all methods that return a ChatMember object on success.
+type APIResponseChatMember struct {
+ Result *ChatMember `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseChatMember) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseInteger represents the incoming response from Telegram servers.
+// Used by all methods that return an integer on success.
+type APIResponseInteger struct {
+ APIResponseBase
+ Result int `json:"result,omitempty"`
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseInteger) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponsePoll represents the incoming response from Telegram servers.
+// Used by all methods that return a Poll object on success.
+type APIResponsePoll struct {
+ Result *Poll `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponsePoll) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseGameHighScore represents the incoming response from Telegram servers.
+// Used by all methods that return an array of GameHighScore objects on success.
+type APIResponseGameHighScore struct {
+ Result []*GameHighScore `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseGameHighScore) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseWebhook represents the incoming response from Telegram servers.
+// Used by all methods that return a WebhookInfo object on success.
+type APIResponseWebhook struct {
+ Result *WebhookInfo `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseWebhook) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseSentWebAppMessage represents the incoming response from Telegram servers.
+// Used by all methods that return a SentWebAppMessage object on success.
+type APIResponseSentWebAppMessage struct {
+ Result *SentWebAppMessage `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseSentWebAppMessage) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseMenuButton represents the incoming response from Telegram servers.
+// Used by all methods that return a MenuButton object on success.
+type APIResponseMenuButton struct {
+ Result *MenuButton `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseMenuButton) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseChatAdministratorRights represents the incoming response from Telegram servers.
+// Used by all methods that return a ChatAdministratorRights object on success.
+type APIResponseChatAdministratorRights struct {
+ Result *ChatAdministratorRights `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseChatAdministratorRights) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseForumTopic represents the incoming response from Telegram servers.
+// Used by all methods that return a ForumTopic object on success.
+type APIResponseForumTopic struct {
+ Result *ForumTopic `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseForumTopic) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseBotDescription represents the incoming response from Telegram servers.
+// Used by all methods that return a BotDescription object on success.
+type APIResponseBotDescription struct {
+ Result *BotDescription `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseBotDescription) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseBotShortDescription represents the incoming response from Telegram servers.
+// Used by all methods that return a BotShortDescription object on success.
+type APIResponseBotShortDescription struct {
+ Result *BotShortDescription `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseBotShortDescription) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseBotName represents the incoming response from Telegram servers.
+// Used by all methods that return a BotName object on success.
+type APIResponseBotName struct {
+ Result *BotName `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseBotName) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseUserChatBoosts represents the incoming response from Telegram servers.
+// Used by all methods that return a UserChatBoosts object on success.
+type APIResponseUserChatBoosts struct {
+ Result *UserChatBoosts `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseUserChatBoosts) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseBusinessConnection represents the incoming response from Telegram servers.
+// Used by all methods that return a BusinessConnection object on success.
+type APIResponseBusinessConnection struct {
+ Result *BusinessConnection `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseBusinessConnection) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseStarTransactions represents the incoming response from Telegram servers.
+// Used by all methods that return a StarTransactions object on success.
+type APIResponseStarTransactions struct {
+ Result *StarTransactions `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseStarTransactions) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponsePreparedInlineMessage represents the incoming response from Telegram servers.
+// Used by all methods that return a PreparedInlineMessage object on success.
+type APIResponsePreparedInlineMessage struct {
+ Result *PreparedInlineMessage `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponsePreparedInlineMessage) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// APIResponseGifts represents the incoming response from Telegram servers.
+// Used by all methods that return a Gifts object on success.
+type APIResponseGifts struct {
+ Result *Gifts `json:"result,omitempty"`
+ APIResponseBase
+}
+
+// Base returns the contained object of type APIResponseBase.
+func (a APIResponseGifts) Base() APIResponseBase {
+ return a.APIResponseBase
+}
+
+// User represents a Telegram user or bot.
+type User struct {
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name,omitempty"`
+ Username string `json:"username,omitempty"`
+ LanguageCode string `json:"language_code,omitempty"`
+ ID int64 `json:"id"`
+ IsBot bool `json:"is_bot"`
+ IsPremium bool `json:"is_premium,omitempty"`
+ AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"`
+ CanJoinGroups bool `json:"can_join_groups,omitempty"`
+ CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"`
+ SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"`
+ CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"`
+ HasMainWebApp bool `json:"has_main_web_app,omitempty"`
+ AllowsUsersToCreateTopics bool `json:"allows_users_to_create_topics,omitempty"`
+}
+
+// Chat represents a chat.
+type Chat struct {
+ Type string `json:"type"`
+ Title string `json:"title,omitempty"`
+ Username string `json:"username,omitempty"`
+ FirstName string `json:"first_name,omitempty"`
+ LastName string `json:"last_name,omitempty"`
+ ID int64 `json:"id"`
+ IsForum bool `json:"is_forum,omitempty"`
+}
+
+// ChatFullInfo contains full information about a chat.
+type ChatFullInfo struct {
+ Permissions *ChatPermissions `json:"permissions,omitempty"`
+ Location *ChatLocation `json:"location,omitempty"`
+ PinnedMessage *Message `json:"pinned_message,omitempty"`
+ Photo *ChatPhoto `json:"photo,omitempty"`
+ ActiveUsernames *[]string `json:"active_usernames,omitempty"`
+ AvailableReactions *[]ReactionType `json:"available_reactions,omitempty"`
+ BusinessIntro *BusinessIntro `json:"business_intro,omitempty"`
+ BusinessLocation *BusinessLocation `json:"business_location,omitempty"`
+ BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"`
+ PersonalChat *Chat `json:"personal_chat,omitempty"`
+ Birthdate *Birthdate `json:"birthdate,omitempty"`
+ FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"`
+ BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"`
+ ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"`
+ Bio string `json:"bio,omitempty"`
+ Username string `json:"username,omitempty"`
+ Title string `json:"title,omitempty"`
+ StickerSetName string `json:"sticker_set_name,omitempty"`
+ Description string `json:"description,omitempty"`
+ FirstName string `json:"first_name,omitempty"`
+ LastName string `json:"last_name,omitempty"`
+ InviteLink string `json:"invite_link,omitempty"`
+ EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"`
+ Type string `json:"type"`
+ CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"`
+ AccentColorID int `json:"accent_color_id,omitempty"`
+ MaxReactionCount int `json:"max_reaction_count,omitempty"`
+ ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"`
+ EmojiStatusExpirationDate int `json:"emoji_status_expiration_date,omitempty"`
+ MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"`
+ SlowModeDelay int `json:"slow_mode_delay,omitempty"`
+ UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"`
+ LinkedChatID int64 `json:"linked_chat_id,omitempty"`
+ ID int64 `json:"id"`
+ IsForum bool `json:"is_forum,omitempty"`
+ CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"`
+ HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"`
+ HasHiddenMembers bool `json:"has_hidden_members,omitempty"`
+ HasProtectedContent bool `json:"has_protected_content,omitempty"`
+ HasVisibleHistory bool `json:"has_visible_history,omitempty"`
+ HasPrivateForwards bool `json:"has_private_forwards,omitempty"`
+ CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
+ JoinToSendMessages bool `json:"join_to_send_messages,omitempty"`
+ JoinByRequest bool `json:"join_by_request,omitempty"`
+ HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"`
+ AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types,omitempty"`
+}
+
+type AcceptedGiftTypes struct {
+ UnlimitedGifts bool `json:"unlimited_gifs,omitempty"`
+ LimitedGifts bool `json:"limited_gifts,omitempty"`
+ UniqueGifts bool `json:"unique_gifs,omitempty"`
+ PremiumSubscription bool `json:"premium_subscription,omitempty"`
+}
+
+// Message represents a message.
+type Message struct {
+ MessageAutoDeleteTimerChanged *MessageAutoDeleteTimerChanged `json:"message_auto_delete_timer_changed,omitempty"`
+ Contact *Contact `json:"contact,omitempty"`
+ SenderChat *Chat `json:"sender_chat,omitempty"`
+ WebAppData *WebAppData `json:"web_app_data,omitempty"`
+ From *User `json:"from,omitempty"`
+ VideoChatParticipantsInvited *VideoChatParticipantsInvited `json:"video_chat_participants_invited,omitempty"`
+ Invoice *Invoice `json:"invoice,omitempty"`
+ SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"`
+ RefundedPayment *RefundedPayment `json:"refunded_payment,omitempty"`
+ VideoChatEnded *VideoChatEnded `json:"video_chat_ended,omitempty"`
+ VideoChatStarted *VideoChatStarted `json:"video_chat_started,omitempty"`
+ ReplyToMessage *Message `json:"reply_to_message,omitempty"`
+ ViaBot *User `json:"via_bot,omitempty"`
+ Poll *Poll `json:"poll,omitempty"`
+ ProximityAlertTriggered *ProximityAlertTriggered `json:"proximity_alert_triggered,omitempty"`
+ ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+ Document *Document `json:"document,omitempty"`
+ PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"`
+ PinnedMessage *Message `json:"pinned_message,omitempty"`
+ LeftChatMember *User `json:"left_chat_member,omitempty"`
+ Animation *Animation `json:"animation,omitempty"`
+ Audio *Audio `json:"audio,omitempty"`
+ Voice *Voice `json:"voice,omitempty"`
+ Location *Location `json:"location,omitempty"`
+ Sticker *Sticker `json:"sticker,omitempty"`
+ Video *Video `json:"video,omitempty"`
+ VideoNote *VideoNote `json:"video_note,omitempty"`
+ Venue *Venue `json:"venue,omitempty"`
+ Game *Game `json:"game,omitempty"`
+ Dice *Dice `json:"dice,omitempty"`
+ ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"`
+ ForumTopicEdited *ForumTopicEdited `json:"forum_topic_edited,omitempty"`
+ VideoChatScheduled *VideoChatScheduled `json:"video_chat_scheduled,omitempty"`
+ ForumTopicClosed *ForumTopicClosed `json:"forum_topic_closed,omitempty"`
+ ForumTopicReopened *ForumTopicReopened `json:"forum_topic_reopened,omitempty"`
+ GeneralForumTopicHidden *GeneralForumTopicHidden `json:"general_forum_topic_hidden,omitempty"`
+ GeneralForumTopicUnhidden *GeneralForumTopicUnhidden `json:"general_forum_topic_unhidden,omitempty"`
+ ChatOwnerLeft *ChatOwnerLeft `json:"chat_owner_left,omitempty"`
+ ChatOwnerChanged *ChatOwnerChanged `json:"chat_owner_changed,omitempty"`
+ GiveawayCreated *GiveawayCreated `json:"giveaway_created,omitempty"`
+ Giveaway *Giveaway `json:"giveaway,omitempty"`
+ GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"`
+ GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed,omitempty"`
+ WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"`
+ UsersShared *UsersShared `json:"users_shared,omitempty"`
+ ChatShared *ChatShared `json:"chat_shared,omitempty"`
+ Story *Story `json:"story,omitempty"`
+ ReplyToStory *Story `json:"reply_to_story,omitempty"`
+ ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"`
+ Quote *TextQuote `json:"quote,omitempty"`
+ LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
+ ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
+ BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"`
+ ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"`
+ SenderBusinessBot *User `json:"sender_business_bot,omitempty"`
+ MediaGroupID string `json:"media_group_id,omitempty"`
+ ConnectedWebsite string `json:"connected_website,omitempty"`
+ NewChatTitle string `json:"new_chat_title,omitempty"`
+ AuthorSignature string `json:"author_signature,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ Text string `json:"text,omitempty"`
+ BusinessConnectionID string `json:"business_connection_id,omitempty"`
+ EffectID string `json:"effect_id,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ NewChatPhoto []*PhotoSize `json:"new_chat_photo,omitempty"`
+ NewChatMembers []*User `json:"new_chat_members,omitempty"`
+ Photo []*PhotoSize `json:"photo,omitempty"`
+ Entities []*MessageEntity `json:"entities,omitempty"`
+ Chat Chat `json:"chat"`
+ ID int `json:"message_id"`
+ ThreadID int `json:"message_thread_id,omitempty"`
+ MigrateFromChatID int `json:"migrate_from_chat_id,omitempty"`
+ Date int `json:"date"`
+ MigrateToChatID int `json:"migrate_to_chat_id,omitempty"`
+ EditDate int `json:"edit_date,omitempty"`
+ SenderBoostCount int `json:"sender_boost_count,omitempty"`
+ DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"`
+ IsTopicMessage bool `json:"is_topic_message,omitempty"`
+ IsAutomaticForward bool `json:"is_automatic_forward,omitempty"`
+ GroupChatCreated bool `json:"group_chat_created,omitempty"`
+ SupergroupChatCreated bool `json:"supergroup_chat_created,omitempty"`
+ ChannelChatCreated bool `json:"channel_chat_created,omitempty"`
+ HasProtectedContent bool `json:"has_protected_content,omitempty"`
+ HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
+ IsFromOffline bool `json:"is_from_offline,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// MessageID represents a unique message identifier.
+type MessageID struct {
+ MessageID int `json:"message_id"`
+}
+
+// MessageEntity represents one special entity in a text message.
+// For example, hashtags, usernames, URLs, etc.
+type MessageEntity struct {
+ User *User `json:"user,omitempty"`
+ Type MessageEntityType `json:"type"`
+ URL string `json:"url,omitempty"`
+ Language string `json:"language,omitempty"`
+ CustomEmojiID string `json:"custom_emoji_id,omitempty"`
+ Offset int `json:"offset"`
+ Length int `json:"length"`
+}
+
+// PhotoSize represents one size of a photo or a file / sticker thumbnail.
+type PhotoSize struct {
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ FileSize int `json:"file_size,omitempty"`
+}
+
+// Animation represents an animation file (GIF or H.264/MPEG-4 AVC video without sound).
+type Animation struct {
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ FileName string `json:"file_name,omitempty"`
+ MimeType string `json:"mime_type,omitempty"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Duration int `json:"duration"`
+ FileSize int64 `json:"file_size,omitempty"`
+}
+
+// Audio represents an audio file to be treated as music by the Telegram clients.
+type Audio struct {
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ Performer string `json:"performer,omitempty"`
+ Title string `json:"title,omitempty"`
+ FileName string `json:"file_name,omitempty"`
+ MimeType string `json:"mime_type,omitempty"`
+ FileSize int64 `json:"file_size,omitempty"`
+ Duration int `json:"duration"`
+}
+
+// Document represents a general file (as opposed to photos, voice messages and audio files).
+type Document struct {
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ FileName string `json:"file_name,omitempty"`
+ MimeType string `json:"mime_type,omitempty"`
+ FileSize int64 `json:"file_size,omitempty"`
+}
+
+// Video represents a video file.
+type Video struct {
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ FileName string `json:"file_name,omitempty"`
+ MimeType string `json:"mime_type,omitempty"`
+ Qualities []VideoQuality `json:"qualities,omitempty"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Duration int `json:"duration"`
+ FileSize int64 `json:"file_size,omitempty"`
+ Cover []PhotoSize `json:"cover,omitempty"`
+ StartTimestamp int `json:"start_timestamp,omitempty"`
+}
+
+// VideoQuality describes an available quality of a video.
+type VideoQuality struct {
+ Type string `json:"type"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+}
+
+// VideoNote represents a video message (available in Telegram apps as of v.4.0).
+type VideoNote struct {
+ Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ Length int `json:"length"`
+ Duration int `json:"duration"`
+ FileSize int `json:"file_size,omitempty"`
+}
+
+// Voice represents a voice note.
+type Voice struct {
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ MimeType string `json:"mime_type,omitempty"`
+ Duration int `json:"duration"`
+ FileSize int64 `json:"file_size,omitempty"`
+}
+
+// PaidMediaInfo describes the paid media added to a message.
+type PaidMediaInfo struct {
+ PaidMedia []PaidMedia `json:"paid_media"`
+ StarCount int `json:"star_count"`
+}
+
+// PaidMedia describes paid media.
+type PaidMedia struct {
+ Photo *[]PhotoSize `json:"photo,omitempty"`
+ Video *Video `json:"video,omitempty"`
+ Type string `json:"type"`
+ Width int `json:"width,omitempty"`
+ Height int `json:"height,omitempty"`
+ Duration int `json:"duration,omitempty"`
+}
+
+// Contact represents a phone contact.
+type Contact struct {
+ PhoneNumber string `json:"phone_number"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name,omitempty"`
+ VCard string `json:"vcard,omitempty"`
+ UserID int `json:"user_id,omitempty"`
+}
+
+// Dice represents an animated emoji that displays a random value.
+type Dice struct {
+ Emoji string `json:"emoji"`
+ Value int `json:"value"`
+}
+
+// PollOption contains information about one answer option in a poll.
+type PollOption struct {
+ Text string `json:"text"`
+ TextEntities []*MessageEntity `json:"text_entities,omitempty"`
+ VoterCount int `json:"voter_count"`
+}
+
+// InputPollOption contains information about one answer option in a poll to send.
+type InputPollOption struct {
+ Text string `json:"text"`
+ TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
+ TextEntities []*MessageEntity `json:"text_entities,omitempty"`
+}
+
+// PollAnswer represents an answer of a user in a non-anonymous poll.
+type PollAnswer struct {
+ PollID string `json:"poll_id"`
+ VoterChat *Chat `json:"chat,omitempty"`
+ User *User `json:"user,omitempty"`
+ OptionIDs []int `json:"option_ids"`
+}
+
+// Poll contains information about a poll.
+type Poll struct {
+ Type string `json:"type"`
+ Question string `json:"question"`
+ Explanation string `json:"explanation,omitempty"`
+ ID string `json:"id"`
+ ExplanationEntities []*MessageEntity `json:"explanation_entities,omitempty"`
+ QuestionEntities []*MessageEntity `json:"question_entities,omitempty"`
+ Options []*PollOption `json:"options"`
+ OpenPeriod int `json:"open_period,omitempty"`
+ TotalVoterCount int `json:"total_voter_count"`
+ CorrectOptionID int `json:"correct_option_id,omitempty"`
+ CloseDate int `json:"close_date,omitempty"`
+ AllowsMultipleAnswers bool `json:"allows_multiple_answers"`
+ IsClosed bool `json:"is_closed"`
+ IsAnonymous bool `json:"is_anonymous"`
+}
+
+// Location represents a point on the map.
+type Location struct {
+ Longitude float64 `json:"longitude"`
+ Latitude float64 `json:"latitude"`
+ HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"`
+ LivePeriod int `json:"live_period,omitempty"`
+ Heading int `json:"heading,omitempty"`
+ ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"`
+}
+
+// Venue represents a venue.
+type Venue struct {
+ Location *Location `json:"location"`
+ Title string `json:"title"`
+ Address string `json:"address"`
+ FoursquareID string `json:"foursquare_id,omitempty"`
+ FoursquareType string `json:"foursquare_type,omitempty"`
+ GooglePlaceID string `json:"google_place_id,omitempty"`
+ GooglePlaceType string `json:"google_place_type,omitempty"`
+}
+
+// ProximityAlertTriggered represents the content of a service message, sent whenever a user in the chat triggers a proximity alert set by another user.
+type ProximityAlertTriggered struct {
+ Traveler *User `json:"traveler"`
+ Watcher *User `json:"watcher"`
+ Distance int `json:"distance"`
+}
+
+// MessageAutoDeleteTimerChanged represents a service message about a change in auto-delete timer settings.
+type MessageAutoDeleteTimerChanged struct {
+ MessageAutoDeleteTime int `json:"message_auto_delete_time"`
+}
+
+// VideoChatScheduled represents a service message about a voice chat scheduled in the chat.
+type VideoChatScheduled struct {
+ StartDate int `json:"start_date"`
+}
+
+// VideoChatStarted represents a service message about a voice chat started in the chat.
+type VideoChatStarted struct{}
+
+// VideoChatEnded represents a service message about a voice chat ended in the chat.
+type VideoChatEnded struct {
+ Duration int `json:"duration"`
+}
+
+// VideoChatParticipantsInvited represents a service message about new members invited to a voice chat.
+type VideoChatParticipantsInvited struct {
+ Users []*User `json:"users,omitempty"`
+}
+
+// UserProfilePhotos represents a user's profile pictures.
+type UserProfilePhotos struct {
+ Photos [][]PhotoSize `json:"photos"`
+ TotalCount int `json:"total_count"`
+}
+
+// UserProfileAudios represents a list of audios added to a user's profile.
+type UserProfileAudios struct {
+ Audios []Audio `json:"audios"`
+ TotalCount int `json:"total_count"`
+}
+
+// File represents a file ready to be downloaded.
+type File struct {
+ FileID string `json:"file_id"`
+ FileUniqueID string `json:"file_unique_id"`
+ FilePath string `json:"file_path,omitempty"`
+ FileSize int64 `json:"file_size,omitempty"`
+}
+
+// LoginURL represents a parameter of the inline keyboard button used to automatically authorize a user.
+type LoginURL struct {
+ URL string `json:"url"`
+ ForwardText string `json:"forward_text,omitempty"`
+ BotUsername string `json:"bot_username,omitempty"`
+ RequestWriteAccess bool `json:"request_write_access,omitempty"`
+}
+
+// SwitchInlineQueryChosenChat represents an inline button that switches the current user to inline mode in a chosen chat, with an optional default inline query.
+type SwitchInlineQueryChosenChat struct {
+ Query string `json:"query,omitempty"`
+ AllowUserChats bool `json:"allow_user_chats,omitempty"`
+ AllowBotChats bool `json:"allow_bot_chats,omitempty"`
+ AllowGroupChats bool `json:"allow_group_chats,omitempty"`
+ AllowChannelChats bool `json:"allow_channel_chats,omitempty"`
+}
+
+// CallbackQuery represents an incoming callback query from a callback button in an inline keyboard.
+// If the button that originated the query was attached to a message sent by the bot,
+// the field message will be present. If the button was attached to a message sent via the bot (in inline mode),
+// the field inline_message_id will be present. Exactly one of the fields data or game_short_name will be present.
+type CallbackQuery struct {
+ ID string `json:"id"`
+ From *User `json:"from"`
+ Message *Message `json:"message,omitempty"`
+ InlineMessageID string `json:"inline_message_id,omitempty"`
+ ChatInstance string `json:"chat_instance,omitempty"`
+ Data string `json:"data,omitempty"`
+ GameShortName string `json:"game_short_name,omitempty"`
+}
+
+// ChatPhoto represents a chat photo.
+type ChatPhoto struct {
+ SmallFileID string `json:"small_file_id"`
+ SmallFileUniqueID string `json:"small_file_unique_id"`
+ BigFileID string `json:"big_file_id"`
+ BigFileUniqueID string `json:"big_file_unique_id"`
+}
+
+// ChatInviteLink represents an invite link for a chat.
+type ChatInviteLink struct {
+ Creator *User `json:"creator"`
+ InviteLink string `json:"invite_link"`
+ Name string `json:"name,omitempty"`
+ PendingJoinRequestCount int `json:"pending_join_request_count,omitempty"`
+ ExpireDate int `json:"expire_date,omitempty"`
+ MemberLimit int `json:"member_limit,omitempty"`
+ IsPrimary bool `json:"is_primary"`
+ IsRevoked bool `json:"is_revoked"`
+ CreatesJoinRequest bool `json:"creates_join_request"`
+}
+
+// ChatMember contains information about one member of a chat.
+type ChatMember struct {
+ User *User `json:"user"`
+ Status string `json:"status"`
+ CustomTitle string `json:"custom_title,omitempty"`
+ IsAnonymous bool `json:"is_anonymous,omitempty"`
+ CanBeEdited bool `json:"can_be_edited,omitempty"`
+ CanManageChat bool `json:"can_manage_chat,omitempty"`
+ CanPostMessages bool `json:"can_post_messages,omitempty"`
+ CanEditMessages bool `json:"can_edit_messages,omitempty"`
+ CanDeleteMessages bool `json:"can_delete_messages,omitempty"`
+ CanManageVideoChats bool `json:"can_manage_video_chats,omitempty"`
+ CanRestrictMembers bool `json:"can_restrict_members,omitempty"`
+ CanPromoteMembers bool `json:"can_promote_members,omitempty"`
+ CanChangeInfo bool `json:"can_change_info,omitempty"`
+ CanInviteUsers bool `json:"can_invite_users,omitempty"`
+ CanPinMessages bool `json:"can_pin_messages,omitempty"`
+ IsMember bool `json:"is_member,omitempty"`
+ CanSendMessages bool `json:"can_send_messages,omitempty"`
+ CanSendAudios bool `json:"can_send_audios,omitempty"`
+ CanSendDocuments bool `json:"can_send_documents,omitempty"`
+ CanSendPhotos bool `json:"can_send_photos,omitempty"`
+ CanSendVideos bool `json:"can_send_videos,omitempty"`
+ CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"`
+ CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"`
+ CanSendPolls bool `json:"can_send_polls,omitempty"`
+ CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"`
+ CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"`
+ CanManageTopics bool `json:"can_manage_topics,omitempty"`
+ CanPostStories bool `json:"can_post_stories,omitempty"`
+ CanEditStories bool `json:"can_edit_stories,omitempty"`
+ CanDeleteStories bool `json:"can_delete_stories,omitempty"`
+ UntilDate int `json:"until_date,omitempty"`
+}
+
+// ChatMemberUpdated represents changes in the status of a chat member.
+type ChatMemberUpdated struct {
+ InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
+ Chat Chat `json:"chat"`
+ From User `json:"from"`
+ OldChatMember ChatMember `json:"old_chat_member"`
+ NewChatMember ChatMember `json:"new_chat_member"`
+ Date int `json:"date"`
+ ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"`
+ ViaJoinRequest bool `json:"via_join_request,omitempty"`
+}
+
+// ChatPermissions describes actions that a non-administrator user is allowed to take in a chat.
+type ChatPermissions struct {
+ CanSendMessages bool `json:"can_send_messages,omitempty"`
+ CanSendAudios bool `json:"can_send_audios,omitempty"`
+ CanSendDocuments bool `json:"can_send_documents,omitempty"`
+ CanSendPhotos bool `json:"can_send_photos,omitempty"`
+ CanSendVideos bool `json:"can_send_videos,omitempty"`
+ CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"`
+ CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"`
+ CanSendPolls bool `json:"can_send_polls,omitempty"`
+ CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"`
+ CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"`
+ CanChangeInfo bool `json:"can_change_info,omitempty"`
+ CanInviteUsers bool `json:"can_invite_users,omitempty"`
+ CanPinMessages bool `json:"can_pin_messages,omitempty"`
+ CanManageTopics bool `json:"can_manage_topics,omitempty"`
+}
+
+// Birthdate
+type Birthdate struct {
+ Day int `json:"day"`
+ Month int `json:"month"`
+ Year int `json:"year"`
+}
+
+// BusinessIntro
+type BusinessIntro struct {
+ Sticker *Sticker `json:"sticker,omitempty"`
+ Title string `json:"title,omitempty"`
+ Message string `json:"message,omitempty"`
+}
+
+// BusinessLocation
+type BusinessLocation struct {
+ Location *Location `json:"location,omitempty"`
+ Address string `json:"address"`
+}
+
+// BusinessOpeningHoursInterval
+type BusinessOpeningHoursInterval struct {
+ OpeningMinute int `json:"opening_minute"`
+ ClosingMinute int `json:"closing_minute"`
+}
+
+// BusinessOpeningHours
+type BusinessOpeningHours struct {
+ TimeZoneName string `json:"time_zone_name"`
+ OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"`
+}
+
+// ChatLocation represents a location to which a chat is connected.
+type ChatLocation struct {
+ Location *Location `json:"location"`
+ Address string `json:"address"`
+}
+
+// BotCommand represents a bot command.
+type BotCommand struct {
+ Command string `json:"command"`
+ Description string `json:"description"`
+}
+
+// ResponseParameters contains information about why a request was unsuccessful.
+type ResponseParameters struct {
+ MigrateToChatID int `json:"migrate_to_chat_id,omitempty"`
+ RetryAfter int `json:"retry_after,omitempty"`
+}
+
+// InputMediaType is a custom type for the various InputMedia*'s Type field.
+type InputMediaType string
+
+// These are all the possible types for the various InputMedia*'s Type field.
+const (
+ MediaTypePhoto InputMediaType = "photo"
+ MediaTypeVideo = "video"
+ MediaTypeAnimation = "animation"
+ MediaTypeAudio = "audio"
+ MediaTypeDocument = "document"
+)
+
+// InputMedia is an interface for the various media types.
+type InputMedia interface {
+ media() InputFile
+ thumbnail() InputFile
+}
+
+// GroupableInputMedia is an interface for the various groupable media types.
+type GroupableInputMedia interface {
+ InputMedia
+ groupable()
+}
+
+// mediaEnvelope is a generic struct for all the various structs under the InputMedia interface.
+type mediaEnvelope struct {
+ InputMedia
+ media string
+ thumbnail string
+}
+
+// MarshalJSON is a custom marshaler for the mediaEnvelope struct.
+func (i mediaEnvelope) MarshalJSON() (cnt []byte, err error) {
+ var tmp any
+
+ switch o := i.InputMedia.(type) {
+ case InputMediaPhoto:
+ tmp = struct {
+ Media string `json:"media"`
+ InputMediaPhoto
+ }{
+ InputMediaPhoto: o,
+ Media: i.media,
+ }
+
+ case InputMediaVideo:
+ tmp = struct {
+ Media string `json:"media"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ InputMediaVideo
+ }{
+ InputMediaVideo: o,
+ Media: i.media,
+ Thumbnail: i.thumbnail,
+ }
+
+ case InputMediaAnimation:
+ tmp = struct {
+ Media string `json:"media"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ InputMediaAnimation
+ }{
+ InputMediaAnimation: o,
+ Media: i.media,
+ Thumbnail: i.thumbnail,
+ }
+
+ case InputMediaAudio:
+ tmp = struct {
+ Media string `json:"media"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ InputMediaAudio
+ }{
+ InputMediaAudio: o,
+ Media: i.media,
+ Thumbnail: i.thumbnail,
+ }
+
+ case InputMediaDocument:
+ tmp = struct {
+ Media string `json:"media"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ InputMediaDocument
+ }{
+ InputMediaDocument: o,
+ Media: i.media,
+ Thumbnail: i.thumbnail,
+ }
+
+ case InputPaidMediaPhoto:
+ tmp = struct {
+ Media string `json:"media"`
+ InputPaidMediaPhoto
+ }{
+ InputPaidMediaPhoto: o,
+ Media: i.media,
+ }
+
+ case InputPaidMediaVideo:
+ tmp = struct {
+ Media string `json:"media"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ InputPaidMediaVideo
+ }{
+ InputPaidMediaVideo: o,
+ Media: i.media,
+ Thumbnail: i.thumbnail,
+ }
+ }
+
+ return json.Marshal(tmp)
+}
+
+// InputMediaPhoto represents a photo to be sent.
+// Type MUST BE "photo".
+type InputMediaPhoto struct {
+ Type InputMediaType `json:"type"`
+ Media InputFile `json:"-"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode ParseMode `json:"parse_mode,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ HasSpoiler bool `json:"has_spoiler,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
+func (i InputMediaPhoto) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputMedia* struct.
+func (i InputMediaPhoto) thumbnail() InputFile { return InputFile{} }
+
+// groupable is a dummy method which exists to implement the interface GroupableInputMedia.
+func (i InputMediaPhoto) groupable() {}
+
+// InputMediaVideo represents a video to be sent.
+// Type MUST BE "video".
+type InputMediaVideo struct {
+ Type InputMediaType `json:"type"`
+ Media InputFile `json:"-"`
+ Thumbnail InputFile `json:"-"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode ParseMode `json:"parse_mode,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ Width int `json:"width,omitempty"`
+ Height int `json:"height,omitempty"`
+ Duration int `json:"duration,omitempty"`
+ SupportsStreaming bool `json:"supports_streaming,omitempty"`
+ HasSpoiler bool `json:"has_spoiler,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+ Cover string `json:"cover,omitempty"`
+ StartTimestamp int `json:"start_timestamp,omitempty"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
+func (i InputMediaVideo) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputMedia* struct.
+func (i InputMediaVideo) thumbnail() InputFile { return i.Thumbnail }
+
+// groupable is a dummy method which exists to implement the interface GroupableInputMedia.
+func (i InputMediaVideo) groupable() {}
+
+// InputMediaAnimation represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
+// Type MUST BE "animation".
+type InputMediaAnimation struct {
+ Type InputMediaType `json:"type"`
+ Media InputFile `json:"-"`
+ Thumbnail InputFile `json:"-"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode ParseMode `json:"parse_mode,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ Width int `json:"width,omitempty"`
+ Height int `json:"height,omitempty"`
+ Duration int `json:"duration,omitempty"`
+ HasSpoiler bool `json:"has_spoiler,omitempty"`
+ ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
+func (i InputMediaAnimation) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputMedia* struct.
+func (i InputMediaAnimation) thumbnail() InputFile { return i.Thumbnail }
+
+// InputMediaAudio represents an audio file to be treated as music to be sent.
+// Type MUST BE "audio".
+type InputMediaAudio struct {
+ Type InputMediaType `json:"type"`
+ Performer string `json:"performer,omitempty"`
+ Title string `json:"title,omitempty"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode ParseMode `json:"parse_mode,omitempty"`
+ Media InputFile `json:"-"`
+ Thumbnail InputFile `json:"-"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ Duration int `json:"duration,omitempty"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
+func (i InputMediaAudio) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputMedia* struct.
+func (i InputMediaAudio) thumbnail() InputFile { return i.Thumbnail }
+
+// groupable is a dummy method which exists to implement the interface GroupableInputMedia.
+func (i InputMediaAudio) groupable() {}
+
+// InputMediaDocument represents a general file to be sent.
+// Type MUST BE "document".
+type InputMediaDocument struct {
+ Type InputMediaType `json:"type"`
+ Media InputFile `json:"-"`
+ Thumbnail InputFile `json:"-"`
+ Caption string `json:"caption,omitempty"`
+ ParseMode ParseMode `json:"parse_mode,omitempty"`
+ CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
+ DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
+func (i InputMediaDocument) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputMedia* struct.
+func (i InputMediaDocument) thumbnail() InputFile { return i.Thumbnail }
+
+// groupable is a dummy method which exists to implement the interface GroupableInputMedia.
+func (i InputMediaDocument) groupable() {}
+
+// InputPaidMediaType represents the various InputPaidMedia types.
+type InputPaidMediaType string
+
+// These are the various InputPaidMediaType values.
+const (
+ InputPaidMediaTypePhoto InputPaidMediaType = "photo"
+ InputPaidMediaTypeVideo = "video"
+)
+
+// InputPaidMediaPhoto represents a paid photo to send.
+type InputPaidMediaPhoto struct {
+ Type InputPaidMediaType `json:"type"`
+ Media InputFile `json:"-"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputPaidMedia* struct.
+func (i InputPaidMediaPhoto) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputPaidMedia* struct.
+func (i InputPaidMediaPhoto) thumbnail() InputFile { return InputFile{} }
+
+// groupable is a dummy method which exists to implement the interface GroupableInputMedia.
+func (i InputPaidMediaPhoto) groupable() {}
+
+// InputPaidMediaVideo represents a paid video to send.
+type InputPaidMediaVideo struct {
+ Type InputPaidMediaType `json:"type"`
+ Media InputFile `json:"-"`
+ Thumbnail InputFile `json:"-"`
+ Width int `json:"width,omitempty"`
+ Height int `json:"height,omitempty"`
+ Duration int `json:"duration,omitempty"`
+ SupportsStreaming bool `json:"supports_streaming,omitempty"`
+ Cover string `json:"cover,omitempty"`
+ StartTimestamp int `json:"start_timestamp,omitempty"`
+}
+
+// media is a method which allows to obtain the Media (type InputFile) field from the InputPaidMedia* struct.
+func (i InputPaidMediaVideo) media() InputFile { return i.Media }
+
+// thumbnail is a method which allows to obtain the Thumbnail (type InputFile) field from the InputPaidMedia* struct.
+func (i InputPaidMediaVideo) thumbnail() InputFile { return i.Thumbnail }
+
+// groupable is a dummy method which exists to implement the interface GroupableInputMedia.
+func (i InputPaidMediaVideo) groupable() {}
+
+// InputProfilePhoto represents an interface that implements all the various input profile photo types.
+type InputProfilePhoto interface {
+ file() InputFile
+ inputProfilePhoto()
+}
+
+// profilePhotoEnvelope is a generic struct for all the various structs under the InputProfilePhoto interface.
+type profilePhotoEnvelope struct {
+ InputProfilePhoto
+ ProfilePhoto string `json:"photo,omitempty"`
+ Animation string `json:"animation,omitempty"`
+}
+
+// MarshalJSON is a custom marshaler for the profilePhotoEnvelope struct.
+func (i profilePhotoEnvelope) MarshalJSON() (cnt []byte, err error) {
+ var tmp any
+
+ switch o := i.InputProfilePhoto.(type) {
+ case InputProfilePhotoStatic:
+ tmp = struct {
+ Photo string `json:"photo"`
+ InputProfilePhotoStatic
+ }{
+ InputProfilePhotoStatic: o,
+ Photo: i.ProfilePhoto,
+ }
+
+ case InputProfilePhotoAnimated:
+ tmp = struct {
+ Animation string `json:"animation"`
+ InputProfilePhotoAnimated
+ }{
+ InputProfilePhotoAnimated: o,
+ Animation: i.Animation,
+ }
+ }
+
+ return json.Marshal(tmp)
+}
+
+// InputProfilePhotoStatic describes a static profile photo to set for the bot.
+// Type must be "static".
+type InputProfilePhotoStatic struct {
+ Type string `json:"type"`
+ Photo InputFile `json:"-"`
+}
+
+func (i InputProfilePhotoStatic) file() InputFile { return i.Photo }
+
+func (i InputProfilePhotoStatic) inputProfilePhoto() {}
+
+// InputProfilePhotoAnimated describes an animated profile photo to set for the bot.
+// Type must be "animation".
+type InputProfilePhotoAnimated struct {
+ Type string `json:"type"`
+ Animation InputFile `json:"-"`
+ MainFrameTimestamp float64 `json:"main_frame_timestamp,omitempty"`
+}
+
+func (i InputProfilePhotoAnimated) file() InputFile { return i.Animation }
+
+func (i InputProfilePhotoAnimated) inputProfilePhoto() {}
+
+// BotCommandScopeType is a custom type for the various bot command scope types.
+type BotCommandScopeType string
+
+// These are all the various bot command scope types.
+const (
+ BCSTDefault BotCommandScopeType = "default"
+ BCSTAllPrivateChats = "all_private_chats"
+ BCSTAllGroupChats = "all_group_chats"
+ BCSTAllChatAdministrators = "all_chat_administrators"
+ BCSTChat = "chat"
+ BCSTChatAdministrators = "chat_administrators"
+ BCSTChatMember = "chat_member"
+)
+
+// BotCommandScope is an optional parameter used in the SetMyCommands, DeleteMyCommands and GetMyCommands methods.
+type BotCommandScope struct {
+ Type BotCommandScopeType `json:"type"`
+ ChatID int64 `json:"chat_id"`
+ UserID int64 `json:"user_id"`
+}
+
+// BotDescription represents the bot's description.
+type BotDescription struct {
+ Description string `json:"description"`
+}
+
+// BotShortDescription represents the bot's short description.
+type BotShortDescription struct {
+ ShortDescription string `json:"short_description"`
+}
+
+// BotName represents the bot's name.
+type BotName struct {
+ Name string `json:"name"`
+}
+
+// ChatJoinRequest represents a join request sent to a chat.
+type ChatJoinRequest struct {
+ InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
+ Bio string `json:"bio,omitempty"`
+ Chat Chat `json:"chat"`
+ From User `json:"user"`
+ Date int `json:"date"`
+ UserChatID int64 `json:"user_chat_id"`
+}
+
+// ChatBoostAdded represents a service message about a user boosting a chat.
+type ChatBoostAdded struct {
+ BoostCount int `json:"boost_count"`
+}
+
+// BackgroundFill describes the way a background is filled based on the selected colors.
+type BackgroundFill interface {
+ ImplementsBackgroundFill()
+}
+
+// BackgroundFillSolid is a background filled using the selected color.
+// Type MUST be "solid".
+type BackgroundFillSolid struct {
+ Type string `json:"type"`
+ Color int `json:"color"`
+}
+
+func (b BackgroundFillSolid) ImplementsBackgroundFill() {}
+
+// BackgroundFillGradient is a background with a gradient fill.
+// Type MUST be "gradient".
+type BackgroundFillGradient struct {
+ Type string `json:"type"`
+ TopColor int `json:"top_color"`
+ BottomColor int `json:"bottom_color"`
+ RotationAngle int `json:"rotation_angle"`
+}
+
+func (b BackgroundFillGradient) ImplementsBackgroundFill() {}
+
+// BackgroundFillFreeformGradient is a background with a freeform gradient that rotates after every message in the chat.
+// Type MUST be "freeform_gradient".
+type BackgroundFillFreeformGradient struct {
+ Type string `json:"type"`
+ Colors []int `json:"colors"`
+}
+
+func (b BackgroundFillFreeformGradient) ImplementsBackgroundFill() {}
+
+// BackgroundType describes the type of a background.
+type BackgroundType interface {
+ ImplementsBackgroundType()
+}
+
+// BackgroundTypeFill is a background which is automatically filled based on the selected colors.
+// Type MUST be "fill".
+type BackgroundTypeFill struct {
+ Fill BackgroundFill `json:"fill"`
+ Type string `json:"type"`
+ DarkThemeDimming int `json:"dark_theme_dimming"`
+}
+
+func (b BackgroundTypeFill) ImplementsBackgroundType() {}
+
+// BackgroundTypeWallpaper is a background which is a wallpaper in the JPEG format.
+// Type MUST be "wallpaper".
+type BackgroundTypeWallpaper struct {
+ Type string `json:"type"`
+ Document Document `json:"document"`
+ DarkThemeDimming int `json:"dark_theme_dimming"`
+ IsBlurred bool `json:"is_blurred,omitempty"`
+ IsMoving bool `json:"is_moving,omitempty"`
+}
+
+func (b BackgroundTypeWallpaper) ImplementsBackgroundType() {}
+
+// BackgroundTypePattern is a PNG or TGV (gzipped subset of SVG with MIME type “application/x-tgwallpattern”) pattern
+// to be combined with the background fill chosen by the user.
+// Type MUST be "pattern".
+type BackgroundTypePattern struct {
+ Fill BackgroundFill `json:"fill"`
+ Type string `json:"type"`
+ Document Document `json:"document"`
+ Intensity int `json:"intensity"`
+ IsInverted bool `json:"is_inverted,omitempty"`
+ IsMoving bool `json:"is_moving,omitempty"`
+}
+
+func (b BackgroundTypePattern) ImplementsBackgroundType() {}
+
+// BackgroundTypeChatTheme is taken directly from a built-in chat theme.
+// Type MUST be "chat_theme".
+type BackgroundTypeChatTheme struct {
+ Type string `json:"type"`
+ ThemeName string `json:"theme_name"`
+}
+
+func (b BackgroundTypeChatTheme) ImplementsBackgroundType() {}
+
+// ForumTopicCreated represents a service message about a new forum topic created in the chat.
+type ForumTopicCreated struct {
+ Name string `json:"name"`
+ IconCustomEmojiID string `json:"icon_custom_emoji_id"`
+ IconColor int `json:"icon_color"`
+}
+
+// ChatBackground represents a chat background.
+type ChatBackground struct {
+ Type BackgroundType `json:"type"`
+}
+
+// ForumTopicClosed represents a service message about a forum topic closed in the chat.
+type ForumTopicClosed struct{}
+
+// ForumTopicEdited represents a service message about an edited forum topic.
+type ForumTopicEdited struct {
+ Name string `json:"name"`
+ IconCustomEmojiID string `json:"icon_custom_emoji_id"`
+}
+
+// ForumTopicReopened represents a service message about a forum topic reopened in the chat.
+type ForumTopicReopened struct{}
+
+// GeneralForumTopicHidden represents a service message about General forum topic hidden in the chat.
+type GeneralForumTopicHidden struct{}
+
+// GeneralForumTopicUnhidden represents a service message about General forum topic unhidden in the chat.
+type GeneralForumTopicUnhidden struct{}
+
+// ChatOwnerLeft represents a service message about the owner of the direct messages chat leaving the chat.
+type ChatOwnerLeft struct{}
+
+// ChatOwnerChanged represents a service message about a change in the owner of the direct messages chat.
+type ChatOwnerChanged struct {
+ OldOwner User `json:"old_owner"`
+ NewOwner User `json:"new_owner"`
+}
+
+// WriteAccessAllowed represents a service message about a user allowing a bot added to the attachment menu to write messages.
+type WriteAccessAllowed struct {
+ WebAppName string `json:"web_app_name,omitempty"`
+ FromRequest bool `json:"from_request,omitempty"`
+ FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
+}
+
+// IconColor represents a forum topic icon in RGB format.
+type IconColor int
+
+// These are all the various icon colors.
+const (
+ LightBlue IconColor = 0x6FB9F0
+ Yellow = 0xFFD67E
+ Purple = 0xCB86DB
+ Green = 0x8EEE98
+ Pink = 0xFF93B2
+ Red = 0xFB6F5F
+)
+
+// ForumTopic represents a forum topic.
+type ForumTopic struct {
+ Name string `json:"name"`
+ IconCustomEmojiID string `json:"icon_custom_emoji_id"`
+ IconColor IconColor `json:"icon_color"`
+ MessageThreadID int64 `json:"message_thread_id"`
+}
+
+// UserShared contains information about the user whose identifier was shared with the bot using a KeyboardButtonRequestUser button.
+type UserShared struct {
+ RequestID int `json:"request_id"`
+ UserID int64 `json:"user_id"`
+}
+
+// ChatShared contains information about the chat whose identifier was shared with the bot using a KeyboardButtonRequestChat button.
+type ChatShared struct {
+ Photo *[]PhotoSize `json:"photo,omitempty"`
+ Title string `json:"title,omitempty"`
+ Username string `json:"username,omitempty"`
+ RequestID int `json:"request_id"`
+ ChatID int64 `json:"chat_id"`
+}
+
+// Story represents a story.
+type Story struct {
+ Chat Chat `json:"chat"`
+ ID int64 `json:"id"`
+}
+
+type ReactionType struct {
+ Type string `json:"type"`
+ Emoji string `json:"emoji"`
+ CustomEmoji string `json:"custom_emoji"`
+}
+
+// ReactionCount represents a reaction added to a message along with the number of times it was added.
+type ReactionCount struct {
+ Type ReactionType `json:"type"`
+ TotalCount int `json:"total_count"`
+}
+
+// MessageReactionUpdated represents a change of a reaction on a message performed by a user.
+type MessageReactionUpdated struct {
+ Chat Chat `json:"chat"`
+ ActorChat Chat `json:"actor_chat,omitempty"`
+ OldReaction []ReactionType `json:"old_reaction"`
+ NewReaction []ReactionType `json:"new_reaction"`
+ User User `json:"user,omitempty"`
+ MessageID int `json:"message_id"`
+ Date int `json:"date"`
+}
+
+// MessageReactionCountUpdated represents reaction changes on a message with anonymous reactions.
+type MessageReactionCountUpdated struct {
+ Reactions []ReactionCount `json:"reactions"`
+ Chat Chat `json:"chat"`
+ MessageID int `json:"message_id"`
+ Date int `json:"date"`
+}
+
+// TextQuote contains information about the quoted part of a message that is replied to by the given message.
+type TextQuote struct {
+ Entities *[]MessageEntity `json:"entities,omitempty"`
+ Text string `json:"text"`
+ Position int `json:"position"`
+ IsManual bool `json:"is_manual,omitempty"`
+}
+
+// ExternalReplyInfo contains information about a message that is being replied to, which may come from another chat or forum topic.
+type ExternalReplyInfo struct {
+ Venue Venue `json:"venue,omitempty"`
+ Chat Chat `json:"chat,omitempty"`
+ Document Document `json:"document,omitempty"`
+ PaidMedia PaidMediaInfo `json:"paid_media,omitempty"`
+ Origin MessageOrigin `json:"origin"`
+ Contact Contact `json:"contact,omitempty"`
+ Invoice Invoice `json:"invoice,omitempty"`
+ Dice Dice `json:"dice,omitempty"`
+ LinkPreviewOptions LinkPreviewOptions `json:"link_preview_options,omitempty"`
+ Photo []PhotoSize `json:"photo,omitempty"`
+ Audio Audio `json:"audio,omitempty"`
+ Story Story `json:"story,omitempty"`
+ Voice Voice `json:"voice,omitempty"`
+ VideoNote VideoNote `json:"video_note,omitempty"`
+ Game Game `json:"game,omitempty"`
+ Video Video `json:"video,omitempty"`
+ Animation Animation `json:"animation,omitempty"`
+ Sticker Sticker `json:"sticker,omitempty"`
+ Giveaway Giveaway `json:"giveaway,omitempty"`
+ Poll Poll `json:"poll,omitempty"`
+ GiveawayWinners GiveawayWinners `json:"giveaway_winners,omitempty"`
+ Location Location `json:"location,omitempty"`
+ MessageID int `json:"message_id,omitempty"`
+ HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
+}
+
+// MessageOrigin describes the origin of a message.
+type MessageOrigin struct {
+ SenderChat *Chat `json:"sender_chat,omitempty"`
+ SenderUser *User `json:"sender_user,omitempty"`
+ Type string `json:"type"`
+ SenderUserName string `json:"sender_user_name,omitempty"`
+ AuthorSignature string `json:"author_signature,omitempty"`
+ Date int `json:"date"`
+}
+
+// LinkPreviewOptions describes the options used for link preview generation.
+type LinkPreviewOptions struct {
+ URL string `json:"url,omitempty"`
+ IsDisabled bool `json:"is_disabled,omitempty"`
+ PreferSmallMedia bool `json:"prefer_small_media,omitempty"`
+ PreferLargeMedia bool `json:"prefer_large_media,omitempty"`
+ ShowAboveText bool `json:"show_above_text,omitempty"`
+}
+
+// ReplyParameters describes reply parameters for the message that is being sent.
+type ReplyParameters struct {
+ Quote string `json:"quote,omitempty"`
+ QuoteParseMode string `json:"quote_parse_mode,omitempty"`
+ QuoteEntities []MessageEntity `json:"quote_entities,omitempty"`
+ MessageID int `json:"message_id"`
+ ChatID int64 `json:"chat_id,omitempty"`
+ QuotePosition int `json:"quote_position,omitempty"`
+ AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
+}
+
+// SharedUser contains information about a user that was shared with the bot using a KeyboardButtonRequestUser button.
+type SharedUser struct {
+ Photo *[]PhotoSize `json:"photo,omitempty"`
+ FirstName string `json:"firstname,omitempty"`
+ LastName string `json:"lastname,omitempty"`
+ Username string `json:"username,omitempty"`
+ UserID int64 `json:"user_id"`
+}
+
+// UsersShared contains information about the users whose identifiers were shared with the bot using a KeyboardButtonRequestUsers button.
+type UsersShared struct {
+ Users []SharedUser `json:"users"`
+ RequestID int `json:"request_id"`
+}
+
+// ChatBoost contains information about a chat boost.
+type ChatBoost struct {
+ BoostID string `json:"boost_id"`
+ Source ChatBoostSource `json:"source"`
+ AddDate int `json:"add_date"`
+ ExpirationDate int `json:"expiration_date"`
+}
+
+// ChatBoostSourceType is a custom type for the various chat boost sources.
+type ChatBoostSourceType string
+
+// These are all the possible chat boost types.
+const (
+ ChatBoostSourcePremium ChatBoostSourceType = "premium"
+ ChatBoostSourceGiftCode = "gift_code"
+ ChatBoostSourceGiveaway = "giveaway"
+)
+
+// ChatBoostSource describes the source of a chat boost.
+type ChatBoostSource struct {
+ User *User `json:"user,omitempty"`
+ Source ChatBoostSourceType `json:"source"`
+ GiveawayMessageID int `json:"giveaway_message_id,omitempty"`
+ PrizeStarCount int `json:"prize_star_count,omitempty"`
+ IsUnclaimed bool `json:"is_unclaimed,omitempty"`
+}
+
+// ChatBoostUpdated represents a boost added to a chat or changed.
+type ChatBoostUpdated struct {
+ Chat Chat `json:"chat"`
+ Boost ChatBoost `json:"boost"`
+}
+
+// ChatBoostRemoved represents a boost removed from a chat.
+type ChatBoostRemoved struct {
+ BoostID string `json:"boost_id"`
+ Chat Chat `json:"chat"`
+ Source ChatBoostSource `json:"source"`
+ RemoveDate int `json:"remove_date"`
+}
+
+// UserChatBoosts represents a list of boosts added to a chat by a user.
+type UserChatBoosts struct {
+ Boosts []ChatBoost `json:"boosts"`
+}
+
+// BusinessConnection describes the connection of the bot with a business account.
+type BusinessConnection struct {
+ ID string `json:"id"`
+ User User `json:"user"`
+ UserChatID int64 `json:"user_chat_id"`
+ Date int64 `json:"date"`
+ CanReply bool `json:"can_reply"`
+ IsEnabled bool `json:"is_enabled"`
+}
+
+// BusinessMessagesDeleted is received when messages are deleted from a connected business account.
+type BusinessMessagesDeleted struct {
+ BusinessConnectionID string `json:"business_connection_id"`
+ MessageIDs []int `json:"message_ids"`
+ Chat Chat `json:"chat"`
+}
+
+// Giveaway represents a message about a scheduled giveaway.
+type Giveaway struct {
+ CountryCodes *[]string `json:"country_codes,omitempty"`
+ PrizeDescription string `json:"prize_description,omitempty"`
+ Chats []Chat `json:"chats"`
+ PrizeStarCount int `json:"prize_star_count,omitempty"`
+ WinnersSelectionDate int `json:"winners_selection_date"`
+ WinnerCount int `json:"winner_count"`
+ PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
+ OnlyNewMembers bool `json:"only_new_members,omitempty"`
+ HasPublicWinners bool `json:"has_public_winners,omitempty"`
+}
+
+// GiveawayCreated represents a service message about the creation of a scheduled giveaway.
+type GiveawayCreated struct {
+ PrizeStarCount int `json:"prize_star_count,omitempty"`
+}
+
+// GiveawayWinners represents a message about the completion of a giveaway with public winners.
+type GiveawayWinners struct {
+ PrizeDescription string `json:"prize_description,omitempty"`
+ Chats []Chat `json:"chats"`
+ Winners []User `json:"winners"`
+ PrizeStarCount int `json:"prize_star_count,omitempty"`
+ GiveawayMessageID int `json:"giveaway_message_id"`
+ WinnersSelectionDate int `json:"winners_selection_date"`
+ WinnerCount int `json:"winner_count"`
+ AdditionalChatCount int `json:"additional_chat_count,omitempty"`
+ PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
+ UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
+ OnlyNewMembers bool `json:"only_new_members,omitempty"`
+ WasRefunded bool `json:"was_refunded,omitempty"`
+}
+
+// GiveawayCompleted represents a service message about the completion of a giveaway without public winners.
+type GiveawayCompleted struct {
+ GiveawayMessage *Message `json:"giveaway_message,omitempty"`
+ IsStarGiveaway bool `json:"is_star_giveaway,omitempty"`
+ WinnerCount int `json:"winner_count"`
+ UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
+}
+
+// Gift represents a gift that can be sent by the bot.
+type Gift struct {
+ ID string `json:"id"`
+ Sticker Sticker `json:"sticker"`
+ StarCount int `json:"star_count"`
+ UpgradeStarCount int `json:"upgrade_star_count,omitempty"`
+ TotalCount int `json:"total_count,omitempty"`
+ RemainingCount int `json:"remaining_count,omitempty"`
+}
+
+// Gifts represents a list of gifts.
+type Gifts struct {
+ Gifts []Gift `json:"gifts"`
+}
+
+// UniqueGiftBackdropColors describes colors of the backdrop of a unique gift.
+type UniqueGiftBackdropColors struct {
+ CenterColor int `json:"center_color"`
+ EdgeColor int `json:"edge_color"`
+ SymbolColor int `json:"symbol_color"`
+ TextColor int `json:"text_color"`
+}
+
+// UniqueGiftBackdrop describes the backdrop of a unique gift.
+type UniqueGiftBackdrop struct {
+ Name string `json:"name"`
+ Colors UniqueGiftBackdropColors `json:"colors"`
+ Rarity int `json:"rarity"`
+}
+
+// UniqueGiftSymbol describes a symbol of a unique gift.
+type UniqueGiftSymbol struct {
+ Name string `json:"name"`
+ Sticker Sticker `json:"sticker"`
+ Rarity int `json:"rarity"`
+}
+
+// UniqueGiftModel describes a model of a unique gift.
+type UniqueGiftModel struct {
+ Name string `json:"name"`
+ Sticker Sticker `json:"sticker"`
+ Number int `json:"number"`
+ Rarity int `json:"rarity"`
+ Model Sticker `json:"model"`
+ Symbol Sticker `json:"symbol"`
+ Backdrop UniqueGiftBackdrop `json:"backdrop"`
+}
+
+// UniqueGift describes an upgraded gift with unique characteristics.
+type UniqueGift struct {
+ Model UniqueGiftModel `json:"model"`
+ Symbol UniqueGiftSymbol `json:"symbol"`
+ Backdrop UniqueGiftBackdrop `json:"backdrop"`
+ PublisherChat *Chat `json:"publisher_chat,omitempty"`
+ OwnerChat *Chat `json:"owner_chat,omitempty"`
+ SellerBot *User `json:"seller_bot,omitempty"`
+ BaseName string `json:"base_name"`
+ Name string `json:"name"`
+ OwnerName string `json:"owner_name,omitempty"`
+ Text string `json:"text,omitempty"`
+ Entities []MessageEntity `json:"entities,omitempty"`
+ Number int `json:"number"`
+ LastResaleStarCount int `json:"last_resale_star_count,omitempty"`
+ SellStarCount int `json:"sell_star_count,omitempty"`
+ TransferStarCount int `json:"transfer_star_count,omitempty"`
+ NextTransferDate int `json:"next_transfer_date,omitempty"`
+ CanBeTransferred bool `json:"can_be_transferred,omitempty"`
+ WasTransferred bool `json:"was_transferred,omitempty"`
+ CanBeUpgraded bool `json:"can_be_upgraded,omitempty"`
+ HasBeenUpgraded bool `json:"has_been_upgraded,omitempty"`
+ IsResellable bool `json:"is_resellable,omitempty"`
+ IsDisplayed bool `json:"is_displayed,omitempty"`
+ IsPublic bool `json:"is_public,omitempty"`
+ IsLimited bool `json:"is_limited,omitempty"`
+ IsSoldOut bool `json:"is_sold_out,omitempty"`
+ IsPermanent bool `json:"is_permanent,omitempty"`
+ IsBanned bool `json:"is_banned,omitempty"`
+ IsBurned bool `json:"is_burned,omitempty"`
+}
diff --git a/shared/echotron/types_test.go b/shared/echotron/types_test.go
new file mode 100644
index 0000000..cefaedc
--- /dev/null
+++ b/shared/echotron/types_test.go
@@ -0,0 +1,248 @@
+package echotron
+
+import "testing"
+
+func TestAPIResponseBase(_ *testing.T) {
+ a := APIResponseBase{}
+ a.Base()
+}
+
+func TestAPIResponseUpdate(_ *testing.T) {
+ a := APIResponseUpdate{}
+ a.Base()
+}
+
+func TestAPIResponseUser(_ *testing.T) {
+ a := APIResponseUser{}
+ a.Base()
+}
+
+func TestAPIResponseMessage(_ *testing.T) {
+ a := APIResponseMessage{}
+ a.Base()
+}
+
+func TestAPIResponseMessageArray(_ *testing.T) {
+ a := APIResponseMessageArray{}
+ a.Base()
+}
+
+func TestAPIResponseMessageID(_ *testing.T) {
+ a := APIResponseMessageID{}
+ a.Base()
+}
+
+func TestAPIResponseCommands(_ *testing.T) {
+ a := APIResponseCommands{}
+ a.Base()
+}
+
+func TestAPIResponseBool(_ *testing.T) {
+ a := APIResponseBool{}
+ a.Base()
+}
+
+func TestAPIResponseString(_ *testing.T) {
+ a := APIResponseString{}
+ a.Base()
+}
+
+func TestAPIResponseChat(_ *testing.T) {
+ a := APIResponseChat{}
+ a.Base()
+}
+
+func TestAPIResponseInviteLink(_ *testing.T) {
+ a := APIResponseInviteLink{}
+ a.Base()
+}
+
+func TestAPIResponseStickerSet(_ *testing.T) {
+ a := APIResponseStickerSet{}
+ a.Base()
+}
+
+func TestAPIResponseUserProfile(_ *testing.T) {
+ a := APIResponseUserProfile{}
+ a.Base()
+}
+
+func TestAPIResponseUserProfileAudios(_ *testing.T) {
+ a := APIResponseUserProfileAudios{}
+ a.Base()
+}
+
+func TestAPIResponseFile(_ *testing.T) {
+ a := APIResponseFile{}
+ a.Base()
+}
+
+func TestAPIResponseAdministrators(_ *testing.T) {
+ a := APIResponseAdministrators{}
+ a.Base()
+}
+
+func TestAPIResponseChatMember(_ *testing.T) {
+ a := APIResponseChatMember{}
+ a.Base()
+}
+
+func TestAPIResponseInteger(_ *testing.T) {
+ a := APIResponseInteger{}
+ a.Base()
+}
+
+func TestAPIResponsePoll(_ *testing.T) {
+ a := APIResponsePoll{}
+ a.Base()
+}
+
+func TestAPIResponseGameHighScore(_ *testing.T) {
+ a := APIResponseGameHighScore{}
+ a.Base()
+}
+
+func TestAPIResponseWebhook(_ *testing.T) {
+ a := APIResponseWebhook{}
+ a.Base()
+}
+
+func TestAPIResponseSentWebAppMessage(_ *testing.T) {
+ a := APIResponseSentWebAppMessage{}
+ a.Base()
+}
+
+func TestAPIResponseMenuButton(_ *testing.T) {
+ a := APIResponseMenuButton{}
+ a.Base()
+}
+
+func TestAPIResponseChatAdministratorRights(_ *testing.T) {
+ a := APIResponseChatAdministratorRights{}
+ a.Base()
+}
+
+func TestAPIResponseBotDescription(_ *testing.T) {
+ a := APIResponseBotDescription{}
+ a.Base()
+}
+
+func TestAPIResponseBotShortDescription(_ *testing.T) {
+ a := APIResponseBotShortDescription{}
+ a.Base()
+}
+
+func TestAPIResponseBusinessConnection(_ *testing.T) {
+ a := APIResponseBusinessConnection{}
+ a.Base()
+}
+
+func TestAPIResponseStarTransactions(_ *testing.T) {
+ a := APIResponseStarTransactions{}
+ a.Base()
+}
+
+func TestAPIResponsePreparedInlineMessage(_ *testing.T) {
+ a := APIResponsePreparedInlineMessage{}
+ a.Base()
+}
+
+func TestAPIResponseGifts(_ *testing.T) {
+ a := APIResponseGifts{}
+ a.Base()
+}
+
+func TestInputMediaPhoto(_ *testing.T) {
+ i := InputMediaPhoto{}
+ i.media()
+ i.thumbnail()
+ i.groupable()
+}
+
+func TestInputMediaVideo(_ *testing.T) {
+ i := InputMediaVideo{}
+ i.media()
+ i.thumbnail()
+ i.groupable()
+}
+
+func TestInputMediaAnimation(_ *testing.T) {
+ i := InputMediaAnimation{}
+ i.media()
+ i.thumbnail()
+}
+
+func TestInputMediaAudio(_ *testing.T) {
+ i := InputMediaAudio{}
+ i.media()
+ i.thumbnail()
+ i.groupable()
+}
+
+func TestInputMediaDocument(_ *testing.T) {
+ i := InputMediaDocument{}
+ i.media()
+ i.thumbnail()
+ i.groupable()
+}
+
+func TestInputPaidMediaPhoto(_ *testing.T) {
+ i := InputPaidMediaPhoto{}
+ i.media()
+ i.groupable()
+ i.thumbnail()
+}
+
+func TestInputPaidMediaVideo(_ *testing.T) {
+ i := InputPaidMediaVideo{}
+ i.media()
+ i.groupable()
+ i.thumbnail()
+}
+
+func TestInputProfilePhotoStatic(_ *testing.T) {
+ i := InputProfilePhotoStatic{}
+ i.file()
+ i.inputProfilePhoto()
+}
+
+func TestInputProfilePhotoAnimated(_ *testing.T) {
+ i := InputProfilePhotoAnimated{}
+ i.file()
+ i.inputProfilePhoto()
+}
+
+func TestBackgroundFillSolid(_ *testing.T) {
+ b := BackgroundFillSolid{}
+ b.ImplementsBackgroundFill()
+}
+
+func TestBackgroundFillGradient(_ *testing.T) {
+ b := BackgroundFillGradient{}
+ b.ImplementsBackgroundFill()
+}
+
+func TestBackgroundFillFreeformGradient(_ *testing.T) {
+ b := BackgroundFillFreeformGradient{}
+ b.ImplementsBackgroundFill()
+}
+
+func TestBackgroundTypeFill(_ *testing.T) {
+ b := BackgroundTypeFill{}
+ b.ImplementsBackgroundType()
+}
+
+func TestBackgroundTypeWallpaper(_ *testing.T) {
+ b := BackgroundTypeWallpaper{}
+ b.ImplementsBackgroundType()
+}
+
+func TestBackgroundTypePattern(_ *testing.T) {
+ b := BackgroundTypePattern{}
+ b.ImplementsBackgroundType()
+}
+
+func TestBackgroundTypeChatTheme(_ *testing.T) {
+ b := BackgroundTypeChatTheme{}
+ b.ImplementsBackgroundType()
+}
diff --git a/shared/echotron/webapp.go b/shared/echotron/webapp.go
new file mode 100644
index 0000000..8b978d3
--- /dev/null
+++ b/shared/echotron/webapp.go
@@ -0,0 +1,57 @@
+/*
+ * Echotron
+ * Copyright (C) 2022 The Echotron Contributors
+ *
+ * Echotron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Echotron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package echotron
+
+import (
+ "encoding/json"
+ "net/url"
+)
+
+// WebAppInfo contains information about a Web App.
+type WebAppInfo struct {
+ URL string `json:"url"`
+}
+
+// SentWebAppMessage contains information about an inline message sent
+// by a Web App on behalf of a user.
+type SentWebAppMessage struct {
+ InlineMessageID string `json:"inline_message_id,omitempty"`
+}
+
+// WebAppData contains data sent from a Web App to the bot.
+type WebAppData struct {
+ Data string `json:"data"`
+ ButtonText string `json:"button_text"`
+}
+
+// AnswerWebAppQuery is used to set the result of an interaction with a Web App
+// and send a corresponding message on behalf of the user to the chat from which
+// the query originated.
+func (a API) AnswerWebAppQuery(webAppQueryID string, result InlineQueryResult) (res APIResponseSentWebAppMessage, err error) {
+ var vals = make(url.Values)
+
+ resultJson, err := json.Marshal(result)
+ if err != nil {
+ return res, err
+ }
+
+ vals.Set("web_app_query_id", webAppQueryID)
+ vals.Set("result", string(resultJson))
+ return res, client.get(a.base, "answerWebAppQuery", vals, &res)
+}
diff --git a/tg_bot/Dockerfile b/tg_bot/Dockerfile
index 307a1a1..8e62791 100644
--- a/tg_bot/Dockerfile
+++ b/tg_bot/Dockerfile
@@ -5,6 +5,7 @@ WORKDIR /app/tg_bot
# Modules layer
COPY tg_bot/go.mod tg_bot/go.sum ./
COPY pkg /app/pkg
+COPY shared/echotron /app/shared/echotron
RUN go mod download
# Build layer
diff --git a/tg_bot/go.mod b/tg_bot/go.mod
index ed39a4b..4610315 100644
--- a/tg_bot/go.mod
+++ b/tg_bot/go.mod
@@ -17,3 +17,5 @@ require (
)
replace github.com/TelegramExchange/pkg => ../pkg
+
+replace github.com/NicoNex/echotron/v3 => ../shared/echotron
diff --git a/tg_bot/screens/add_project.go b/tg_bot/screens/add_project.go
index a0630bc..d36482a 100644
--- a/tg_bot/screens/add_project.go
+++ b/tg_bot/screens/add_project.go
@@ -30,7 +30,7 @@ func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
addBotURL := fmt.Sprintf("https://t.me/%s?startchannel&admin=invite_users+post_messages+edit_messages+delete_messages", botUsername)
buttons := [][]echotron.InlineKeyboardButton{
- Row(URLButton("Назначить администратором", addBotURL)),
+ Row(Stylish(URLButton("Назначить администратором", addBotURL), echotron.DangerButtonStyle)),
Row(Button("← Назад", "back")),
}
diff --git a/tg_bot/screens/helpers.go b/tg_bot/screens/helpers.go
index 01cfcf0..79d8abb 100644
--- a/tg_bot/screens/helpers.go
+++ b/tg_bot/screens/helpers.go
@@ -14,6 +14,12 @@ func Button(text, data string) echotron.InlineKeyboardButton {
}
}
+func Stylish(btn echotron.InlineKeyboardButton, style echotron.ButtonStyle) echotron.InlineKeyboardButton {
+ btn.Style = style
+
+ return btn
+}
+
func URLButton(text, url string) echotron.InlineKeyboardButton {
return echotron.InlineKeyboardButton{
Text: text,
diff --git a/tg_bot/screens/purchase_optional_details.go b/tg_bot/screens/purchase_optional_details.go
index 40e511a..1a5a25f 100644
--- a/tg_bot/screens/purchase_optional_details.go
+++ b/tg_bot/screens/purchase_optional_details.go
@@ -319,8 +319,8 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
value := strings.TrimPrefix(u.CallbackQuery.Data, "type:")
// Нормализуем значение для БД
switch value {
- case "self_promo":
- s.PurchaseType = "самопиар"
+ case "mutual_pr":
+ s.PurchaseType = "взаимный пиар"
case "standard":
s.PurchaseType = "стандарт"
}
@@ -635,7 +635,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
case "type":
text += "Выберите тип закупа"
keyboard := Keyboard(
- Row(Button("Самопиар", "type:self_promo"), Button("Стандарт", "type:standard")),
+ Row(Button("Взаимный пиар", "type:mutual_pr"), Button("Стандарт", "type:standard")),
Row(Button("← Назад", "back_to_optional")),
)
if mode == bot.EditMessage {
@@ -2363,8 +2363,8 @@ func (s *PurchaseOptionalDetails) normalizePurchaseType() (*string, bool) {
normalized := strings.ToLower(raw)
normalized = strings.TrimSpace(strings.Trim(normalized, "."))
switch normalized {
- case "самопиар", "само пиар", "self_promo", "self promo", "вп", "vp":
- value := "self_promo"
+ case "взаимный пиар", "взаимнопиар", "mutual_pr", "mutual pr", "вп", "vp":
+ value := "mutual_pr"
return &value, true
case "стандарт", "standard":
value := "standard"