кастомный echotron + fix текста
20
shared/echotron/.github/workflows/build.yml
vendored
Normal file
@@ -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
|
||||||
25
shared/echotron/.github/workflows/test.yml
vendored
Normal file
@@ -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 }}
|
||||||
165
shared/echotron/COPYING.LESSER
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
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.
|
||||||
674
shared/echotron/LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
339
shared/echotron/README.md
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [](https://golang.org/) [](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [](https://codecov.io/gh/NicoNex/echotron) [](https://github.com/NicoNex/echotron/blob/master/LICENSE) [](https://github.com/avelino/awesome-go) [](https://t.me/echotronnews) |
|
||||||
|
| :------: |
|
||||||
|
|
||||||
|
**Echotron** is an elegant and concurrent library for the Telegram bot API in Go.
|
||||||
|
|
||||||
|
Fetch with
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/NicoNex/echotron/v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
### Simplest implementations
|
||||||
|
#### Long polling
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
api := echotron.NewAPI(token)
|
||||||
|
|
||||||
|
for u := range echotron.PollingUpdates(token) {
|
||||||
|
if u.Message.Text == "/start" {
|
||||||
|
api.SendMessage("Hello world", u.ChatID(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
#### Webhook
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
api := echotron.NewAPI(token)
|
||||||
|
|
||||||
|
for u := range echotron.WebhookUpdates("https://example.com:443/my_token", token) {
|
||||||
|
if u.Message.Text == "/start" {
|
||||||
|
api.SendMessage("Hello world", u.ChatID(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
For more scalable and recommended implementations see the other examples.
|
||||||
|
|
||||||
|
### Long Polling
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Struct useful for managing internal states in your bot, but it could be of
|
||||||
|
// any type such as `type bot int64` if you only need to store the chatID.
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
// This function needs to be of type 'echotron.NewBotFn' and is called by
|
||||||
|
// the echotron dispatcher upon any new message from a chatID that has never
|
||||||
|
// interacted with the bot before.
|
||||||
|
// This means that echotron keeps one instance of the echotron.Bot implementation
|
||||||
|
// for each chat where the bot is used.
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
return &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is needed to implement the echotron.Bot interface.
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// This is the entry point of echotron library.
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
for {
|
||||||
|
log.Println(dsp.Poll())
|
||||||
|
// In case of connection issues wait 5 seconds before trying to reconnect.
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
**Echotron** makes a new instance of the struct bot for each open chat with a Telegram user, channel or group.
|
||||||
|
This allows to:
|
||||||
|
- safely call the `Update(*echotron.Update)` method concurrently
|
||||||
|
- give to the user a convenient way to manage the bot internal states across all the chats
|
||||||
|
- make sure that, even if one instance of the bot is deadlocked, the other ones keep running just fine, making the bot work for other users without any issues and/or slowdowns.
|
||||||
|
|
||||||
|
Please note that the the aforementioned behaviour is dictated by the `echotron.Dispatcher` object whose usage is not mandatory and for special needs can be ignored and implemented in different ways still keeping all the methods in the `echotron.API` object.
|
||||||
|
|
||||||
|
**Echotron** is designed to be as similar to the official [Telegram API](https://core.telegram.org/bots/api) as possible, but there are some things to take into account before starting to work with this library.
|
||||||
|
|
||||||
|
- The methods have the exact same name, but with a capital first letter, since in Go methods have to start with a capital letter to be exported.
|
||||||
|
_Example: `sendMessage` becomes `SendMessage`_
|
||||||
|
- The order of the parameters in some methods is different than in the official Telegram API, so refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for the correct one.
|
||||||
|
- The only `chat_id` (or, in this case, `chatID`) type supported is `int64`, instead of the "Integer or String" requirement of the official API. That's because numeric IDs can't change in any way, which isn't the case with text-based usernames.
|
||||||
|
- In some methods, you might find a `InputFile` type parameter. [`InputFile`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#InputFile) is a struct with unexported fields, since only three combination of fields are valid, which can be obtained through the methods [`NewInputFileID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileID), [`NewInputFilePath`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFilePath) and [`NewInputFileBytes`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileBytes).
|
||||||
|
- In some methods, you might find a `MessageIDOptions` type parameter. [`MessageIDOptions`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#MessageIDOptions) is another struct with unexported fields, since only two combination of field are valid, which can be obtained through the methods [`NewMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewMessageID) and [`NewInlineMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInlineMessageID).
|
||||||
|
- Optional parameters can be added by passing the correct struct to each method that might request optional parameters. If you don't want to pass any optional parameter, `nil` is more than enough. Refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) to check for each method's optional parameters struct: it's the type of the `opts` parameter.
|
||||||
|
- Some parameters are hardcoded to avoid putting random stuff which isn't recognized by the Telegram API. Some notable examples are [`ParseMode`](https://github.com/NicoNex/echotron/blob/master/options.go#L21), [`ChatAction`](https://github.com/NicoNex/echotron/blob/master/options.go#L54) and [`InlineQueryType`](https://github.com/NicoNex/echotron/blob/master/inline.go#L27). For a full list of custom hardcoded parameters, refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for each custom type: by clicking on the type's name, you'll get the source which contains the possible values for that type.
|
||||||
|
|
||||||
|
## Additional examples
|
||||||
|
### Functional approach to state management
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recursive type definition of the bot state function.
|
||||||
|
type stateFn func(*echotron.Update) stateFn
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
state stateFn
|
||||||
|
name string
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
bot := &bot{
|
||||||
|
chatID: chatID,
|
||||||
|
API: echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
// We set the default state to the bot.handleMessage method.
|
||||||
|
bot.state = bot.handleMessage
|
||||||
|
return bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
// Here we execute the current state and set the next one.
|
||||||
|
b.state = b.state(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) handleMessage(update *echotron.Update) stateFn {
|
||||||
|
if strings.HasPrefix(update.Message.Text, "/set_name") {
|
||||||
|
b.SendMessage("Send me my new name!", b.chatID, nil)
|
||||||
|
// Here we return b.handleName since next time we receive a message it
|
||||||
|
// will be the new name.
|
||||||
|
return b.handleName
|
||||||
|
}
|
||||||
|
return b.handleMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) handleName(update *echotron.Update) stateFn {
|
||||||
|
b.name = update.Message.Text
|
||||||
|
b.SendMessage(fmt.Sprintf("My new name is %q", b.name), b.chatID, nil)
|
||||||
|
// Here we return b.handleMessage since the next time we receive a message
|
||||||
|
// it will be handled in the default way.
|
||||||
|
return b.handleMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
log.Println(dsp.Poll())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Self destruction for lower memory footprint
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
var dsp *echotron.Dispatcher
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
bot := &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
go bot.selfDestruct(time.After(time.Hour))
|
||||||
|
return bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) selfDestruct(timech <-chan time.Time) {
|
||||||
|
<-timech
|
||||||
|
b.SendMessage("goodbye", b.chatID, nil)
|
||||||
|
dsp.DelSession(b.chatID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsp = echotron.NewDispatcher(token, newBot)
|
||||||
|
log.Println(dsp.Poll())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
return &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
dsp.ListenWebhook("https://example.com:443/my_bot_token")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Webhook with a custom http.Server
|
||||||
|
|
||||||
|
This is an example for a custom http.Server which handles your own specified routes
|
||||||
|
and also the webhook route which is specified by ListenWebhook.
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bot struct {
|
||||||
|
chatID int64
|
||||||
|
echotron.API
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "MY TELEGRAM TOKEN"
|
||||||
|
|
||||||
|
func newBot(chatID int64) echotron.Bot {
|
||||||
|
return &bot{
|
||||||
|
chatID,
|
||||||
|
echotron.NewAPI(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) Update(update *echotron.Update) {
|
||||||
|
if update.Message.Text == "/start" {
|
||||||
|
b.SendMessage("Hello world", b.chatID, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
termChan := make(chan os.Signal, 1) // Channel for terminating the app via os.Interrupt signal
|
||||||
|
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle user login
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle user logout
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Tell something about your awesome telegram bot
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set custom http.Server
|
||||||
|
server := &http.Server{Addr: ":8080", Handler: mux}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-termChan
|
||||||
|
// Perform some cleanup..
|
||||||
|
if err := server.Shutdown(context.Background()); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Capture the interrupt signal for app termination handling
|
||||||
|
dsp := echotron.NewDispatcher(token, newBot)
|
||||||
|
dsp.SetHTTPServer(server)
|
||||||
|
// Start your custom http.Server with a registered /my_bot_token handler.
|
||||||
|
log.Println(dsp.ListenWebhook("https://example.com/my_bot_token"))
|
||||||
|
}
|
||||||
|
```
|
||||||
1097
shared/echotron/api.go
Normal file
2174
shared/echotron/api_test.go
Normal file
42
shared/echotron/apierror.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
17
shared/echotron/apierror_test.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
BIN
shared/echotron/assets/banner.jpg
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
shared/echotron/assets/logo.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
shared/echotron/assets/readme_banner.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
shared/echotron/assets/tests/animation.mp4
Normal file
BIN
shared/echotron/assets/tests/audio.mp3
Normal file
BIN
shared/echotron/assets/tests/audio_inv.mp3
Normal file
BIN
shared/echotron/assets/tests/document.pdf
Normal file
BIN
shared/echotron/assets/tests/echotron_sticker.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
shared/echotron/assets/tests/echotron_test.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
shared/echotron/assets/tests/echotron_thumb.jpg
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
shared/echotron/assets/tests/echotron_thumb.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
shared/echotron/assets/tests/echotron_thumb_inv.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
shared/echotron/assets/tests/video.webm
Normal file
BIN
shared/echotron/assets/tests/video_note.mp4
Normal file
64
shared/echotron/chatadminrights.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
48
shared/echotron/chatadminrights_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
226
shared/echotron/dispatcher.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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: '<hostname>:<port>/<path>',
|
||||||
|
// eg: 'https://example.com:443/bot_token'.
|
||||||
|
// ListenWebhook will then proceed to communicate the webhook url '<hostname>/<path>' to Telegram
|
||||||
|
// and run a webserver that listens to ':<port>' 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
175
shared/echotron/dispatcher_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
73
shared/echotron/games.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
74
shared/echotron/games_test.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
shared/echotron/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/NicoNex/echotron/v3
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require golang.org/x/time v0.5.0
|
||||||
2
shared/echotron/go.sum
Normal file
@@ -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=
|
||||||
164
shared/echotron/helpers.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
601
shared/echotron/inline.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
128
shared/echotron/inline_test.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
57
shared/echotron/menubutton.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
38
shared/echotron/menubutton_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
437
shared/echotron/network.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
861
shared/echotron/options.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
57
shared/echotron/options_test.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
228
shared/echotron/passport.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
48
shared/echotron/passport_test.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
335
shared/echotron/payments.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
87
shared/echotron/payments_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
88
shared/echotron/querybuilder.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
72
shared/echotron/querybuilder_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
137
shared/echotron/simpledsp.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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: '<hostname>:<port>/<path>',
|
||||||
|
// eg: 'https://example.com:443/bot_token'.
|
||||||
|
// WebhookUpdatesOptions will then proceed to communicate the webhook url '<hostname>/<path>'
|
||||||
|
// to Telegram and run a webserver that listens to ':<port>' 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
|
||||||
|
}
|
||||||
7
shared/echotron/simpledsp_test.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package echotron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPollingUpdates(t *testing.T) {
|
||||||
|
PollingUpdates(api.token)
|
||||||
|
}
|
||||||
271
shared/echotron/stickers.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
229
shared/echotron/stickers_test.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
1986
shared/echotron/types.go
Normal file
248
shared/echotron/types_test.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
57
shared/echotron/webapp.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ WORKDIR /app/tg_bot
|
|||||||
# Modules layer
|
# Modules layer
|
||||||
COPY tg_bot/go.mod tg_bot/go.sum ./
|
COPY tg_bot/go.mod tg_bot/go.sum ./
|
||||||
COPY pkg /app/pkg
|
COPY pkg /app/pkg
|
||||||
|
COPY shared/echotron /app/shared/echotron
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Build layer
|
# Build layer
|
||||||
|
|||||||
@@ -17,3 +17,5 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/TelegramExchange/pkg => ../pkg
|
replace github.com/TelegramExchange/pkg => ../pkg
|
||||||
|
|
||||||
|
replace github.com/NicoNex/echotron/v3 => ../shared/echotron
|
||||||
|
|||||||
@@ -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)
|
addBotURL := fmt.Sprintf("https://t.me/%s?startchannel&admin=invite_users+post_messages+edit_messages+delete_messages", botUsername)
|
||||||
|
|
||||||
buttons := [][]echotron.InlineKeyboardButton{
|
buttons := [][]echotron.InlineKeyboardButton{
|
||||||
Row(URLButton("Назначить администратором", addBotURL)),
|
Row(Stylish(URLButton("Назначить администратором", addBotURL), echotron.DangerButtonStyle)),
|
||||||
Row(Button("← Назад", "back")),
|
Row(Button("← Назад", "back")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
func URLButton(text, url string) echotron.InlineKeyboardButton {
|
||||||
return echotron.InlineKeyboardButton{
|
return echotron.InlineKeyboardButton{
|
||||||
Text: text,
|
Text: text,
|
||||||
|
|||||||
@@ -319,8 +319,8 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
|||||||
value := strings.TrimPrefix(u.CallbackQuery.Data, "type:")
|
value := strings.TrimPrefix(u.CallbackQuery.Data, "type:")
|
||||||
// Нормализуем значение для БД
|
// Нормализуем значение для БД
|
||||||
switch value {
|
switch value {
|
||||||
case "self_promo":
|
case "mutual_pr":
|
||||||
s.PurchaseType = "самопиар"
|
s.PurchaseType = "взаимный пиар"
|
||||||
case "standard":
|
case "standard":
|
||||||
s.PurchaseType = "стандарт"
|
s.PurchaseType = "стандарт"
|
||||||
}
|
}
|
||||||
@@ -635,7 +635,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
|
|||||||
case "type":
|
case "type":
|
||||||
text += "<b>Выберите тип закупа</b>"
|
text += "<b>Выберите тип закупа</b>"
|
||||||
keyboard := Keyboard(
|
keyboard := Keyboard(
|
||||||
Row(Button("Самопиар", "type:self_promo"), Button("Стандарт", "type:standard")),
|
Row(Button("Взаимный пиар", "type:mutual_pr"), Button("Стандарт", "type:standard")),
|
||||||
Row(Button("← Назад", "back_to_optional")),
|
Row(Button("← Назад", "back_to_optional")),
|
||||||
)
|
)
|
||||||
if mode == bot.EditMessage {
|
if mode == bot.EditMessage {
|
||||||
@@ -2363,8 +2363,8 @@ func (s *PurchaseOptionalDetails) normalizePurchaseType() (*string, bool) {
|
|||||||
normalized := strings.ToLower(raw)
|
normalized := strings.ToLower(raw)
|
||||||
normalized = strings.TrimSpace(strings.Trim(normalized, "."))
|
normalized = strings.TrimSpace(strings.Trim(normalized, "."))
|
||||||
switch normalized {
|
switch normalized {
|
||||||
case "самопиар", "само пиар", "self_promo", "self promo", "вп", "vp":
|
case "взаимный пиар", "взаимнопиар", "mutual_pr", "mutual pr", "вп", "vp":
|
||||||
value := "self_promo"
|
value := "mutual_pr"
|
||||||
return &value, true
|
return &value, true
|
||||||
case "стандарт", "standard":
|
case "стандарт", "standard":
|
||||||
value := "standard"
|
value := "standard"
|
||||||
|
|||||||