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 @@ +| logo

[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) [![PkgGoDev](https://pkg.go.dev/badge/github.com/NicoNex/echotron/v3)](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/NicoNex/echotron/v3)](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [![codecov](https://codecov.io/gh/NicoNex/echotron/graph/badge.svg?token=LVJGOEYL5M)](https://codecov.io/gh/NicoNex/echotron) [![License](http://img.shields.io/badge/license-LGPL3.0-orange.svg?style=flat)](https://github.com/NicoNex/echotron/blob/master/LICENSE) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![Telegram](https://img.shields.io/badge/Echotron%20News-blue?logo=telegram&style=flat)](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"