diff --git a/.env.dist b/.env.dist
new file mode 100644
index 0000000..09e8e5b
--- /dev/null
+++ b/.env.dist
@@ -0,0 +1,12 @@
+bot_token = ""
+
+api_id = ""
+api_hash = ""
+
+group_id = ""
+log_group_id = ""
+
+vt_api = ""
+
+db_url = "sqlite://db.db"
+telegram_api_server = "127.0.0.1:5326"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d38e195
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,630 @@
+Copyright (C) 2021 Hack4th5tR
+
+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.
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/app.py b/app.py
new file mode 100755
index 0000000..a3afe68
--- /dev/null
+++ b/app.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+import logging
+from aiogram import executor
+from database import models
+
+from load import dp, bot
+from load import loop,tgc
+
+import handlers
+import config
+
+
+logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
+
+WEBAPP_HOST = '127.0.0.1'
+WEBAPP_PORT = 3001
+
+# Don`t touch anything!
+WEBHOOK_HOST = f'http://{WEBAPP_HOST}:{WEBAPP_PORT}'
+WEBHOOK_PATH = f'/bot{config.token}/'
+WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
+
+async def on_startup(dp):
+ from utils.notify_start import notify_started_bot
+ await notify_started_bot(bot)
+
+ from utils.default_commands import set_default_commands
+ await set_default_commands(dp)
+
+ await bot.set_webhook(WEBHOOK_URL)
+
+ # Connect to client
+ await tgc._connect()
+
+async def on_shutdown(dp):
+ await bot.delete_webhook()
+
+ # Close Redis connection.
+ await dp.storage.close()
+ await dp.storage.wait_closed()
+
+def main() -> None:
+ models.build()
+
+ if config.use_webhook:
+ executor.start_webhook(
+ dispatcher=dp,
+ webhook_path=WEBHOOK_PATH,
+ on_startup=on_startup,
+ on_shutdown=on_shutdown,
+ loop = loop,
+ skip_updates=True,
+ host=WEBAPP_HOST,
+ port=WEBAPP_PORT,
+ )
+
+ else:
+ executor.start_polling(dp,skip_updates=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..d085c3a
--- /dev/null
+++ b/config/__init__.py
@@ -0,0 +1 @@
+from .config import *
\ No newline at end of file
diff --git a/config/config.py b/config/config.py
new file mode 100644
index 0000000..571b744
--- /dev/null
+++ b/config/config.py
@@ -0,0 +1,36 @@
+import json
+
+from aiogram import Dispatcher,Bot
+from environs import Env
+
+env = Env()
+env.read_env()
+
+use_webhook = True
+
+# bot token
+token = env.str("bot_token")
+
+group_id = env.str("group_id")
+telegram_log_chat_id = env.str("log_group_id")
+
+# Telegram Application
+api_id = env.str("api_id")
+api_hash = env.str("api_hash")
+
+# Virus Total API
+vt_api = env.str("vt_api")
+
+with open("config/roles.json","r") as jsonfile:
+ roles = json.load(jsonfile)
+
+db_url = env.str("db_url")
+
+# telegram-bot-api-service
+telegram_api_server = env.str("telegram_api_server").split(":")
+telegram_api_server = {
+ "ip":telegram_api_server[0],
+ "port":telegram_api_server[1]
+}
+
+telegram_api_server = f"http://{telegram_api_server['ip']}:{telegram_api_server['port']}"
diff --git a/config/roles.json b/config/roles.json
new file mode 100644
index 0000000..fef6d36
--- /dev/null
+++ b/config/roles.json
@@ -0,0 +1,72 @@
+{
+ "level": {
+ "owner": 3,
+ "admin": 2,
+ "helper": 1,
+ "member": 0
+ },
+ "group_permissions": {
+ "can_send_messages": true,
+ "can_send_media_messages": true,
+ "can_send_other_messages": true,
+ "can_send_polls": false,
+ "can_invite_users": false,
+ "can_change_info": false,
+ "can_add_web_page_previews": false,
+ "can_pin_messages": false
+ },
+ "roles": {
+ "owner": {
+ "ban": true,
+ "kick": true,
+ "mute": true,
+ "umute": true,
+ "warn": true,
+ "pin": true,
+ "srole": true,
+ "media": true,
+ "stickers": true,
+ "ro": true,
+ "reload": true
+ },
+ "admin": {
+ "ban": true,
+ "kick": true,
+ "mute": true,
+ "umute": true,
+ "warn": true,
+ "pin": true,
+ "srole": true,
+ "media": true,
+ "stickers": true,
+ "ro": true,
+ "reload": true
+ },
+ "helper": {
+ "ban": false,
+ "kick": true,
+ "mute": false,
+ "umute": false,
+ "warn": true,
+ "pin": false,
+ "srole": false,
+ "media": true,
+ "stickers": true,
+ "ro": false,
+ "reload": true
+ },
+ "member": {
+ "ban": false,
+ "kick": false,
+ "mute": false,
+ "umute": false,
+ "warn": false,
+ "pin": false,
+ "srole": false,
+ "media": false,
+ "stickers": false,
+ "ro": false,
+ "reload": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/database/__init__.py b/database/__init__.py
new file mode 100644
index 0000000..ef3f969
--- /dev/null
+++ b/database/__init__.py
@@ -0,0 +1 @@
+from .database import Database
diff --git a/database/database.py b/database/database.py
new file mode 100644
index 0000000..a9dbde8
--- /dev/null
+++ b/database/database.py
@@ -0,0 +1,109 @@
+from .models import Member,Restriction
+from peewee import Field
+
+class Database:
+ def check_data_exists(self, fieldname:Field, value) -> bool | None:
+ """Check if data exists in db"""
+ query = Member.select().where(fieldname == value)
+
+ if (query is None):
+ return None
+
+ return query.exists()
+
+ def register_user(self, user_id, first_name, user_name=None, role:str='member') -> bool:
+ """If the user doesn't exist, returns true. Registers a user in the db."""
+
+ if self.check_data_exists(Member.user_id,user_id):
+ return False
+
+ Member.create(
+ user_id = user_id,
+ first_name = first_name,
+ user_name = user_name,
+
+ role = role,
+
+ reports = 0,
+ )
+
+ return True
+
+ def search_single_member(self,fieldname:Field,value) -> Member | None:
+ """If the user is found, returns dataclass. Returns user info."""
+ exists = self.check_data_exists(fieldname,value)
+
+ if not (exists):
+ return None
+
+ user = Member.get(fieldname == value)
+
+ return user
+
+ def create_restriction(self, from_user_id, to_user_id, operation, reason):
+ from_admin = self.search_single_member(Member.user_id,to_user_id)
+ to_user = self.search_single_member(Member.user_id,from_user_id)
+
+ if not (from_admin) or not (to_user):
+ return None
+
+ Restriction.create(
+ operation = operation,
+
+ from_admin = from_admin,
+ to_user = to_user,
+
+ reason = reason,
+ )
+
+ def search_user_restriction(self, user_id) -> list[Restriction] | None:
+ user = Member.get(Member.user_id == user_id)
+
+ query = Restriction.select().join(Member,on=Restriction.to_user)
+
+ if (query is None):
+ return None
+
+ return query.where(Restriction.to_user == user)
+
+ def delete_user(self,user_id) -> bool:
+ """If the user exists, returns true. Deletes the user from the db."""
+
+ exists = self.check_data_exists(Member.user_id,user_id)
+
+ if not (exists):
+ return False
+
+ Member.delete().where(Member.user_id == user_id)
+
+ return True
+
+ def update_member_data(self, user_id, fieldnames:list[Field], newvalues:list) -> bool:
+ """Update member data."""
+ exists = self.check_data_exists(Member.user_id,user_id)
+
+ if (not exists):
+ return False
+
+ for i in range(len(newvalues)):
+ query = Member.update({fieldnames[i]:newvalues[i]}).where(Member.user_id == user_id)
+ if (query is None):
+ return False
+
+ return True
+
+ def change_reports(self,user_id,delete=False) -> int | None:
+ """If the user exists, returns number reports. Gives the user a warning or retrieves it."""
+ exists = self.check_data_exists(Member.user_id,user_id)
+
+ if not (exists):
+ return False
+
+ count = Member.get(Member.user_id == user_id).reports
+
+ if delete:count += 1
+ else:count -= 1
+
+ query = Member.update(reports = count).where(Member.user_id == user_id).execute()
+
+ return count
diff --git a/database/models.py b/database/models.py
new file mode 100644
index 0000000..1473546
--- /dev/null
+++ b/database/models.py
@@ -0,0 +1,39 @@
+from peewee import Model, BigIntegerField, CharField, DateField, DateTimeField, ForeignKeyField
+
+import config
+from playhouse.db_url import connect
+
+from datetime import datetime, date
+
+db = connect(config.db_url)
+
+class Member(Model):
+ user_id = BigIntegerField()
+ first_name = CharField()
+ user_name = CharField(null=True)
+ role = CharField()
+
+ join_date = DateField(default=date.today())
+
+ reports = BigIntegerField()
+
+ class Meta:
+ db_table = "members"
+ database = db
+
+class Restriction(Model):
+ restriction_id = BigIntegerField()
+ operation = CharField()
+
+ from_admin = ForeignKeyField(Member,lazy_load=False)
+ to_user = ForeignKeyField(Member,lazy_load=False)
+
+ reason = CharField(null=True)
+ date = DateTimeField(default=datetime.now)
+
+ class Meta:
+ db_table = "restrictions"
+ database = db
+
+def build() -> None:
+ db.create_tables([Member,Restriction])
diff --git a/filters/__init__.py b/filters/__init__.py
new file mode 100644
index 0000000..f9ac2c2
--- /dev/null
+++ b/filters/__init__.py
@@ -0,0 +1 @@
+from .filters import IsAdminFilter,ReplayMessageFilter,UserHasRights
diff --git a/filters/filters.py b/filters/filters.py
new file mode 100644
index 0000000..7e6603e
--- /dev/null
+++ b/filters/filters.py
@@ -0,0 +1,77 @@
+from aiogram import types
+from aiogram.dispatcher.filters import BoundFilter
+
+# from config import roles
+from database.database import Member
+
+class IsAdminFilter(BoundFilter):
+ """Check admin permission on hadler"""
+ key = 'is_admin'
+
+ def __init__(self, is_admin):
+ self.is_admin = is_admin
+
+ async def check(self, message: types.Message):
+ member = await message.bot.get_chat_member(message.chat.id, message.from_user.id)
+ result = member.is_chat_admin()
+ if not result:
+ await message.reply("🔒This command can only be used by an admin!")
+ return result
+
+class UserHasRights(BoundFilter):
+ """Check command in user rights"""
+
+ key = 'hasRights'
+
+ def __init__(self,hasRights):
+ self.hasRights = hasRights
+
+ async def check(self,message:types.Message):
+ import config
+ from load import database
+
+ roles = config.roles["roles"]
+
+ command = message.text.split()[0].lstrip("!")
+
+ user = database.search_single_member(Member.user_id,message.from_user.id)
+
+ # If data not exists,return False
+ if (user is None):
+ return False
+
+ # If role not exist,return False.
+ if not (user.role in roles.keys()):
+ return False
+
+ can_run_it = roles[user.role][command]
+
+ replied = message.reply_to_message
+
+ if (replied):
+ if (replied.from_user.id == message.from_user.id):
+ await message.answer("❌ You can't ")
+ return False
+
+ if (str(replied.from_user.id) == config.token.split(":")[0]):
+ await message.answer("You can't restrict bot.")
+ return False
+
+ if not (can_run_it):
+ await message.answer("You can't use this command.")
+ return False
+
+ return roles[user.role][command]
+
+class ReplayMessageFilter(BoundFilter):
+ """Check if message replied"""
+ key = 'replied'
+
+ def __init__(self, replied):
+ self.replied = replied
+
+ async def check(self, message: types.Message):
+ if message.reply_to_message is None:
+ await message.reply("Is command must be reply")
+ return False
+ return True
diff --git a/handlers/__init__.py b/handlers/__init__.py
new file mode 100644
index 0000000..10dfc08
--- /dev/null
+++ b/handlers/__init__.py
@@ -0,0 +1,5 @@
+from . import groups
+from . import private
+from . import channels
+from . import event
+from . import errors
diff --git a/handlers/channels/__init__.py b/handlers/channels/__init__.py
new file mode 100644
index 0000000..af238c4
--- /dev/null
+++ b/handlers/channels/__init__.py
@@ -0,0 +1 @@
+from . import channels_handler
diff --git a/handlers/channels/channels_handler.py b/handlers/channels/channels_handler.py
new file mode 100644
index 0000000..787a508
--- /dev/null
+++ b/handlers/channels/channels_handler.py
@@ -0,0 +1,6 @@
+from load import dp,types
+
+# TODO: channel post forward in chat
+@dp.channel_post_handler()
+async def channel_handler(message:types.Message):
+ print(message.text)
diff --git a/handlers/errors/__init__.py b/handlers/errors/__init__.py
new file mode 100644
index 0000000..d2aa1e3
--- /dev/null
+++ b/handlers/errors/__init__.py
@@ -0,0 +1 @@
+from . import errors_handler
diff --git a/handlers/errors/errors_handler.py b/handlers/errors/errors_handler.py
new file mode 100644
index 0000000..3095a4c
--- /dev/null
+++ b/handlers/errors/errors_handler.py
@@ -0,0 +1,23 @@
+import logging
+
+from load import dp,bot
+import config
+
+from aiogram.utils.exceptions import Unauthorized
+
+
+@dp.errors_handler()
+async def errors_handler(update, exception):
+ if (isinstance(exception,Unauthorized)):
+ logging.info(f"Unathorized:{config.token}")
+ return True
+
+ await update.message.answer("Error happaned!\nBot terminated!")
+
+ await bot.send_message(
+ config.telegram_log_chat_id,
+ f"**Bot terminated**!\nException:{exception}",
+ parse_mode="Markdown"
+ )
+
+ logging.info(f"Bot terminated!Exception:{exception}")
diff --git a/handlers/event.py b/handlers/event.py
new file mode 100644
index 0000000..d5d88b4
--- /dev/null
+++ b/handlers/event.py
@@ -0,0 +1,29 @@
+from load import dp, bot, types
+
+# TODO: fix it
+# import utils
+# import config
+# vt = utils.VirusTotalAPI(config.vt_api,True)
+# @dp.message_handler(content_types=["document"],chat_type=[types.ChatType.SUPERGROUP])
+# async def file_handler(message:types.Message):
+# file = await bot.get_file(message.document.file_id)
+#
+# await bot.send_message(
+# message.chat.id,
+# await vt.scan_file(file.file_path),
+# parse_mode="Markdown"
+# )
+
+@dp.message_handler()
+async def filter_link_shorts(message:types.Message):
+ link_shorters = open("txt/link_shorters.txt","r").read().split()
+
+ for y in link_shorters:
+ for user_message in message.text.lower().split():
+ if (y in user_message):await message.delete()
+
+# Joke
+@dp.message_handler(content_types=types.ContentType.VOICE)
+async def voice_message(message:types.Message):
+ photo = types.InputFile(path_or_bytesio="media/photo.jpg")
+ await message.answer_photo(photo)
diff --git a/handlers/groups/__init__.py b/handlers/groups/__init__.py
new file mode 100644
index 0000000..e09065e
--- /dev/null
+++ b/handlers/groups/__init__.py
@@ -0,0 +1,2 @@
+from . import admin
+from . import user
diff --git a/handlers/groups/admin.py b/handlers/groups/admin.py
new file mode 100644
index 0000000..139a69a
--- /dev/null
+++ b/handlers/groups/admin.py
@@ -0,0 +1,423 @@
+from load import bot, dp, types
+from aiogram.types.chat_permissions import ChatPermissions
+
+import config
+import utils
+
+from load import database
+from database.models import Member
+
+import re
+import json
+
+from dataclasses import dataclass
+
+
+# TODO:Automatic malware checking with VirusTotal(add skipping queue virustotal report)
+# vt = utils.VirusTotalAPI(config.vt_api,True)
+
+def getArgument(arguments:list,index:int=0) -> str | None:
+ """ Get element from a list.If element not exist return None """
+ if not (arguments):
+ return None
+ if (len(arguments) >= index):
+ return arguments[index]
+ else:
+ return None
+
+@dataclass
+class CommandArguments:
+ user:Member | None
+ arguments:list
+
+async def getCommandArgs(message:types.Message) -> CommandArguments:
+ """ Describe user data and arguments from message """
+
+ #Example:
+ #1.!command @username ... (not reply)
+ #2.!command (not_reply)
+ #3.!command ... (not reply)
+
+ arguments_list = message.text.split()[1:]
+
+ is_reply = message.reply_to_message
+
+ member = None
+ arguments = []
+
+ if (is_reply):
+ member = database.search_single_member(Member.user_id,message.reply_to_message)
+ arguments = arguments_list
+ else:
+ first_word = getArgument(arguments_list)
+ if (first_word):
+ if (first_word.isdigit()):
+ member = database.search_single_member(Member.user_id,first_word)
+
+ if (first_word[0] == "@") :
+ member = database.search_single_member(Member.user_name,first_word)
+
+ arguments = arguments_list[1:]
+ else:
+ arguments = arguments_list
+
+ if (member is None) and (first_word):
+ await message.answer(f"❌ User {first_word} not exist.")
+
+ return CommandArguments(member,arguments)
+
+def checkArg(message:str) -> bool | None:
+ """ Check if first argument in ["enable","on","true"] then return true """
+ if (not message):
+ return None
+
+ argument = message.split()[1]
+
+ on = ['enable','on','true']
+ off = ['disable','off','false']
+
+ return (argument in on) or (not argument in off)
+
+def delete_substring_from_string(string:str,substring:str) -> str:
+ string_list = string.split(substring)
+ return "".join(string_list).lstrip()
+
+# Filters:
+# is_admin=True - Check admin permission, if user is admin, continue
+# replied=True - If message is answer, continue
+
+@dp.message_handler(commands=["ban"],commands_prefix="!",hasRights=True)
+async def ban_user(message: types.Message):
+ command = await getCommandArgs(message)
+ reason = getArgument(command.arguments)
+
+ user = command.user
+ admin = message.from_user
+
+ # If can't descibe user data
+ if (user is None):
+ await message.answer((
+ "Usage:!ban @username reason=None"
+ "Reply to a message or use with a username.")
+ )
+ return
+
+ # Ban user and save (bool)
+ status = await bot.kick_chat_member(chat_id=message.chat.id, user_id=user.user_id, until_date=None)
+
+ if status:
+ await message.answer(f"User [{user.first_name}](tg://user?id={user.user_id}) has been banned.",
+ parse_mode="Markdown")
+
+ # Delete user from database
+ database.delete_user(user.user_id)
+
+ # Open restrict
+ database.create_restriction(user.user_id, admin.id, "ban", reason)
+
+@dp.message_handler(commands=["unban"],commands_prefix="!",hasRights=True)
+async def unban_user(message: types.Message):
+ command = await getCommandArgs(message)
+ user = command.user
+
+ # If can't descibe user data
+ if (user is None):
+ await message.answer((
+ "Usage:!unban @username reason=None\n"
+ "Reply to a message or use with username/id.")
+ )
+ return
+
+ # Unban user and set status (bool)
+ status = await bot.unban_chat_member(chat_id=message.chat.id, user_id=user.user_id)
+
+ # add user to database
+ database.register_user(user.user_id, user.first_name)
+
+ if status:
+ await message.answer(f"User [{user.first_name}](tg://user?id={user.user_id}) has been unbaned.",
+ parse_mode="Markdown")
+
+@dp.message_handler(commands=["kick"],commands_prefix="!",hasRights=True)
+async def kick_user(message:types.Message):
+ command = await getCommandArgs(message)
+ arguments = command.arguments
+
+ user = command.user
+ admin = message.from_user
+
+ reason = getArgument(arguments)
+
+ if (user is None):
+ await message.answer((
+ "Usage:!kick @username reason=None\n"
+ "Reply to a message or use with a username/id.")
+ )
+ return
+
+
+ status1 = await bot.kick_chat_member(chat_id=message.chat.id, user_id=user.user_id, until_date=None)
+ status2 = await bot.unban_chat_member(chat_id=message.chat.id, user_id=user.user_id)
+
+ if (status1 and status2):
+ await message.answer(f"User [{user.first_name}](tg://user?id={user.user_id}) has been kicked.",
+ parse_mode="Markdown")
+
+ database.create_restriction(user.user_id,admin.id,"kick",reason)
+
+@dp.message_handler(commands=["mute"],commands_prefix="!",hasRights=True)
+async def mute_user(message:types.Message):
+ command = await getCommandArgs(message)
+ arguments = command.arguments
+
+ user = command.user
+ admin = message.from_user
+
+ if (user is None):
+ await message.answer((
+ "Usage:!mute @username time\n"
+ "Reply to a message or use with a username/id.")
+ )
+ return
+
+ duration = re.findall(r"(\d+d|\d+h|\d+m|\d+s)",''.join(arguments))
+ duration = " ".join(duration)
+ reason = delete_substring_from_string(" ".join(arguments),duration)
+ duration_timedelta = utils.parse_timedelta(duration)
+
+ if not duration:
+ await message.answer(f"Error: \"{duration}\" — неверный формат времени. Examles: 3ч, 5м, 4h30s.")
+ return
+
+ permissions = ChatPermissions(can_send_messages=False)
+
+ status = await bot.restrict_chat_member(
+ chat_id=message.chat.id,
+ user_id=user.user_id,
+ until_date=duration_timedelta,
+ permissions=permissions
+ )
+
+ if status:
+ await message.answer(f"User **{user.first_name}** has been muted.",
+ parse_mode="Markdown")
+
+ database.create_restriction(user.user_id,admin.id,"mute",reason)
+
+@dp.message_handler(commands=["umute"],commands_prefix="!",hasRights=True)
+async def umute_user(message: types.Message):
+ # Get information
+ command = await getCommandArgs(message)
+ user = command.user
+
+ # If can't
+ if (user is None):
+ await message.answer((
+ "Usage:!unmute @username reason=None\n"
+ "Reply to a message or use with a username/id.")
+ )
+ return
+
+ # Get chat permissions
+ group_permissions = config.roles["group_permissions"]
+
+ # Set permissions
+ permissions = ChatPermissions(
+ can_send_messages= group_permissions["can_send_messages"],
+ can_send_media_messages= group_permissions["can_send_media_messages"],
+ can_send_polls= group_permissions["can_send_polls"],
+ can_send_other_messages= group_permissions["can_send_other_messages"],
+ can_add_web_page_previews= group_permissions["can_add_web_page_previews"],
+ can_change_info= group_permissions["can_change_info"],
+ can_invite_users= group_permissions["can_invite_users"],
+ can_pin_messages= group_permissions["can_pin_messages"]
+ )
+
+ # Restrict user and save
+ status = await bot.restrict_chat_member(
+ chat_id=message.chat.id,
+ user_id=user.user_id,
+ permissions=permissions
+ )
+
+ if status:
+ await message.answer(f"User [{user.first_name}](tg://user?id={user.user_id}) has been unmuted.",
+ parse_mode="Markdown")
+
+@dp.message_handler(commands=["pin"],commands_prefix="!",hasRights=True)
+async def pin_message(message:types.Message):
+ await bot.pin_chat_message(message.chat.id, message.reply_to_message.message_id)
+
+@dp.message_handler(commands=["ro"],commands_prefix="!",hasRights=True)
+async def readonly_mode(message:types.Message):
+ check = checkArg(message.text)
+
+ if (check is None):
+ await message.answer("!ro on/off alias:disable,enable,start,stop.")
+ return
+
+ # Get chat permissions
+ group_permissions = config.roles["group_permissions"]
+
+ # Set permissions
+ if (check):
+ chat_permissions = ChatPermissions(
+ can_send_messages=not check
+ )
+ else:
+ chat_permissions = ChatPermissions(
+ can_send_messages=group_permissions['can_send_messages'],
+ can_send_media_messages=group_permissions["can_send_media_messages"],
+ can_send_other_messages=group_permissions['can_send_other_messages'],
+ can_send_polls=group_permissions['can_send_polls'],
+ can_invite_users=group_permissions['can_invite_users'],
+ can_change_info=group_permissions['can_change_info'],
+ can_add_web_page_previews=group_permissions['can_add_web_page_previews'],
+ can_pin_messages=group_permissions['can_pin_messages']
+ )
+
+ status = await bot.set_chat_permissions(chat_id=message.chat.id, permissions=chat_permissions)
+
+ if (status):
+ await message.answer(f"readonly - {check}")
+
+@dp.message_handler(commands=["media"],commands_prefix="!",hasRights=True)
+async def media_content(message: types.Message):
+ check = checkArg(message.text)
+
+ if (check is None):
+ await message.answer("!media on/off alias:disable,enable,start,stop.")
+ return
+
+ # Get chat permissions
+ group_permissions = config.roles["group_permissions"]
+
+ # Set permissions
+ chat_permissions = ChatPermissions(
+ can_send_messages=group_permissions['can_send_messages'],
+ can_send_media_messages=check,
+ can_send_other_messages=group_permissions['can_send_other_messages'],
+ can_send_polls=group_permissions['can_send_polls'],
+ can_invite_users=group_permissions['can_invite_users'],
+ can_change_info=group_permissions['can_change_info'],
+ can_add_web_page_previews=group_permissions['can_add_web_page_previews'],
+ can_pin_messages=group_permissions['can_pin_messages']
+ )
+
+ # Set chat pemissions and save results
+ status = await bot.set_chat_permissions(chat_id=message.chat.id, permissions=chat_permissions)
+
+ if status:
+ await message.answer(f"media - {check}.")
+
+@dp.message_handler(commands=["stickers"],commands_prefix="!",hasRights=True)
+async def send_stickes(message: types.Message):
+ # Get arguments
+ check = checkArg(message.text)
+
+ if (check is None):
+ await message.answer("!stickers on/off alias:disable,enable,start,stop")
+ return
+
+ # Get chat permissions
+ group_permissions = config.roles["group_permissions"]
+
+ # Set permissions.
+ chat_permissions = ChatPermissions(
+ can_send_messages=group_permissions['can_send_messages'],
+ can_send_media_messages=group_permissions['can_send_media_messages'],
+ can_send_other_messages=check,
+ can_send_polls=group_permissions['can_send_polls'],
+ can_invite_users=group_permissions['can_invite_users'],
+ can_change_info=group_permissions['can_change_info'],
+ can_add_web_page_previews=group_permissions['can_add_web_page_previews'],
+ can_pin_messages=group_permissions['can_pin_messages']
+ )
+
+ # Start and save to satus (bool)
+ status = await bot.set_chat_permissions(chat_id=message.chat.id, permissions=chat_permissions)
+
+ if status:
+ await message.answer(f"stickes - {check}.")
+
+@dp.message_handler(commands=["warn"],commands_prefix="!",hasRights=True)
+async def warn_user(message: types.Message):
+ # Get information
+ command = await getCommandArgs(message)
+ reason = getArgument(command.arguments)
+
+ user = command.user
+ admin = message.from_user
+
+ if (user is None):
+ await message.answer((
+ "Usage:!warn @username reason=None\n"
+ "Reply to a message or use with username/id.")
+ )
+ return
+
+ # Add warning
+ database.change_reports(user.user_id, delete=True)
+
+ await message.answer(f"User [{user.first_name}](tg://user?id={user.user_id}) has gotten a warning.",
+ parse_mode="Markdown")
+
+ database.create_restriction(user.user_id, admin.id, "warn", reason)
+
+@dp.message_handler(commands=["reload"],commands_prefix="!")
+async def reload(message:types.Message):
+ await utils.check_user_data()
+
+ group = await bot.get_chat(message.chat.id)
+ group_permissions = dict(group["permissions"])
+
+ with open("config/roles.json","r") as jsonfile:
+ data = json.load(jsonfile)
+
+ if group_permissions.keys() != data["group_permissions"].keys():
+ await message.answer("Add some permissions to roles.json")
+ return
+
+ for permission in group_permissions.keys():
+ data["group_permissions"][permission] = group_permissions[permission]
+
+ with open("config/roles.json", "w") as jsonfile:
+ json.dump(data, jsonfile,indent=4)
+
+ await message.answer(f"✅ The synchronization was successful.")
+
+@dp.message_handler(commands=["srole"],commands_prefix="!",hasRights=True)
+async def set_role(message:types.Message):
+ command = await getCommandArgs(message)
+ new_role = getArgument(command.arguments)
+
+ roles = config.roles
+
+ user = command.user
+ admin = database.search_single_member(Member.user_id,message.from_user)
+
+ if (admin is None):
+ return
+
+ if (user is None) or (new_role is None):
+ await message.answer("""
+ !srole @username/id role(owner,admin,helper,member)
+Reply to a message or use with username.""")
+ return
+
+ if not (new_role in roles["level"].keys()):
+ await message.answer(f"Role {new_role} not exists.")
+ return
+
+ if (admin.user_id == user.user_id):
+ await message.answer("❌ You can't set role yourself.")
+ return
+
+ if (roles['level'][new_role] > roles['level'][admin.role]):
+ await message.answer("Your rank is not high enough to change roles.")
+ return
+
+ database.update_member_data(user.user_id,[Member.role],[new_role])
+
+ await message.answer(f"{new_role.capitalize()} role set for [{user.first_name}](tg://user?id={user.user_id}).",
+ parse_mode="Markdown")
diff --git a/handlers/groups/user.py b/handlers/groups/user.py
new file mode 100644
index 0000000..0ac8b53
--- /dev/null
+++ b/handlers/groups/user.py
@@ -0,0 +1,104 @@
+from load import bot, dp, types
+
+import config
+
+from load import database
+from database.models import Member
+
+
+@dp.message_handler(content_types=["new_chat_members"])
+async def welcome_message(message:types.Message):
+ # User
+ user = message.from_user
+
+ exists = database.check_data_exists(Member.user_id,user.id)
+
+ if (exists):
+ await message.answer("Спасибо что вы с нами.")
+
+ if not (exists):
+ database.register_user(user.id,user.first_name,user.username)
+ # TODO: translate it
+ await message.answer((
+ f"Привет,{user.first_name}\n"
+ "Просим ознакомится с [правилами](https://telegra.ph/Pravila-CHata-Open-Source-05-29)\n"
+ "Советы на 'хороший тон':\n"
+ "\t\t1.Формулируй свою мысль в 1-2 предложения\n"
+ "\t\t1.Не задавай [мета](nometa.xyz) вопросы\n"),
+ parse_mode="Markdown")
+
+
+ await message.delete()
+
+@dp.message_handler(commands=["leave"],chat_type=[types.ChatType.SUPERGROUP])
+async def leave_group(message:types.Message):
+ user = message.from_user
+ args = message.text.split()
+
+ # TODO: translate it too
+ if (len(args) < 1) or not ( ' '.join(args[1:]) == "I UNDERSTAND" ):
+ await message.answer("Для того чтобы покинуть чат вам нужно ввести /leave I UNDERSTANT!")
+ return
+
+ database.delete_user(user.id)
+
+ # Ban user and save (bool)
+ status = await bot.kick_chat_member(chat_id=message.chat.id,user_id=user.id,until_date=None)
+
+ if status:
+ await message.answer(f"User [{user.first_name}](tg://user?id={user.id}) has laved chat forever.",
+ parse_mode="Markdown")
+
+@dp.message_handler(commands=["start","help"],chat_type=[types.ChatType.SUPERGROUP])
+async def start_command_group(message:types.Message):
+ await message.answer((
+ f"Hi,**{message.from_user.first_name}**!\n"
+ "My commands:\n"
+ " /help , /start - read the message.\n"
+ " /me , /bio - member information (if member group)."),
+ parse_mode="Markdown"
+ )
+
+@dp.message_handler(commands=["bio","me"],chat_type=[types.ChatType.SUPERGROUP])
+async def get_information(message: types.Message):
+ user = database.search_single_member(Member.user_id,message.from_user.id)
+
+ role_level = config.roles["level"]
+
+ if (user is None):
+ await message.answer("❌Sorry,you not member group.")
+ return
+
+ await message.answer((
+ f"User:[{user.first_name}](tg://user?id={user.user_id})\n"
+ f"level:{role_level[user.role]}\n"),
+ parse_mode="Markdown"
+ )
+
+@dp.message_handler(commands=["report"],replied=True,chat_type=[types.ChatType.SUPERGROUP])
+async def report(message: types.Message):
+ args = message.text.split()
+
+ if (len(args) != 2):
+ await message.reply("Please,enter reason.")
+ return
+
+ reported_user = message.reply_to_message.from_user
+ reporter_user = message.from_user
+ reason = args[1]
+
+ # TODO: translate it
+ msg = ("Жалоба на: [{}](tg://user?id={})\nПожаловался:[{}](tg://user?id={})\nПричина: {}\n{}"
+ .format(reported_user['first_name'],
+ reported_user['id'],
+ reporter_user.first_name,
+ reporter_user.id,
+ reason,
+ message.reply_to_message.link("Link message", as_html=False)
+ ))
+
+ await bot.send_message(config.telegram_log_chat_id, msg, parse_mode="Markdown")
+
+@dp.message_handler(content_types=["left_chat_member"])
+async def event_left_chat(message:types.Message):
+ await message.delete()
diff --git a/handlers/private/__init__.py b/handlers/private/__init__.py
new file mode 100644
index 0000000..f9b61db
--- /dev/null
+++ b/handlers/private/__init__.py
@@ -0,0 +1 @@
+from . import user
diff --git a/handlers/private/user.py b/handlers/private/user.py
new file mode 100644
index 0000000..a71dfb7
--- /dev/null
+++ b/handlers/private/user.py
@@ -0,0 +1,107 @@
+from load import dp,types,database,bot
+from database.models import Member
+
+from aiogram.types import KeyboardButton,ReplyKeyboardMarkup
+from aiogram.types.reply_keyboard import ReplyKeyboardRemove
+
+import config
+from keyboards.default import menu
+
+from aiogram.types import CallbackQuery
+from aiogram.dispatcher.filters import Text
+
+from aiogram.dispatcher.storage import FSMContext
+from states.report_message import States
+
+from keyboards.inline.report_button import report_button
+from keyboards.inline.callback_data import report_callback
+
+@dp.message_handler(commands=["start","help"],chat_type=[types.ChatType.PRIVATE])
+async def start_command_private(message:types.Message):
+ await message.answer((
+ "Hello,**{message.from_user.first_name}**!\n"
+ "\t\tMy commands:\n"
+ "\t\t/help , /start - read this message.")
+ ,parse_mode="Markdown",reply_markup=menu
+ )
+
+# Keyboard
+@dp.message_handler(Text(equals=["About Us"]))
+async def about_us(message:types.Message):
+ await message.answer((
+ "Moderator bot - an open source project for managing a Telegram group.\n\n"
+ "Possibilities:\n"
+ "1. Role system\n"
+ "2. Simple commands such as !ban, !mute\n"
+ "3. Convenient sticker/photo disabling with !stickers, !media\n"
+ "4. Users can report admins.\n"
+ "5. Admins can give warnings to users.\n"
+ "\nRelease version:2.5.2\n"
+ "[Github](https://github.com/hok7z/moderator-bot)"),
+ parse_mode="Markdown"
+ )
+
+
+@dp.message_handler(Text(equals=["Check restrictions"]),state=None)
+async def check_for_restrict(message:types.Message):
+ user = message.from_user
+ restrictions = database.search_user_restriction(user_id=user.id)
+
+ if (restrictions is None):
+ await message.answer("✅No restrictions.")
+ return
+
+ for restriction in restrictions:
+ callback = report_callback.new(user_id=message.from_user.id)
+ markup = report_button("✉️ Report restriction",callback)
+
+ await message.answer(f"Restriction\n{restriction.operation}\nReason:{restriction.reason}\nDate:{restriction.date}",reply_markup=markup)
+
+ await States.state1.set()
+
+@dp.callback_query_handler(text_contains="report_restriction",state=States.state1)
+async def report_restriction(call:CallbackQuery,state:FSMContext):
+ await call.answer(cache_time=60)
+
+ # callback_data = call.data
+ # restriction_id = callback_data.split(":")[1]
+
+ markup = ReplyKeyboardMarkup(resize_keyboard=True)
+ cancel = KeyboardButton("❌ Cancel")
+ markup.add(cancel)
+
+ await state.update_data(restriction_id=restriction_id)
+
+ await call.message.answer("Please,enter your report.",reply_markup=markup)
+
+@dp.message_handler(state=States.state2)
+async def get_message_report(message: types.Message,state:FSMContext):
+ answer = message.text
+
+ if not ("Cancel" in answer):
+
+ restriction = database.search_user_restriction(message.from_user.id)
+
+ if (restriction is None):
+ return
+
+ #from_admin = restriction.from_admin
+ #to_user = restriction.to_user
+
+ reason = restriction.reason
+ if (not reason):
+ reason = "No reason"
+
+ await bot.send_message(config.telegram_log_chat_id,(
+ f"Report on restriction #{restriction_id}\n"
+ f"From admin:[{from_admin.first_name}](tg://user?id={from_admin.id})\n"
+ f"To user:[{from_admin.first_name}](tg://user?id={to_user.id})\n"
+ f"Reason:{reason}\n"
+ f"Message:{answer}"
+ ),parse_mode="Markdown")
+
+ await message.answer("Report restriction sended",reply_markup=ReplyKeyboardRemove())
+ else:
+ await message.answer("Operation cancaled",reply_markup=ReplyKeyboardRemove())
+
+ await state.finish()
diff --git a/keyboards/__init__.py b/keyboards/__init__.py
new file mode 100644
index 0000000..8abaca9
--- /dev/null
+++ b/keyboards/__init__.py
@@ -0,0 +1,2 @@
+from . import default
+from . import inline
diff --git a/keyboards/default/__init__.py b/keyboards/default/__init__.py
new file mode 100644
index 0000000..cb8d856
--- /dev/null
+++ b/keyboards/default/__init__.py
@@ -0,0 +1,2 @@
+from .menu import menu
+from .menu import cancel
diff --git a/keyboards/default/menu.py b/keyboards/default/menu.py
new file mode 100644
index 0000000..138210d
--- /dev/null
+++ b/keyboards/default/menu.py
@@ -0,0 +1,19 @@
+from aiogram.types import ReplyKeyboardMarkup,KeyboardButton
+
+menu = ReplyKeyboardMarkup(
+ resize_keyboard=True,
+ keyboard=[
+ [
+ KeyboardButton("Check restrictions"),
+ KeyboardButton("About Us"),
+ ]
+])
+
+cancel = ReplyKeyboardMarkup(
+ resize_keyboard=True,
+ keyboard=[
+ [
+ KeyboardButton("❌Cancel")
+ ]
+ ]
+)
diff --git a/keyboards/inline/__init__.py b/keyboards/inline/__init__.py
new file mode 100644
index 0000000..82b070f
--- /dev/null
+++ b/keyboards/inline/__init__.py
@@ -0,0 +1 @@
+from . import report_button
diff --git a/keyboards/inline/callback_data.py b/keyboards/inline/callback_data.py
new file mode 100644
index 0000000..d3be68d
--- /dev/null
+++ b/keyboards/inline/callback_data.py
@@ -0,0 +1,4 @@
+from aiogram.utils.callback_data import CallbackData
+
+
+report_callback = CallbackData("report_restriction","user_id")
diff --git a/keyboards/inline/report_button.py b/keyboards/inline/report_button.py
new file mode 100644
index 0000000..ebb6365
--- /dev/null
+++ b/keyboards/inline/report_button.py
@@ -0,0 +1,8 @@
+from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
+
+
+def report_button(text,callback_data):
+ markup = InlineKeyboardMarkup()
+ button = InlineKeyboardButton(text,callback_data=callback_data)
+ markup.insert(button)
+ return markup
diff --git a/load.py b/load.py
new file mode 100644
index 0000000..3a880e0
--- /dev/null
+++ b/load.py
@@ -0,0 +1,33 @@
+import asyncio
+
+from aiogram import Bot, Dispatcher
+from aiogram import types
+from aiogram.bot.api import TelegramAPIServer
+from aiogram.contrib.fsm_storage.memory import MemoryStorage
+
+import config
+import utils
+import filters
+
+from database.database import Database
+
+
+database = Database()
+
+loop = asyncio.new_event_loop()
+asyncio.set_event_loop(loop)
+
+storage = MemoryStorage()
+
+tgc = utils.TelegramClientScrapper(config.api_id, config.api_hash, token=config.token, loop = loop)
+
+bot = Bot(
+ token=config.token,
+ server=TelegramAPIServer.from_base(config.telegram_api_server)
+)
+
+dp = Dispatcher(bot, storage = storage)
+
+dp.filters_factory.bind(filters.IsAdminFilter)
+dp.filters_factory.bind(filters.ReplayMessageFilter)
+dp.filters_factory.bind(filters.UserHasRights)
diff --git a/media/photo.jpg b/media/photo.jpg
new file mode 100644
index 0000000..c907621
Binary files /dev/null and b/media/photo.jpg differ
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..c48862f
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,707 @@
+[[package]]
+name = "aiogram"
+version = "2.21"
+description = "Is a pretty simple and fully asynchronous framework for Telegram Bot API"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+aiohttp = ">=3.8.0,<3.9.0"
+Babel = ">=2.9.1,<2.10.0"
+certifi = ">=2021.10.8"
+
+[package.extras]
+fast = ["uvloop (>=0.16.0,<0.17.0)", "ujson (>=1.35)"]
+proxy = ["aiohttp-socks (>=0.5.3,<0.6.0)"]
+
+[[package]]
+name = "aiohttp"
+version = "3.8.1"
+description = "Async http client/server framework (asyncio)"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+aiosignal = ">=1.1.2"
+async-timeout = ">=4.0.0a3,<5.0"
+attrs = ">=17.3.0"
+charset-normalizer = ">=2.0,<3.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["aiodns", "brotli", "cchardet"]
+
+[[package]]
+name = "aioschedule"
+version = "0.5.2"
+description = "Job scheduling for humans."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "aiosignal"
+version = "1.2.0"
+description = "aiosignal: a list of registered asynchronous callbacks"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+
+[[package]]
+name = "async-timeout"
+version = "4.0.2"
+description = "Timeout context manager for asyncio programs"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "attrs"
+version = "22.1.0"
+description = "Classes Without Boilerplate"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
+
+[[package]]
+name = "babel"
+version = "2.9.1"
+description = "Internationalization utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pytz = ">=2015.7"
+
+[[package]]
+name = "certifi"
+version = "2022.6.15"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.1.0"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "environs"
+version = "9.5.0"
+description = "simplified environment variable parsing"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+marshmallow = ">=3.0.0"
+python-dotenv = "*"
+
+[package.extras]
+dev = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "tox"]
+django = ["dj-database-url", "dj-email-url", "django-cache-url"]
+lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"]
+tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"]
+
+[[package]]
+name = "frozenlist"
+version = "1.3.1"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "marshmallow"
+version = "3.17.0"
+description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+packaging = ">=17.0"
+
+[package.extras]
+dev = ["pytest", "pytz", "simplejson", "mypy (==0.961)", "flake8 (==4.0.1)", "flake8-bugbear (==22.6.22)", "pre-commit (>=2.4,<3.0)", "tox"]
+docs = ["sphinx (==4.5.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.8)"]
+lint = ["mypy (==0.961)", "flake8 (==4.0.1)", "flake8-bugbear (==22.6.22)", "pre-commit (>=2.4,<3.0)"]
+tests = ["pytest", "pytz", "simplejson"]
+
+[[package]]
+name = "multidict"
+version = "6.0.2"
+description = "multidict implementation"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "peewee"
+version = "3.15.1"
+description = "a little orm"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "psycopg2"
+version = "2.9.3"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pyaes"
+version = "1.6.1"
+description = "Pure-Python Implementation of the AES block-cipher and common modes of operation"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyasn1"
+version = "0.4.8"
+description = "ASN.1 types and codecs"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pygments"
+version = "2.12.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "main"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["railroad-diagrams", "jinja2"]
+
+[[package]]
+name = "python-dotenv"
+version = "0.20.0"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "pytz"
+version = "2022.1"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "requests"
+version = "2.28.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "rich"
+version = "12.5.1"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
+optional = false
+python-versions = ">=3.6.3,<4.0.0"
+
+[package.dependencies]
+commonmark = ">=0.9.0,<0.10.0"
+pygments = ">=2.6.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+
+[[package]]
+name = "rsa"
+version = "4.9"
+description = "Pure-Python RSA implementation"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4"
+
+[package.dependencies]
+pyasn1 = ">=0.1.3"
+
+[[package]]
+name = "telethon"
+version = "1.24.0"
+description = "Full-featured Telegram client library for Python 3"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+pyaes = "*"
+rsa = "*"
+
+[package.extras]
+cryptg = ["cryptg"]
+
+[[package]]
+name = "urllib3"
+version = "1.26.11"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
+
+[package.extras]
+brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "yarl"
+version = "1.8.1"
+description = "Yet another URL library"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.10"
+content-hash = "cf2aa8bebb164e8dbd85f9e22232813ccaf44fdcd786cb900b459b0feeb738d6"
+
+[metadata.files]
+aiogram = [
+ {file = "aiogram-2.21-py3-none-any.whl", hash = "sha256:33ee61db550f6fc455e2d74d8911af31108e3c398eda03c2f91b0a7cb32a97d9"},
+ {file = "aiogram-2.21.tar.gz", hash = "sha256:390ac56a629cd0d151d544e3b87d539ee49f734ccc7a1a7e375c33f436e556e0"},
+]
+aiohttp = [
+ {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
+ {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
+ {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
+ {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
+ {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
+ {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
+ {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
+ {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
+ {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
+ {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
+ {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
+ {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
+ {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
+ {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
+ {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
+ {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
+ {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
+ {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
+ {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
+ {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
+ {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
+ {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
+ {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
+ {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
+ {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
+ {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
+ {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
+ {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
+ {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
+ {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
+ {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
+ {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
+ {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
+ {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
+ {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
+ {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
+ {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
+ {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
+ {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
+ {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
+ {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
+ {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
+ {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
+ {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
+ {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
+ {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
+ {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
+ {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
+]
+aioschedule = [
+ {file = "aioschedule-0.5.2.tar.gz", hash = "sha256:1fe8621d287f58cbba3d73695fbbd890355294ac0c01981a1fd1e4f0510fc744"},
+]
+aiosignal = [
+ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
+ {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
+]
+async-timeout = [
+ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
+ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
+]
+attrs = [
+ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+]
+babel = [
+ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
+ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
+]
+certifi = []
+charset-normalizer = []
+commonmark = [
+ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+environs = [
+ {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"},
+ {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"},
+]
+frozenlist = [
+ {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"},
+ {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"},
+ {file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"},
+ {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"},
+ {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"},
+ {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"},
+ {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"},
+ {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"},
+ {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"},
+ {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"},
+ {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"},
+ {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"},
+ {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"},
+ {file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"},
+ {file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"},
+ {file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"},
+ {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"},
+ {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"},
+ {file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"},
+ {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"},
+ {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"},
+ {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"},
+ {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"},
+ {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"},
+ {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"},
+ {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"},
+ {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"},
+ {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"},
+ {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"},
+ {file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"},
+ {file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"},
+ {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"},
+ {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"},
+ {file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"},
+ {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"},
+ {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"},
+ {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"},
+ {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"},
+ {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"},
+ {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"},
+ {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"},
+ {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"},
+ {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"},
+ {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"},
+ {file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"},
+ {file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"},
+ {file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"},
+]
+idna = [
+ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
+marshmallow = []
+multidict = [
+ {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
+ {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
+ {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
+ {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
+ {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
+ {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
+ {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
+ {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
+ {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
+ {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
+ {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
+ {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
+ {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
+ {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
+ {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
+ {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
+ {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
+ {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
+ {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
+ {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
+ {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
+ {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
+ {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
+ {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
+ {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
+ {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
+ {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
+ {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
+ {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
+ {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
+ {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
+ {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
+ {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
+ {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
+ {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
+ {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
+ {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
+ {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
+ {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
+ {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
+ {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
+ {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
+ {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
+ {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
+ {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
+ {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
+ {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
+ {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
+ {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
+ {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
+ {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
+ {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
+ {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
+ {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
+ {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
+ {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
+ {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
+ {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
+ {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
+]
+packaging = [
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+peewee = []
+psycopg2 = [
+ {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"},
+ {file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"},
+ {file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"},
+ {file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"},
+ {file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"},
+ {file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"},
+ {file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"},
+ {file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"},
+ {file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"},
+ {file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"},
+ {file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"},
+]
+pyaes = [
+ {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"},
+]
+pyasn1 = [
+ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
+ {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
+ {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
+ {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
+ {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
+ {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
+ {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
+ {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
+ {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
+ {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
+ {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
+ {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
+ {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
+]
+pygments = [
+ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
+ {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
+]
+pyparsing = [
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+python-dotenv = [
+ {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"},
+ {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"},
+]
+pytz = [
+ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
+ {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
+]
+requests = [
+ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
+ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+]
+rich = []
+rsa = []
+telethon = [
+ {file = "Telethon-1.24.0-py3-none-any.whl", hash = "sha256:04fdc5fa4ed3e886e6ecf4bad79205ab8880c6aefbd42c29c89c689a502aa816"},
+ {file = "Telethon-1.24.0.tar.gz", hash = "sha256:818cb61281ed3f75ba4da9b68cb69486bed9474d2db4e0aa16e482053117452c"},
+]
+urllib3 = []
+yarl = [
+ {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"},
+ {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"},
+ {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"},
+ {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"},
+ {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"},
+ {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"},
+ {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"},
+ {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"},
+ {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"},
+ {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"},
+ {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"},
+ {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"},
+ {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"},
+ {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"},
+ {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"},
+ {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"},
+ {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"},
+ {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"},
+ {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"},
+ {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"},
+ {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"},
+ {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"},
+ {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"},
+ {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"},
+ {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"},
+ {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"},
+ {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"},
+ {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"},
+ {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"},
+ {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"},
+ {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"},
+ {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"},
+ {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"},
+ {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"},
+ {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"},
+ {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"},
+ {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"},
+ {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"},
+ {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"},
+ {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"},
+ {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"},
+ {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"},
+ {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"},
+ {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"},
+ {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"},
+ {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"},
+ {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"},
+ {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"},
+ {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"},
+ {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"},
+ {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"},
+ {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"},
+ {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"},
+ {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"},
+ {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"},
+ {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"},
+ {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"},
+ {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"},
+ {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c7defa7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+[tool.poetry]
+name = "moderator_bot"
+version = "0.1.0"
+description = "Telegram bot for moderation of Telegram groups."
+authors = ["hok7z "]
+license = "GPL3"
+
+[tool.poetry.dependencies]
+python = "^3.10"
+aiogram = "^2.20"
+peewee = "^3.14.10"
+rich = "^12.4.4"
+environs = "^9.5.0"
+requests = "^2.27.1"
+Telethon = "^1.24.0"
+psycopg2 = "^2.9.3"
+aioschedule = "^0.5.2"
+
+[tool.poetry.dev-dependencies]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/states/__init__.py b/states/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/states/report_message.py b/states/report_message.py
new file mode 100644
index 0000000..d11ac1a
--- /dev/null
+++ b/states/report_message.py
@@ -0,0 +1,6 @@
+from aiogram.dispatcher.filters.state import StatesGroup,State
+
+
+class States(StatesGroup):
+ state1 = State()
+ state2 = State()
diff --git a/txt/link_shorters.txt b/txt/link_shorters.txt
new file mode 100644
index 0000000..9c00e3d
--- /dev/null
+++ b/txt/link_shorters.txt
@@ -0,0 +1,231 @@
+bit.ly
+goo.gl
+tinyurl.com
+is.gd
+cli.gs
+pic.gd
+DwarfURL.com
+ow.ly
+yfrog.com
+migre.me
+ff.im
+tiny.cc
+url4.eu
+tr.im
+twit.ac
+su.pr
+twurl.nl
+snipurl.com
+BudURL.com
+short.to
+ping.fm
+Digg.com
+post.ly
+Just.as
+.tk
+bkite.com
+snipr.com
+flic.kr
+loopt.us
+doiop.com
+twitthis.com
+htxt.it
+AltURL.com
+RedirX.com
+DigBig.com
+short.ie
+u.mavrev.com
+kl.am
+wp.me
+u.nu
+rubyurl.com
+om.ly
+linkbee.com
+Yep.it
+posted.at
+xrl.us
+metamark.net
+sn.im
+hurl.ws
+eepurl.com
+idek.net
+urlpire.com
+chilp.it
+moourl.com
+snurl.com
+xr.com
+lin.cr
+EasyURI.com
+zz.gd
+ur1.ca
+URL.ie
+adjix.com
+twurl.cc
+s7y.us shrinkify
+EasyURL.net
+atu.ca
+sp2.ro
+Profile.to
+ub0.cc
+minurl.fr
+cort.as
+fire.to
+2tu.us
+twiturl.de
+to.ly
+BurnURL.com
+nn.nf
+clck.ru
+notlong.com
+thrdl.es
+spedr.com
+vl.am
+miniurl.com
+virl.com
+PiURL.com
+1url.com
+gri.ms
+tr.my
+Sharein.com
+urlzen.com
+fon.gs
+Shrinkify.com
+ri.ms
+b23.ru
+Fly2.ws
+xrl.in
+Fhurl.com
+wipi.es
+korta.nu
+shortna.me
+fa.b
+WapURL.co.uk
+urlcut.com
+6url.com
+abbrr.com
+SimURL.com
+klck.me
+x.se
+2big.at
+url.co.uk
+ewerl.com
+inreply.to
+TightURL.com
+a.gg
+tinytw.it
+zi.pe
+riz.gd
+hex.io
+fwd4.me
+bacn.me
+shrt.st
+ln-s.ru
+tiny.pl
+o-x.fr
+StartURL.com
+jijr.com
+shorl.com
+icanhaz.com
+updating.me
+kissa.be
+hellotxt.com
+pnt.me
+nsfw.in
+xurl.jp
+yweb.com
+urlkiss.com
+QLNK.net
+w3t.org
+lt.tl
+twirl.at
+zipmyurl.com
+urlot.com
+a.nf
+hurl.me
+URLHawk.com
+Tnij.org
+4url.cc
+firsturl.de
+Hurl.it
+sturly.com
+shrinkster.com
+ln-s.net
+go2cut.com
+liip.to
+shw.me
+XeeURL.com
+liltext.com
+lnk.gd
+xzb.cc
+linkbun.ch
+href.in
+urlbrief.com
+2ya.com
+safe.mn
+shrunkin.com
+bloat.me
+krunchd.com
+minilien.com
+ShortLinks.co.uk
+qicute.com
+rb6.me
+urlx.ie
+pd.am
+go2.me
+tinyarro.ws
+tinyvid.io
+lurl.no
+ru.ly
+lru.jp
+rickroll.it
+togoto.us
+ClickMeter.com
+hugeurl.com
+tinyuri.ca
+shrten.com
+shorturl.com
+Quip-Art.com
+urlao.com
+a2a.me
+tcrn.ch
+goshrink.com
+DecentURL.com
+decenturl.com
+zi.ma
+1link.in
+sharetabs.com
+shoturl.us
+fff.to
+hover.com
+lnk.in
+jmp2.net
+dy.fi
+urlcover.com
+2pl.us
+tweetburner.com
+u6e.de
+xaddr.com
+gl.am
+dfl8.me
+go.9nl.com
+gurl.es
+C-O.IN
+TraceURL.com
+liurl.cn
+MyURL.in
+urlenco.de
+ne1.net
+buk.me
+rsmonkey.com
+cuturl.com
+turo.us
+sqrl.it
+iterasi.net
+tiny123.com
+EsyURL.com
+urlx.org
+IsCool.net
+twitterpan.com
+GoWat.ch
+poprl.com
+njx.me
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..833675a
--- /dev/null
+++ b/utils/__init__.py
@@ -0,0 +1,9 @@
+from .notify_start import notify_started_bot
+from .default_commands import set_default_commands
+
+from .update_user_data import check_user_data
+
+from .telegram_client import TelegramClientScrapper
+from .parse_timedelta import parse_timedelta
+
+from .virustotal import VirusTotalAPI
diff --git a/utils/default_commands.py b/utils/default_commands.py
new file mode 100644
index 0000000..94567f4
--- /dev/null
+++ b/utils/default_commands.py
@@ -0,0 +1,6 @@
+async def set_default_commands(dp):
+ from load import types
+ await dp.bot.set_my_commands([
+ types.BotCommand("start","Start bot"),
+ types.BotCommand("help","Help")
+ ])
diff --git a/utils/notify_start.py b/utils/notify_start.py
new file mode 100644
index 0000000..6697043
--- /dev/null
+++ b/utils/notify_start.py
@@ -0,0 +1,4 @@
+import config
+
+async def notify_started_bot(bot):
+ await bot.send_message(config.telegram_log_chat_id,"Bot started!")
diff --git a/utils/parse_timedelta.py b/utils/parse_timedelta.py
new file mode 100644
index 0000000..3491762
--- /dev/null
+++ b/utils/parse_timedelta.py
@@ -0,0 +1,12 @@
+import re
+import datetime as dt
+from typing import Union
+
+def parse_timedelta(specification: str) -> Union[None, dt.timedelta]:
+ specification = specification.strip().replace(' ', '')
+ match = re.fullmatch(r'(?:(\d+)(?:d|д))?(?:(\d+)(?:h|ч))?(?:(\d+)(?:m|м))?(?:(\d+)(?:s|с))?', specification)
+ if match:
+ units = [(0 if i is None else int(i)) for i in match.groups()]
+ return dt.timedelta(days=units[0], hours=units[1], minutes=units[2], seconds=units[3])
+ else:
+ return None
diff --git a/utils/telegram_client.py b/utils/telegram_client.py
new file mode 100644
index 0000000..74477d5
--- /dev/null
+++ b/utils/telegram_client.py
@@ -0,0 +1,61 @@
+from telethon import TelegramClient
+from telethon.errors import SessionPasswordNeededError
+from telethon.tl.functions.channels import GetParticipantsRequest
+from telethon.tl.types import ChannelParticipantsSearch
+from telethon.tl.types import PeerChannel
+
+
+class TelegramClientScrapper:
+ def __init__(self, api_id, api_hash, phone=None, token=None, loop=None):
+ self.api_id = api_id
+ self.api_hash = api_hash
+ self.phone = phone
+ self.loop = loop
+ self.token = token
+
+ async def _connect(self):
+ self.client = TelegramClient("session", self.api_id, self.api_hash, loop=self.loop)
+ await self.client.start(bot_token=self.token)
+ if not await self.client.is_user_authorized():
+ await self.client.send_code_request(self.phone)
+ try:
+ await self.client.sign_in(self.phone, input("Enter you just recieved:"))
+ except SessionPasswordNeededError:
+ await self.client.sign_in(password=input("Enter password:"))
+
+ async def get_group_users(self, group_id):
+
+ chat_entity = PeerChannel(int(group_id))
+
+ offset = 0
+ limit = 100
+ list_participants = []
+
+
+ while True:
+ participants = await self.client(GetParticipantsRequest(
+ chat_entity, ChannelParticipantsSearch(''), offset, limit,
+ hash=0
+ ))
+
+ if (not participants.users):
+ break
+
+ list_participants.extend(participants.users)
+ offset += len(participants.users)
+
+ participants_details = []
+ for participant in list_participants:
+ is_bot = participant.bot
+ user_name = participant.username
+ if (user_name):
+ user_name = f"@{user_name}"
+
+ if (not is_bot):
+ participants_details.append({
+ "id": participant.id,
+ "first_name": participant.first_name,
+ "user_name":user_name
+ })
+
+ return participants_details
diff --git a/utils/update_user_data.py b/utils/update_user_data.py
new file mode 100644
index 0000000..7ca9083
--- /dev/null
+++ b/utils/update_user_data.py
@@ -0,0 +1,37 @@
+from database.models import Member
+from config import group_id
+
+async def __is_group_owner(user_id):
+ from load import bot
+ member = await bot.get_chat_member(group_id,user_id)
+ return member.is_chat_owner()
+
+async def check_user_data():
+ """Check user data in database and update it"""
+ from load import tgc,database
+ users = await tgc.get_group_users(group_id)
+
+ for user in users:
+ user_exists = database.check_data_exists(Member.user_id,user["id"])
+
+ role = "member"
+ if (await __is_group_owner(user["id"])):role = "owner"
+
+ if (not user_exists):
+ user_name = user["user_name"]
+
+ if (user_name):
+ user_name = f"@{user_name}"
+
+ database.register_user(
+ user["id"],
+ user["first_name"],
+ user["user_name"],
+ role,
+ )
+
+ else:
+ database.update_member_data(user["id"],
+ [Member.first_name,Member.user_name],
+ [user["first_name",user["user_name"]]]
+ )
diff --git a/utils/virustotal.py b/utils/virustotal.py
new file mode 100644
index 0000000..32b7700
--- /dev/null
+++ b/utils/virustotal.py
@@ -0,0 +1,77 @@
+import io
+from typing import Union,Any
+import aiohttp
+
+# TODO: skip queue virustotal
+class VirusTotalAPI:
+ def __init__(self,apikey:str,local_telegram_api:bool):
+ self.apikey = apikey
+ self.local_telegram_api = local_telegram_api
+
+ async def __download_file(self,filepath:str,
+ *args,**kw) -> Union[io.BytesIO,Any]:
+
+ if ( self.local_telegram_api ):
+ with open(filepath,'rb') as bf:
+ return io.BytesIO(bf.read())
+ else:
+ from load import bot
+ return await bot.download_file(filepath,
+ *args,**kw)
+
+ async def __file_scan(self,filepath) -> None:
+ file = await self.__download_file(filepath)
+
+ url = "https://www.virustotal.com/vtapi/v2/file/scan"
+ params = {"apikey":self.apikey,"file":file}
+
+ async with aiohttp.ClientSession() as session:
+ response = await session.post(url,data=params)
+ response = await response.json()
+
+ return response["sha1"]
+
+ async def __file_report(self,resource) -> dict:
+ url = "https://www.virustotal.com/vtapi/v2/file/report"
+ params = {"apikey":self.apikey,"resource":resource}
+
+ async with aiohttp.ClientSession() as session:
+ response = await session.get(url,params=params)
+ response = await response.json()
+
+ return response
+
+ def format_output(self,file_report:dict) -> str:
+ """Format file_report
+ File Analys
+ Status:Infected/Clear
+ Positives:positives/total percent%
+ File Report
+ """
+
+ total = file_report["total"]
+ positives = file_report["positives"]
+ permalink = file_report["permalink"]
+ percent = round(positives/total*100)
+
+ if (percent >= 40):
+ status = "Infected ☣️"
+ else:
+ status = "Clear ✅"
+
+ output = (
+ (
+ "File Analys\n"
+ f"Detected:{positives}/{total} %{percent}\n"
+ f"Status:{status}\n"
+ f"[File Report]({permalink})\n"
+ )
+ )
+
+ return output
+
+ async def scan_file(self,filepath:str) -> str:
+ resource = await self.__file_scan(filepath)
+ file_report = await self.__file_report(resource)
+
+ return file_report