diff --git a/.gitignore b/.gitignore
index c96bc0153..135283511 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
*.egg-info/
*.eggs/
*.pyc
+.idea
.cache/
_build/
build/
@@ -11,5 +12,5 @@ dist/
.venv
.vscode
__pycache__
-
*.token
+*.pypirc
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 514896ffe..d9eda9a86 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.0.1
+ rev: v4.1.0
hooks:
- id: requirements-txt-fixer
name: Requirements
@@ -39,7 +39,7 @@ repos:
name: flake8 Formatting
language: python
types: [file, python]
- args: [--max-line-length=100, --ignore=E203 E501 E402 W503 W504]
+ args: [--max-line-length=100, --ignore=E203 E301 E302 E501 E402 E704 W503 W504]
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index d9facf0ca..18a266263 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -10,7 +10,7 @@ the library off of pip:
.. code-block:: bash
- pip install -U discord-py-interactions
+ pip install -U discord-py-interactions[dev]
Once you have the library installed in Python, you are able to instantiate and run a basic bot
with a logging level that is set for debugging purposes. This is recommend in order to make it easier
diff --git a/LICENSE b/LICENSE
index 30bb725f4..f288702d2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,674 @@
-MIT License
-
-Copyright (c) 2020-2021 eunwoo1104
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.rst b/README.rst
index 7a540b0c3..c5e55a284 100644
--- a/README.rst
+++ b/README.rst
@@ -36,7 +36,7 @@ Ever since December 2019, this open-source project has become the culmination of
- Looking for a compatible library that implements all interactions?
- Itching to get your hands on slash commands, but in a simple manner?
-Look no more! The goal of this library is to make all three of these questions go from possibilites to trivial matters.
+Look no more! The goal of this library is to make all three of these questions go from possibilities to trivial matters.
What can we do?
***************
diff --git a/docs/faq.rst b/docs/faq.rst
index 4fcd49219..2b767b9a3 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -48,36 +48,48 @@ What does that mean? Well, we'll show you:
import interactions
from discord.ext import commands
- interactions = interactions.Client(token="...")
+ client = interactions.Client(token="...")
dpy = commands.Bot(prefix="/")
@dpy.command()
async def hello(ctx):
await ctx.send("Hello from discord.py!")
- @interactions.command(
+ @client.command(
name="test",
description="this is just a testing command."
)
async def test(ctx):
await ctx.send("Hello from discord-interactions!")
- interactions.start()
- dpy.run(token="...", bot=True)
+ loop = asyncio.get_event_loop()
+
+ task2 = loop.create_task(dpy.start(token="...", bot=True))
+ task1 = loop.create_task(client.ready())
+
+ gathered = asyncio.gather(task1, task2, loop=loop)
+ loop.run_until_complete(gathered)
Both of these variables ``interactions`` and ``dpy`` will be able to run in the same established environment, and additionally
-will both function properly as their respective libraries intend them to. What about the models, though? That's a simple answer:
+will both function properly as their respective libraries intend them to. This implementation uses asyncio.gather to execute
+both starts simultaneously as asyncio tasks, and runs them under one singular loop.
+
+Compared to traditional startup commands, ``interactions.ready()`` and ``dpy.start()`` is used instead of
+the typical ``interactions.start()`` and ``dpy.run()`` methods because of synchronous/async functions.
+``asyncio.gather()`` works with coroutines, hence the transition.
+
+What about the models, though? That's a simple answer:
.. code-block:: python
import discord
- from interactions.api.models.member import Member
+ import interactions
@dpy.command()
- async def borrowing(ctx, member: Member):
+ async def borrowing(ctx, member: interactions.Member):
await ctx.send(f"Member ID: {member.id}")
- @interactions.command(...)
+ @client.command(...)
async def second_borrowing(ctx, member: discord.Member):
await ctx.send(f"Member ID: {member.id}")
@@ -127,6 +139,26 @@ of discord.py bot developers frown upon doing, so this is at your own risk to co
can take a page out of discord.js' book if you want to do this, since they've never heard of an external command handler framework
before in their entire life.
+
+I'm getting "``AttributeError: HTTPClient not found!``" when I try to execute helper methods!
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Probably you are doing something like this:
+
+.. code-block:: python
+
+ channel = interactions.Channel(**await bot.http.get_channel(channel_id))
+ await channel.send("...")
+
+And the error occurs in the line where you try to send something. You can fix this easy by adding one argument:
+
+.. code-block:: python
+
+ channel = interactions.Channel(**await bot.http.get_channel(channel_id), _client=bot.http)
+ await channel.send("...")
+
+You have to add this extra argument for every object you instantiate by yourself if you want to use it's methods
+
+
My question is not answered on here!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Please join our Discord server for any further support regarding our library and/or any integration code depending on it.
diff --git a/interactions/__init__.py b/interactions/__init__.py
index 3a89e940a..809eed726 100644
--- a/interactions/__init__.py
+++ b/interactions/__init__.py
@@ -12,9 +12,9 @@
Co-authored by DeltaXW.
"""
from .api.models.channel import * # noqa: F401 F403
+from .api.models.flags import * # noqa: F401 F403
from .api.models.guild import * # noqa: F401 F403
from .api.models.gw import * # noqa: F401 F403
-from .api.models.intents import * # noqa: F401 F403
from .api.models.member import * # noqa: F401 F403
from .api.models.message import * # noqa: F401 F403
from .api.models.misc import * # noqa: F401 F403
diff --git a/interactions/api/dispatch.py b/interactions/api/dispatch.py
index 95e6e4cf3..e15c11bf8 100644
--- a/interactions/api/dispatch.py
+++ b/interactions/api/dispatch.py
@@ -1,15 +1,10 @@
from asyncio import get_event_loop
-from logging import Logger, StreamHandler, basicConfig, getLogger
+from logging import Logger
from typing import Coroutine, Optional
-from ..base import CustomFormatter, Data
+from interactions.base import get_logger
-basicConfig(level=Data.LOGGER)
-log: Logger = getLogger("dispatch")
-stream: StreamHandler = StreamHandler()
-stream.setLevel(Data.LOGGER)
-stream.setFormatter(CustomFormatter())
-log.addHandler(stream)
+log: Logger = get_logger("dispatch")
class Listener:
diff --git a/interactions/api/error.py b/interactions/api/error.py
index 0e7248dc4..467e074fe 100644
--- a/interactions/api/error.py
+++ b/interactions/api/error.py
@@ -24,7 +24,7 @@ class InteractionException(Exception):
and for extensive testing/review before integration.
Likewise, this will show the concepts before use, and will be refined when time goes on.
- :ivar interactions.api.error.ErrorFormatter _formatter: The built in formatter.
+ :ivar interactions.api.error.ErrorFormatter _formatter: The built-in formatter.
:ivar dict _lookup: A dictionary containing the values from the built-in Enum.
"""
@@ -145,7 +145,7 @@ class GatewayException(InteractionException):
"""
This is a derivation of InteractionException in that this is used to represent Gateway closing OP codes.
- :ivar ErrorFormatter _formatter: The built in formatter.
+ :ivar ErrorFormatter _formatter: The built-in formatter.
:ivar dict _lookup: A dictionary containing the values from the built-in Enum.
"""
@@ -178,7 +178,7 @@ class HTTPException(InteractionException):
"""
This is a derivation of InteractionException in that this is used to represent HTTP Exceptions.
- :ivar ErrorFormatter _formatter: The built in formatter.
+ :ivar ErrorFormatter _formatter: The built-in formatter.
:ivar dict _lookup: A dictionary containing the values from the built-in Enum.
"""
@@ -204,7 +204,7 @@ class JSONException(InteractionException):
"""
This is a derivation of InteractionException in that this is used to represent JSON API Exceptions.
- :ivar ErrorFormatter _formatter: The built in formatter.
+ :ivar ErrorFormatter _formatter: The built-in formatter.
:ivar dict _lookup: A dictionary containing the values from the built-in Enum.
"""
diff --git a/interactions/api/gateway.py b/interactions/api/gateway.py
index c2c0ab49f..c4d2b2e4d 100644
--- a/interactions/api/gateway.py
+++ b/interactions/api/gateway.py
@@ -1,7 +1,7 @@
import sys
from asyncio import get_event_loop, run_coroutine_threadsafe
from json import dumps
-from logging import Logger, StreamHandler, basicConfig, getLogger
+from logging import Logger
from random import random
from threading import Event, Thread
from typing import Any, List, Optional, Union
@@ -10,21 +10,16 @@
from orjson import loads
from interactions.api.models.gw import Presence
+from interactions.base import get_logger
from interactions.enums import InteractionType, OptionType
-from ..base import CustomFormatter, Data
from .dispatch import Listener
from .enums import OpCodeType
from .error import GatewayException
from .http import HTTPClient
-from .models.intents import Intents
+from .models.flags import Intents
-basicConfig(level=Data.LOGGER)
-log: Logger = getLogger("gateway")
-stream: StreamHandler = StreamHandler()
-stream.setLevel(Data.LOGGER)
-stream.setFormatter(CustomFormatter())
-log.addHandler(stream)
+log: Logger = get_logger("gateway")
__all__ = ("Heartbeat", "WebSocket")
@@ -32,6 +27,7 @@
class Heartbeat(Thread):
"""
A class representing a consistent heartbeat connection with the gateway.
+
:ivar WebSocket ws: The WebSocket class to infer on.
:ivar Union[int, float] interval: The heartbeat interval determined by the gateway.
:ivar Event event: The multi-threading event.
@@ -78,6 +74,7 @@ def stop(self) -> None:
class WebSocket:
"""
A class representing a websocket connection with the gateway.
+
:ivar Intents intents: An instance of :class:`interactions.api.models.Intents`.
:ivar AbstractEventLoop loop: The coroutine event loop established on.
:ivar Request req: An instance of :class:`interactions.api.http.Request`.
@@ -149,6 +146,7 @@ async def connect(
) -> None:
"""
Establishes a connection to the gateway.
+
:param token: The token to use for identifying.
:type token: str
:param shard?: The shard ID to identify under.
@@ -157,10 +155,10 @@ async def connect(
:type presence: Optional[Presence]
"""
self.http = HTTPClient(token)
- self.options["headers"] = {"User-Agent": self.http.req.headers["User-Agent"]}
+ self.options["headers"] = {"User-Agent": self.http.req._headers["User-Agent"]}
url = await self.http.get_gateway()
- async with self.http._req.session.ws_connect(url, **self.options) as self.session:
+ async with self.http._req._session.ws_connect(url, **self.options) as self.session:
while not self.closed:
stream = await self.recv()
@@ -178,6 +176,7 @@ async def handle_connection(
) -> None:
"""
Handles the connection to the gateway.
+
:param stream: The data stream from the gateway.
:type stream: dict
:param shard?: The shard ID to identify under.
@@ -205,13 +204,11 @@ async def handle_connection(
await self.heartbeat()
self.keep_alive.start()
- if op == OpCodeType.HEARTBEAT:
- if self.keep_alive:
- await self.heartbeat()
+ if op == OpCodeType.HEARTBEAT and self.keep_alive:
+ await self.heartbeat()
- if op == OpCodeType.HEARTBEAT_ACK:
- if self.keep_alive:
- log.debug("HEARTBEAT_ACK")
+ if op == OpCodeType.HEARTBEAT_ACK and self.keep_alive:
+ log.debug("HEARTBEAT_ACK")
if op in (OpCodeType.INVALIDATE_SESSION, OpCodeType.RECONNECT):
log.debug("INVALID_SESSION/RECONNECT")
@@ -226,19 +223,19 @@ async def handle_connection(
self.session_id = None
self.sequence = None
self.closed = True
+ elif event == "READY":
+ self.session_id = data["session_id"]
+ self.sequence = stream["s"]
+ self.dispatch.dispatch("on_ready")
+ log.debug(f"READY (SES_ID: {self.session_id}, SEQ_ID: {self.sequence})")
else:
- if event == "READY":
- self.session_id = data["session_id"]
- self.sequence = stream["s"]
- self.dispatch.dispatch("on_ready")
- log.debug(f"READY (SES_ID: {self.session_id}, SEQ_ID: {self.sequence})")
- else:
- log.debug(f"{event}: {dumps(data, indent=4, sort_keys=True)}")
- self.handle_dispatch(event, data)
+ log.debug(f"{event}: {dumps(data, indent=4, sort_keys=True)}")
+ self.handle_dispatch(event, data)
def handle_dispatch(self, event: str, data: dict) -> None:
"""
Handles the dispatched event data from a gateway event.
+
:param event: The name of the event.
:type event: str
:param data: The data of the event.
@@ -246,7 +243,7 @@ def handle_dispatch(self, event: str, data: dict) -> None:
"""
def check_sub_command(option: dict) -> dict:
- kwargs = dict()
+ kwargs: dict = {}
if option["type"] == OptionType.SUB_COMMAND_GROUP:
kwargs["sub_command_group"] = option["name"]
if option.get("options"):
@@ -270,7 +267,7 @@ def check_sub_auto(option: dict) -> tuple:
if option["type"] == OptionType.SUB_COMMAND_GROUP:
for group_option in option["options"]:
if group_option.get("options"):
- for sub_option in option["options"]:
+ for sub_option in group_option["options"]:
if sub_option.get("focused"):
return sub_option["name"], sub_option["value"]
elif option["type"] == OptionType.SUB_COMMAND:
@@ -298,6 +295,8 @@ def check_sub_auto(option: dict) -> tuple:
__import__(path),
_name,
)
+ if "_create" in event.lower() or "_add" in event.lower():
+ data["_client"] = self.http
self.dispatch.dispatch(f"on_{name}", obj(**data)) # noqa
except AttributeError as error: # noqa
log.fatal(f"You're missing a data model for the event {name}: {error}")
@@ -310,7 +309,7 @@ def check_sub_auto(option: dict) -> tuple:
_args: list = [context]
_kwargs: dict = dict()
if data["type"] == InteractionType.APPLICATION_COMMAND:
- _name = context.data.name
+ _name = f"command_{context.data.name}"
if context.data._json.get("options"):
if context.data.options:
for option in context.data.options:
@@ -320,9 +319,11 @@ def check_sub_auto(option: dict) -> tuple:
)
)
elif data["type"] == InteractionType.MESSAGE_COMPONENT:
- _name = context.data.custom_id
+ _name = f"component_{context.data.custom_id}"
+ if context.data._json.get("values"):
+ _args.append(context.data.values)
elif data["type"] == InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE:
- _name = "autocomplete_"
+ _name = f"autocomplete_{context.data.id}"
if context.data._json.get("options"):
if context.data.options:
for option in context.data.options:
@@ -330,7 +331,7 @@ def check_sub_auto(option: dict) -> tuple:
option if isinstance(option, dict) else option._json
)
if add_name:
- _name += add_name
+ _name += f"_{add_name}"
if add_args:
_args.append(add_args)
elif data["type"] == InteractionType.MODAL_SUBMIT:
@@ -349,6 +350,7 @@ def contextualize(self, data: dict) -> object:
"""
Takes raw data given back from the gateway
and gives "context" based off of what it is.
+
:param data: The data from the gateway.
:type data: dict
:return: The context object.
@@ -366,10 +368,8 @@ def contextualize(self, data: dict) -> object:
elif data["type"] == InteractionType.MESSAGE_COMPONENT:
_context = "ComponentContext"
- context: object = getattr(__import__("interactions.context"), _context)
-
data["client"] = self.http
-
+ context: object = getattr(__import__("interactions.context"), _context)
return context(**data)
async def send(self, data: Union[str, dict]) -> None:
@@ -382,6 +382,7 @@ async def identify(
) -> None:
"""
Sends an ``IDENTIFY`` packet to the gateway.
+
:param shard?: The shard ID to identify under.
:type shard: Optional[int]
:param presence?: The presence to change the bot to on identify.
diff --git a/interactions/api/gateway.pyi b/interactions/api/gateway.pyi
index 5c93feeb1..d8cb194f7 100644
--- a/interactions/api/gateway.pyi
+++ b/interactions/api/gateway.pyi
@@ -5,7 +5,7 @@ from typing import Any, List, Optional, Union
from .dispatch import Listener
from .http import HTTPClient
from .models.gw import Presence
-from .models.intents import Intents
+from .models.flags import Intents
class Heartbeat(Thread):
ws: Any
diff --git a/interactions/api/http.py b/interactions/api/http.py
index 9dbf8af63..03399a824 100644
--- a/interactions/api/http.py
+++ b/interactions/api/http.py
@@ -1,6 +1,8 @@
-from asyncio import AbstractEventLoop, Event, Lock, get_event_loop, sleep
+import asyncio
+import traceback
+from asyncio import AbstractEventLoop, Lock, get_event_loop, get_running_loop
from json import dumps
-from logging import Logger, StreamHandler, basicConfig, getLogger
+from logging import Logger
from sys import version_info
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union
from urllib.parse import quote
@@ -9,6 +11,8 @@
from aiohttp import __version__ as http_version
import interactions.api.cache
+from interactions.base import __version__, get_logger
+from interactions.models.misc import MISSING
from ..api.cache import Cache, Item
from ..api.error import HTTPException
@@ -28,17 +32,11 @@
User,
WelcomeScreen,
)
-from ..base import CustomFormatter, Data, __version__
-basicConfig(level=Data.LOGGER)
-log: Logger = getLogger("http")
-stream: StreamHandler = StreamHandler()
-stream.setLevel(Data.LOGGER)
-stream.setFormatter(CustomFormatter())
-log.addHandler(stream)
+log: Logger = get_logger("http")
-__all__ = ("Route", "Padlock", "Request", "HTTPClient")
-session: ClientSession = ClientSession()
+__all__ = ("Route", "Request", "HTTPClient")
+_session: ClientSession = ClientSession()
class Route:
@@ -74,47 +72,61 @@ def __init__(self, method: str, path: str, **kwargs) -> None:
self.channel_id = kwargs.get("channel_id")
self.guild_id = kwargs.get("guild_id")
- @property
- def bucket(self) -> str:
+ def get_bucket(self, shared_bucket: Optional[str] = None) -> str:
"""
- Returns the route's bucket.
+ Returns the route's bucket. If shared_bucket is None, returns the path with major parameters.
+ Otherwise, it relies on Discord's given bucket.
+
+ :param shared_bucket: The bucket that Discord provides, if available.
+ :type shared_bucket: Optional[str]
:return: The route bucket.
:rtype: str
"""
- return f"{self.channel_id}:{self.guild_id}:{self.path}"
+ return (
+ f"{self.channel_id}:{self.guild_id}:{self.path}"
+ if shared_bucket is None
+ else f"{self.channel_id}:{self.guild_id}:{shared_bucket}"
+ )
+
+ @property
+ def endpoint(self) -> str:
+ """
+ Returns the route's endpoint.
+
+ :return: The route endpoint.
+ :rtype: str
+ """
+ return f"{self.method}:{self.path}"
-class Padlock:
+class Limiter:
"""
- A class representing ratelimited sessions as a "locked" event.
+ A class representing a limitation for an HTTP request.
- :ivar Lock lock: The lock coroutine event.
- :ivar bool keep_open: Whether the lock should stay open or not.
+ :ivar Lock lock: The "lock" or controller of the request.
+ :ivar float reset_after: The remaining time before the request can be ran.
"""
- __slots__ = ("lock", "keep_open")
lock: Lock
- keep_open: bool
+ reset_after: float
- def __init__(self, lock: Lock) -> None:
+ def __init__(self, *, lock: Lock, reset_after: Optional[float] = MISSING) -> None:
"""
- :param lock: The lock coroutine event.
+ :param lock: The asynchronous lock to control limits for.
:type lock: Lock
+ :param reset_after: The remaining time to run the limited lock on. Defaults to ``0``.
+ :type reset_after: Optional[float]
"""
self.lock = lock
- self.keep_open = True
-
- def click(self) -> None:
- """Re-closes the lock after the instiantiation and invocation ends."""
- self.keep_open = False
+ self.reset_after = 0 if reset_after is MISSING else reset_after
- def __enter__(self) -> Any:
+ async def __aenter__(self) -> "Limiter":
+ await self.lock.acquire()
return self
- def __exit__(self, exc_type, exc_val, exc_tb) -> None:
- if self.keep_open:
- self.lock.release()
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
+ return self.lock.release()
class Request:
@@ -122,20 +134,30 @@ class Request:
A class representing how HTTP requests are sent/read.
:ivar str token: The current application token.
- :ivar AbstractEventLoop loop: The current coroutine event loop.
- :ivar dict ratelimits: The current ratelimits from the Discord API.
- :ivar dict headers: The current headers for an HTTP request.
- :ivar ClientSession session: The current session for making requests.
- :ivar Event lock: The ratelimit lock event.
+ :ivar AbstractEventLoop _loop: The current coroutine event loop.
+ :ivar Dict[str, Limiter] ratelimits: The current per-route rate limiters from the API.
+ :ivar Dict[str, str] buckets: The current endpoint to shared_bucket cache from the API.
+ :ivar dict _headers: The current headers for an HTTP request.
+ :ivar ClientSession _session: The current session for making requests.
+ :ivar Limiter _global_lock: The global rate limiter.
"""
- __slots__ = ("token", "loop", "ratelimits", "headers", "session", "lock")
+ __slots__ = (
+ "token",
+ "_loop",
+ "ratelimits",
+ "buckets",
+ "_headers",
+ "_session",
+ "_global_lock",
+ )
token: str
- loop: AbstractEventLoop
- ratelimits: dict
- headers: dict
- session: ClientSession
- lock: Event
+ _loop: AbstractEventLoop
+ ratelimits: Dict[str, Limiter] # bucket: Limiter
+ buckets: Dict[str, str] # endpoint: shared_bucket
+ _headers: dict
+ _session: ClientSession
+ _global_lock: Limiter
def __init__(self, token: str) -> None:
"""
@@ -143,23 +165,31 @@ def __init__(self, token: str) -> None:
:type token: str
"""
self.token = token
- self.loop = get_event_loop()
- self.session = session
+ self._loop = get_event_loop() if version_info < (3, 10) else get_running_loop()
self.ratelimits = {}
- self.headers = {
+ self.buckets = {}
+ self._headers = {
"Authorization": f"Bot {self.token}",
- "User-Agent": f"DiscordBot (https://github.com/goverfl0w/discord-interactions {__version__} "
+ "User-Agent": f"DiscordBot (https://github.com/goverfl0w/interactions.py {__version__} "
f"Python/{version_info[0]}.{version_info[1]} "
f"aiohttp/{http_version}",
}
- self.lock = Event() if version_info >= (3, 10) else Event(loop=self.loop)
-
- self.lock.set()
+ self._session = _session
+ self._global_lock = (
+ Limiter(lock=Lock(loop=self._loop)) if version_info < (3, 10) else Limiter(lock=Lock())
+ )
- def check_session(self) -> None:
+ def _check_session(self) -> None:
"""Ensures that we have a valid connection session."""
- if self.session.closed:
- self.session = ClientSession()
+ if self._session.closed:
+ self._session = ClientSession()
+
+ async def _check_lock(self) -> None:
+ """Checks the global lock for its current state."""
+ if self._global_lock.lock.locked():
+ log.warning("The HTTP client is still globally locked, waiting for it to clear.")
+ await self._global_lock.lock.acquire()
+ self._global_lock.reset_after = 0
async def request(self, route: Route, **kwargs) -> Optional[Any]:
r"""
@@ -172,75 +202,117 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]:
:return: The contents of the request if any.
:rtype: Optional[Any]
"""
- self.check_session()
- bucket: Optional[str] = route.bucket
+ kwargs["headers"] = {**self._headers, **kwargs.get("headers", {})}
+ kwargs["headers"]["Content-Type"] = "application/json"
+
+ reason = kwargs.pop("reason", None)
+ if reason:
+ kwargs["headers"]["X-Audit-Log-Reason"] = quote(reason, safe="/ ")
- for _ in range(3):
- ratelimit: Lock = self.ratelimits.get(bucket)
+ # Huge credit and thanks to LordOfPolls for the lock/retry logic.
- if not self.lock.is_set():
- log.warning("The request is still locked, waiting for it to clear.")
- await self.lock.wait()
+ bucket = route.get_bucket(
+ self.buckets.get(route.endpoint)
+ ) # string returning path OR prioritised hash bucket metadata.
- if ratelimit is None:
- self.ratelimits[bucket] = Lock()
- continue
+ # The idea is that its regulated by the priority of Discord's bucket header and not just self-computation.
- await ratelimit.acquire()
+ if self.ratelimits.get(bucket):
+ _limiter: Limiter = self.ratelimits.get(bucket)
+ if _limiter.lock.locked():
+ if (
+ _limiter.reset_after != 0
+ ): # Just saying 0 seconds isn't helpful, so this is suppressed.
+ log.warning(
+ f"The current bucket is still under a rate limit. Calling later in {_limiter.reset_after} seconds."
+ )
+ self._loop.call_later(_limiter.reset_after, _limiter.lock.release)
+ _limiter.reset_after = 0
+ else:
+ self.ratelimits[bucket] = (
+ Limiter(lock=Lock(loop=self._loop))
+ if version_info < (3, 10)
+ else Limiter(lock=Lock())
+ )
+ _limiter: Limiter = self.ratelimits.get(bucket)
- with Padlock(ratelimit) as lock: # noqa: F841
- kwargs["headers"] = {**self.headers, **kwargs.get("headers", {})}
- kwargs["headers"]["Content-Type"] = "application/json"
+ await _limiter.lock.acquire() # _limiter is the per shared bucket/route endpoint
- if kwargs.get("reason"):
- kwargs["headers"]["X-Audit-Log-Reason"] = kwargs["reason"]
- del kwargs["reason"]
+ # Implement retry logic. The common seems to be 5, so this is hardcoded, for the most part.
- async with self.session.request(
+ for tries in range(5): # 3, 5? 5 seems to be common
+ try:
+ self._check_session()
+ await self._check_lock()
+
+ async with self._session.request(
route.method, route.__api__ + route.path, **kwargs
) as response:
- log.debug(f"{route.method}: {route.__api__ + route.path}: {kwargs}")
+
data = await response.json(content_type=None)
- log.debug(f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}")
- if "X-Ratelimit-Remaining" in response.headers.keys():
- remaining = response.headers["X-Ratelimit-Remaining"]
+ reset_after: float = float(
+ response.headers.get("X-RateLimit-Reset-After", "0.0")
+ )
+ remaining: str = response.headers.get("X-RateLimit-Remaining")
+ _bucket: str = response.headers.get("X-RateLimit-Bucket")
+ is_global: bool = response.headers.get("X-RateLimit-Global", False)
+
+ log.debug(f"{route.method}: {route.__api__ + route.path}: {kwargs}")
+
+ if _bucket is not None:
+ self.buckets[route.endpoint] = _bucket
+ # real-time replacement/update/add if needed.
+
+ if isinstance(data, dict) and data.get("errors"):
+ log.debug(
+ f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}"
+ )
+ # This "redundant" debug line is for debug use and tracing back the error codes.
- if not int(remaining) and response.status != 429:
- time_left = response.headers["X-Ratelimit-Reset-After"]
- self.lock.clear()
+ raise HTTPException(data["code"], message=data["message"])
+ elif remaining and not int(remaining):
+ if response.status == 429:
log.warning(
- f"The HTTP request has reached the maximum threshold. Cooling down for {time_left} seconds."
+ f"The HTTP client has encountered a per-route ratelimit. Locking down future requests for {reset_after} seconds."
)
- await sleep(float(time_left))
- self.lock.set()
- if response.status in (300, 401, 403, 404):
- raise HTTPException(response.status)
- elif response.status == 429:
- retry_after = data["retry_after"]
-
- if "X-Ratelimit-Global" in response.headers.keys():
- self.lock.clear()
+ _limiter.reset_after = reset_after
+ await asyncio.sleep(_limiter.reset_after)
+ continue
+ elif is_global:
log.warning(
- f"The HTTP request has encountered a global API ratelimit. Retrying in {retry_after} seconds."
+ f"The HTTP client has encountered a global ratelimit. Locking down future requests for {reset_after} seconds."
)
- await sleep(retry_after)
- self.lock.set()
- else:
- log.warning(
- f"A local ratelimit with the bucket has been encountered. Retrying in {retry_after} seconds."
+ self._global_lock.reset_after = reset_after
+ self._loop.call_later(
+ self._global_lock.reset_after, self._global_lock.lock.release
)
- await sleep(retry_after)
- continue
- return data
- if response is not None:
- if response.status >= 500:
- raise HTTPException(
- response.status, message="The server had an error processing your request."
- )
+ log.debug(f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}")
+ return data
- raise HTTPException(response.status) # Unknown, unparsed
+ # These account for general/specific exceptions. (Windows...)
+ except OSError as e:
+ if tries < 4 and e.errno in (54, 10054):
+ await asyncio.sleep(2 * tries + 1)
+ continue
+ try:
+ _limiter.lock.release()
+ except RuntimeError:
+ pass
+ raise
+
+ # For generic exceptions we give a traceback for debug reasons.
+ except Exception as e:
+ try:
+ _limiter.lock.release()
+ except RuntimeError:
+ pass
+ log.error("".join(traceback.format_exception(type(e), e, e.__traceback__)))
+ break
+
+ if _limiter.lock.locked():
+ _limiter.lock.release()
async def close(self) -> None:
"""Closes the current session."""
@@ -249,12 +321,15 @@ async def close(self) -> None:
class HTTPClient:
"""
- A WIP class that represents the http Client that handles all major endpoints to Discord API.
+ The user-facing client of the Web API for individual endpoints.
+
+ :ivar str token: The token of the application.
+ :ivar Request _req: The requesting interface for endpoints.
+ :ivar Cache cache: The referenced cache.
"""
token: str
- headers: dict
- _req: Optional[Request]
+ _req: Request
cache: Cache
def __init__(self, token: str):
@@ -276,6 +351,7 @@ async def get_gateway(self) -> str:
async def get_bot_gateway(self) -> Tuple[int, str]:
"""
This calls the BOT Gateway endpoint.
+
:return: A tuple denoting (shard, gateway_url), url from API v9 and JSON encoding
"""
@@ -320,15 +396,16 @@ async def get_self(self) -> dict:
"""
An alias to `get_user`, but only gets the current bot user.
- :return A partial User object of the current bot user in the form of a dictionary.
+ :return: A partial User object of the current bot user in the form of a dictionary.
"""
return await self.get_user()
async def get_user(self, user_id: Optional[int] = None) -> dict:
"""
Gets a user object for a given user ID.
+
:param user_id: A user snowflake ID. If omitted, this defaults to the current bot user.
- :return A partial User object in the form of a dictionary.
+ :return: A partial User object in the form of a dictionary.
"""
if user_id is None:
@@ -342,6 +419,7 @@ async def get_user(self, user_id: Optional[int] = None) -> dict:
async def modify_self(self, payload: dict) -> dict:
"""
Modify the bot user account settings.
+
:param payload: The data to send.
"""
return await self._req.request(Route("PATCH", "/users/@me"), json=payload)
@@ -362,6 +440,7 @@ async def modify_self_nick_in_guild(self, guild_id: int, nickname: Optional[str]
async def create_dm(self, recipient_id: int) -> dict:
"""
Creates a new DM channel with a user.
+
:param recipient_id: User snowflake ID.
:return: Returns a dictionary representing a DM Channel object.
"""
@@ -428,13 +507,15 @@ async def create_message(self, payload: dict, channel_id: int) -> dict:
request = await self._req.request(
Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id), json=payload
)
- self.cache.messages.add(Item(id=request["id"], value=Message(**request)))
+ if request.get("id"):
+ self.cache.messages.add(Item(id=request["id"], value=Message(**request)))
return request
async def get_message(self, channel_id: int, message_id: int) -> Optional[dict]:
"""
Get a specific message in the channel.
+
:param channel_id: the channel this message belongs to
:param message_id: the id of the message
:return: message if it exists.
@@ -447,7 +528,8 @@ async def delete_message(
self, channel_id: int, message_id: int, reason: Optional[str] = None
) -> None:
"""
- Deletes a message from a specified channel
+ Deletes a message from a specified channel.
+
:param channel_id: Channel snowflake ID.
:param message_id: Message snowflake ID.
:param reason: Optional reason to show up in the audit log. Defaults to `None`.
@@ -464,7 +546,8 @@ async def delete_messages(
self, channel_id: int, message_ids: List[int], reason: Optional[str] = None
) -> None:
"""
- Deletes messages from a specified channel
+ Deletes messages from a specified channel.
+
:param channel_id: Channel snowflake ID.
:param message_ids: An array of message snowflake IDs.
:param reason: Optional reason to show up in the audit log. Defaults to `None`.
@@ -497,21 +580,26 @@ async def edit_message(self, channel_id: int, message_id: int, payload: dict) ->
)
async def pin_message(self, channel_id: int, message_id: int) -> None:
- """Pin a message to a channel.
+ """
+ Pin a message to a channel.
+
:param channel_id: Channel ID snowflake.
:param message_id: Message ID snowflake.
"""
return await self._req.request(Route("PUT", f"/channels/{channel_id}/pins/{message_id}"))
async def unpin_message(self, channel_id: int, message_id: int) -> None:
- """Unpin a message to a channel
+ """
+ Unpin a message to a channel.
+
:param channel_id: Channel ID snowflake.
:param message_id: Message ID snowflake.
"""
return await self._req.request(Route("DELETE", f"/channels/{channel_id}/pins/{message_id}"))
async def publish_message(self, channel_id: int, message_id: int) -> dict:
- """Publishes (API calls it crossposts) a message in a News channel to any that is followed by.
+ """
+ Publishes (API calls it crossposts) a message in a News channel to any that is followed by.
:param channel_id: Channel the message is in
:param message_id: The id of the message to publish
@@ -532,13 +620,15 @@ async def get_self_guilds(self) -> list:
request = await self._req.request(Route("GET", "/users/@me/guilds"))
for guild in request:
- self.cache.self_guilds.add(Item(id=guild["id"], value=Guild(**guild)))
+ if guild.get("id"):
+ self.cache.self_guilds.add(Item(id=guild["id"], value=Guild(**guild)))
return request
async def get_guild(self, guild_id: int):
"""
Requests an individual guild from the API.
+
:param guild_id: The guild snowflake ID associated.
:return: The guild object associated, if any.
"""
@@ -550,6 +640,7 @@ async def get_guild(self, guild_id: int):
async def get_guild_preview(self, guild_id: int) -> GuildPreview:
"""
Get a guild's preview.
+
:param guild_id: Guild ID snowflake.
:return: Guild Preview object associated with the snowflake
"""
@@ -557,19 +648,20 @@ async def get_guild_preview(self, guild_id: int) -> GuildPreview:
async def modify_guild(
self, guild_id: int, payload: dict, reason: Optional[str] = None
- ) -> None:
+ ) -> dict:
"""
Modifies a guild's attributes.
- ..note::
- This only sends the payload. You will have to check it when a higher-level function calls this.
-
:param guild_id: Guild ID snowflake.
:param payload: The parameters to change.
:param reason: Reason to send to the audit log, if given.
+ :return: The modified guild object as a dictionary
+ :rtype: dict
"""
- await self._req.request(Route("PATCH", f"/guilds/{guild_id}"), json=payload, reason=reason)
+ return await self._req.request(
+ Route("PATCH", f"/guilds/{guild_id}"), json=payload, reason=reason
+ )
async def leave_guild(self, guild_id: int) -> None:
"""
@@ -593,6 +685,7 @@ async def delete_guild(self, guild_id: int) -> None:
async def get_guild_widget(self, guild_id: int) -> dict:
"""
Returns the widget for the guild.
+
:param guild_id: Guild ID snowflake.
:return: Guild Widget contents as a dict: {"enabled":bool, "channel_id": str}
"""
@@ -609,7 +702,8 @@ async def get_guild_widget_settings(self, guild_id: int) -> dict:
async def get_guild_widget_image(self, guild_id: int, style: Optional[str] = None) -> str:
"""
- Get a url representing a png image widget for the guild.
+ Get an url representing a png image widget for the guild.
+
..note::
See _ for list of styles.
@@ -633,6 +727,7 @@ async def modify_guild_widget(self, guild_id: int, payload: dict) -> dict:
async def get_guild_invites(self, guild_id: int) -> List[Invite]:
"""
Retrieves a list of invite objects with their own metadata.
+
:param guild_id: Guild ID snowflake.
:return: A list of invite objects
"""
@@ -640,7 +735,8 @@ async def get_guild_invites(self, guild_id: int) -> List[Invite]:
async def get_guild_welcome_screen(self, guild_id: int) -> WelcomeScreen:
"""
- Retrieves from the API a welcome screen associated with the guild
+ Retrieves from the API a welcome screen associated with the guild.
+
:param guild_id: Guild ID snowflake.
:return: Welcome Screen object
"""
@@ -685,6 +781,7 @@ async def modify_vanity_code(
async def get_guild_integrations(self, guild_id: int) -> List[dict]:
"""
Gets a list of integration objects associated with the Guild from the API.
+
:param guild_id: Guild ID snowflake.
:return: An array of integration objects
"""
@@ -693,6 +790,7 @@ async def get_guild_integrations(self, guild_id: int) -> List[dict]:
async def delete_guild_integration(self, guild_id: int, integration_id: int) -> None:
"""
Deletes an integration from the guild.
+
:param guild_id: Guild ID snowflake.
:param integration_id: Integration ID snowflake.
"""
@@ -752,7 +850,7 @@ async def create_guild_from_guild_template(
self, template_code: str, name: str, icon: Optional[str] = None
) -> Guild:
"""
- Create a a new guild based on a template.
+ Create a new guild based on a template.
..note::
This endpoint can only be used by bots in less than 10 guilds.
@@ -858,13 +956,15 @@ async def get_all_channels(self, guild_id: int) -> List[dict]:
)
for channel in request:
- self.cache.channels.add(Item(id=channel["id"], value=Channel(**channel)))
+ if channel.get("id"):
+ self.cache.channels.add(Item(id=channel["id"], value=Channel(**channel)))
return request
async def get_all_roles(self, guild_id: int) -> List[dict]:
"""
Gets all roles from a Guild.
+
:param guild_id: Guild ID snowflake
:return: An array of Role objects as dictionaries.
"""
@@ -873,7 +973,8 @@ async def get_all_roles(self, guild_id: int) -> List[dict]:
)
for role in request:
- self.cache.roles.add(Item(id=role["id"], value=Role(**role)))
+ if role.get("id"):
+ self.cache.roles.add(Item(id=role["id"], value=Role(**role)))
return request
@@ -882,6 +983,7 @@ async def create_guild_role(
) -> Role:
"""
Create a new role for the guild.
+
:param guild_id: Guild ID snowflake.
:param data: A dict containing metadata for the role.
:param reason: The reason for this action, if given.
@@ -890,7 +992,8 @@ async def create_guild_role(
request = await self._req.request(
Route("POST", f"/guilds/{guild_id}/roles"), json=data, reason=reason
)
- self.cache.roles.add(Item(id=request["id"], value=Role(**request)))
+ if request.get("id"):
+ self.cache.roles.add(Item(id=request["id"], value=Role(**request)))
return request
@@ -899,6 +1002,7 @@ async def modify_guild_role_position(
) -> List[Role]:
"""
Modify the position of a role in the guild.
+
:param guild_id: Guild ID snowflake.
:param role_id: Role ID snowflake.
:param position: The new position of the associated role.
@@ -916,6 +1020,7 @@ async def modify_guild_role(
) -> Role:
"""
Modify a given role for the guild.
+
:param guild_id: Guild ID snowflake.
:param role_id: Role ID snowflake.
:param data: A dict containing updated metadata for the role.
@@ -929,6 +1034,7 @@ async def modify_guild_role(
async def delete_guild_role(self, guild_id: int, role_id: int, reason: str = None) -> None:
"""
Delete a guild role.
+
:param guild_id: Guild ID snowflake.
:param role_id: Role ID snowflake.
:param reason: The reason for this action, if any.
@@ -964,6 +1070,7 @@ async def create_guild_ban(
) -> None:
"""
Bans a person from the guild, and optionally deletes previous messages sent by them.
+
:param guild_id: Guild ID snowflake
:param user_id: User ID snowflake
:param delete_message_days: Number of days to delete messages, from 0 to 7. Defaults to 0
@@ -981,21 +1088,22 @@ async def remove_guild_ban(
) -> None:
"""
Unbans someone using the API.
+
:param guild_id: Guild ID snowflake
:param user_id: User ID snowflake
:param reason: Optional reason to unban.
"""
return await self._req.request(
- Route(
- "DELETE", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id
- ),
+ Route("DELETE", f"/guilds/{guild_id}/bans/{user_id}"),
+ json={},
reason=reason,
)
async def get_guild_bans(self, guild_id: int) -> List[dict]:
"""
Gets a list of banned users.
+
:param guild_id: Guild ID snowflake.
:return: A list of banned users.
"""
@@ -1004,6 +1112,7 @@ async def get_guild_bans(self, guild_id: int) -> List[dict]:
async def get_user_ban(self, guild_id: int, user_id: int) -> Optional[dict]:
"""
Gets an object pertaining to the user, if it exists. Returns a 404 if it doesn't.
+
:param guild_id: Guild ID snowflake
:param user_id: User ID snowflake.
:return: Ban object if it exists.
@@ -1056,6 +1165,7 @@ async def remove_guild_member(
) -> None:
"""
A low level method of removing a member from a guild. This is different from banning them.
+
:param guild_id: Guild ID snowflake.
:param user_id: User ID snowflake.
:param reason: Reason to send to audit log, if any.
@@ -1069,6 +1179,7 @@ async def get_guild_prune_count(
) -> dict:
"""
Retrieves a dict from an API that results in how many members would be pruned given the amount of days.
+
:param guild_id: Guild ID snowflake.
:param days: Number of days to count. Defaults to ``7``.
:param include_roles: Role IDs to include, if given.
@@ -1087,6 +1198,7 @@ async def get_guild_prune_count(
async def get_member(self, guild_id: int, member_id: int) -> Optional[Member]:
"""
Uses the API to fetch a member from a guild.
+
:param guild_id: Guild ID snowflake.
:param member_id: Member ID snowflake.
:return: A member object, if any.
@@ -1119,7 +1231,7 @@ async def get_list_of_members(
async def search_guild_members(self, guild_id: int, query: str, limit: int = 1) -> List[Member]:
"""
- Search a guild for members who's username or nickname starts with provided string.
+ Search a guild for members whose username or nickname starts with provided string.
:param guild_id: Guild ID snowflake.
:param query: The string to search for
@@ -1175,15 +1287,18 @@ async def remove_member_role(
reason=reason,
)
- async def modify_member(self, user_id: int, guild_id: int, payload: dict):
+ async def modify_member(
+ self, user_id: int, guild_id: int, payload: dict, reason: Optional[str] = None
+ ):
"""
Edits a member.
- This can nick them, change their roles, mute/deafen (and its contrary), and moving them across channels and/or disconnect them
+ This can nick them, change their roles, mute/deafen (and its contrary), and moving them across channels and/or disconnect them.
:param user_id: Member ID snowflake.
:param guild_id: Guild ID snowflake.
:param payload: Payload representing parameters (nick, roles, mute, deaf, channel_id)
- :return: ? (modified voice state? not sure)
+ :param reason: The reason for this action. Defaults to None.
+ :return: Modified member object.
"""
return await self._req.request(
@@ -1191,13 +1306,15 @@ async def modify_member(self, user_id: int, guild_id: int, payload: dict):
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
),
json=payload,
+ reason=reason,
)
# Channel endpoint.
async def get_channel(self, channel_id: int) -> dict:
"""
- Gets a channel by ID. If the channel is a thread, it also includes thread members (and other thread attributes)
+ Gets a channel by ID. If the channel is a thread, it also includes thread members (and other thread attributes).
+
:param channel_id: Channel ID snowflake.
:return: Dictionary of the channel object.
"""
@@ -1261,7 +1378,8 @@ async def get_channel_messages(
)
for message in request:
- self.cache.messages.add(Item(id=message["id"], value=Message(**message)))
+ if message.get("id"):
+ self.cache.messages.add(Item(id=message["id"], value=Message(**message)))
return request
@@ -1282,7 +1400,8 @@ async def create_channel(
request = await self._req.request(
Route("POST", f"/guilds/{guild_id}/channels"), json=payload, reason=reason
)
- self.cache.channels.add(Item(id=request["id"], value=Channel(**request)))
+ if request.get("id"):
+ self.cache.channels.add(Item(id=request["id"], value=Channel(**request)))
return request
@@ -1319,6 +1438,7 @@ async def modify_channel(
) -> Channel:
"""
Update a channel's settings.
+
:param channel_id: Channel ID snowflake.
:param data: Data representing updated settings.
:param reason: Reason, if any.
@@ -1331,6 +1451,7 @@ async def modify_channel(
async def get_channel_invites(self, channel_id: int) -> List[Invite]:
"""
Get the invites for the channel.
+
:param channel_id: Channel ID snowflake.
:return: List of invite objects
"""
@@ -1357,6 +1478,7 @@ async def create_channel_invite(
async def delete_invite(self, invite_code: str, reason: Optional[str] = None) -> dict:
"""
Delete an invite.
+
:param invite_code: The code of the invite to delete
:param reason: Reason to show in the audit log, if any.
:return: The deleted invite object
@@ -1385,6 +1507,7 @@ async def edit_channel_permission(
return await self._req.request(
Route("PUT", f"/channels/{channel_id}/permissions/{overwrite_id}"),
json={"allow": allow, "deny": deny, "type": perm_type},
+ reason=reason,
)
async def delete_channel_permission(
@@ -1407,6 +1530,7 @@ async def trigger_typing(self, channel_id: int) -> None:
..note:
By default, this lib doesn't use this endpoint, however, this is listed for third-party implementation.
+
:param channel_id: Channel ID snowflake.
"""
return await self._req.request(Route("POST", f"/channels/{channel_id}/typing"))
@@ -1414,6 +1538,7 @@ async def trigger_typing(self, channel_id: int) -> None:
async def get_pinned_messages(self, channel_id: int) -> List[Message]:
"""
Get all pinned messages from a channel.
+
:param channel_id: Channel ID snowflake.
:return: A list of pinned message objects.
"""
@@ -1492,6 +1617,7 @@ async def delete_stage_instance(self, channel_id: int, reason: Optional[str] = N
async def join_thread(self, thread_id: int) -> None:
"""
Have the bot user join a thread.
+
:param thread_id: The thread to join.
"""
return await self._req.request(Route("PUT", f"/channels/{thread_id}/thread-members/@me"))
@@ -1499,6 +1625,7 @@ async def join_thread(self, thread_id: int) -> None:
async def leave_thread(self, thread_id: int) -> None:
"""
Have the bot user leave a thread.
+
:param thread_id: The thread to leave.
"""
return await self._req.request(Route("DELETE", f"/channels/{thread_id}/thread-members/@me"))
@@ -1506,6 +1633,7 @@ async def leave_thread(self, thread_id: int) -> None:
async def add_member_to_thread(self, thread_id: int, user_id: int) -> None:
"""
Add another user to a thread.
+
:param thread_id: The ID of the thread
:param user_id: The ID of the user to add
"""
@@ -1516,6 +1644,7 @@ async def add_member_to_thread(self, thread_id: int, user_id: int) -> None:
async def remove_member_from_thread(self, thread_id: int, user_id: int) -> None:
"""
Remove another user from a thread.
+
:param thread_id: The ID of the thread
:param user_id: The ID of the user to remove
"""
@@ -1539,6 +1668,7 @@ async def get_member_from_thread(self, thread_id: int, user_id: int) -> dict:
async def list_thread_members(self, thread_id: int) -> List[dict]:
"""
Get a list of members in the thread.
+
:param thread_id: the id of the thread
:return: a list of thread member objects
"""
@@ -1569,6 +1699,7 @@ async def list_private_archived_threads(
) -> List[dict]:
"""
Get a list of archived private threads in a channel.
+
:param channel_id: The channel to get threads from
:param limit: Optional limit of threads to
:param before: Get threads before this Thread snowflake ID
@@ -1588,6 +1719,7 @@ async def list_joined_private_archived_threads(
) -> List[dict]:
"""
Get a list of archived private threads in a channel that the bot has joined.
+
:param channel_id: The channel to get threads from
:param limit: Optional limit of threads to
:param before: Get threads before this snowflake ID
@@ -1605,6 +1737,7 @@ async def list_joined_private_archived_threads(
async def list_active_threads(self, guild_id: int) -> List[dict]:
"""
List active threads within a guild.
+
:param guild_id: the guild id to get threads from
:return: A list of active threads
"""
@@ -1642,7 +1775,8 @@ async def create_thread(
json=payload,
reason=reason,
)
- self.cache.channels.add(Item(id=request["id"], value=request))
+ if request.get("id"):
+ self.cache.channels.add(Item(id=request["id"], value=request))
return request
payload["type"] = thread_type
@@ -1650,7 +1784,8 @@ async def create_thread(
request = await self._req.request(
Route("POST", f"/channels/{channel_id}/threads"), json=payload, reason=reason
)
- self.cache.channels.add(Item(id=request["id"], value=request))
+ if request.get("id"):
+ self.cache.channels.add(Item(id=request["id"], value=request))
return request
@@ -1659,6 +1794,7 @@ async def create_thread(
async def create_reaction(self, channel_id: int, message_id: int, emoji: str) -> None:
"""
Create a reaction for a message.
+
:param channel_id: Channel snowflake ID.
:param message_id: Message snowflake ID.
:param emoji: The emoji to use (format: `name:id`)
@@ -1676,6 +1812,7 @@ async def create_reaction(self, channel_id: int, message_id: int, emoji: str) ->
async def remove_self_reaction(self, channel_id: int, message_id: int, emoji: str) -> None:
"""
Remove bot user's reaction from a message.
+
:param channel_id: Channel snowflake ID.
:param message_id: Message snowflake ID.
:param emoji: The emoji to remove (format: `name:id`)
@@ -1694,7 +1831,7 @@ async def remove_user_reaction(
self, channel_id: int, message_id: int, emoji: str, user_id: int
) -> None:
"""
- Remove user's reaction from a message
+ Remove user's reaction from a message.
:param channel_id: The channel this is taking place in
:param message_id: The message to remove the reaction on.
@@ -1733,6 +1870,7 @@ async def remove_all_reactions_of_emoji(
) -> None:
"""
Remove all reactions of a certain emoji from a message.
+
:param channel_id: Channel snowflake ID.
:param message_id: Message snowflake ID.
:param emoji: The emoji to remove (format: `name:id`)
@@ -1752,6 +1890,7 @@ async def get_reactions_of_emoji(
) -> List[User]:
"""
Gets the users who reacted to the emoji.
+
:param channel_id: Channel snowflake ID.
:param message_id: Message snowflake ID.
:param emoji: The emoji to get (format: `name:id`)
@@ -1772,6 +1911,7 @@ async def get_reactions_of_emoji(
async def get_sticker(self, sticker_id: int) -> dict:
"""
Get a specific sticker.
+
:param sticker_id: The id of the sticker
:return: Sticker or None
"""
@@ -1780,6 +1920,7 @@ async def get_sticker(self, sticker_id: int) -> dict:
async def list_nitro_sticker_packs(self) -> list:
"""
Gets the list of sticker packs available to Nitro subscribers.
+
:return: List of sticker packs
"""
return await self._req.request(Route("GET", "/sticker-packs"))
@@ -1787,6 +1928,7 @@ async def list_nitro_sticker_packs(self) -> list:
async def list_guild_stickers(self, guild_id: int) -> List[dict]:
"""
Get the stickers for a guild.
+
:param guild_id: The guild to get stickers from
:return: List of Stickers or None
"""
@@ -1795,6 +1937,7 @@ async def list_guild_stickers(self, guild_id: int) -> List[dict]:
async def get_guild_sticker(self, guild_id: int, sticker_id: int) -> dict:
"""
Get a sticker from a guild.
+
:param guild_id: The guild to get stickers from
:param sticker_id: The sticker to get from the guild
:return: Sticker or None
@@ -1806,6 +1949,7 @@ async def create_guild_sticker(
):
"""
Create a new sticker for the guild. Requires the MANAGE_EMOJIS_AND_STICKERS permission.
+
:param payload: the payload to send.
:param guild_id: The guild to create sticker at.
:param reason: The reason for this action.
@@ -1820,6 +1964,7 @@ async def modify_guild_sticker(
):
"""
Modify the given sticker. Requires the MANAGE_EMOJIS_AND_STICKERS permission.
+
:param payload: the payload to send.
:param guild_id: The guild of the target sticker.
:param sticker_id: The sticker to modify.
@@ -1835,6 +1980,7 @@ async def delete_guild_sticker(
) -> None:
"""
Delete the given sticker. Requires the MANAGE_EMOJIS_AND_STICKERS permission.
+
:param guild_id: The guild of the target sticker.
:param sticker_id: The sticker to delete.
:param reason: The reason for this action.
@@ -1852,7 +1998,8 @@ async def get_application_command(
self, application_id: Union[int, Snowflake], guild_id: Optional[int] = None
) -> List[dict]:
"""
- Get all application commands from an application
+ Get all application commands from an application.
+
:param application_id: Application ID snowflake
:param guild_id: Guild to get commands from, if specified. Defaults to global (None)
:return: A list of Application commands.
@@ -1929,7 +2076,7 @@ async def edit_application_command(
application_id, command_id = int(application_id), int(command_id)
r = (
Route(
- "POST",
+ "PATCH",
"/applications/{application_id}/commands/{command_id}",
application_id=application_id,
command_id=command_id,
@@ -1937,7 +2084,7 @@ async def edit_application_command(
if guild_id in (None, "None")
else Route(
"PATCH",
- "/applications/{application_id}/guilds/" "{guild_id}/commands/{command_id}",
+ "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}",
application_id=application_id,
command_id=command_id,
guild_id=guild_id,
@@ -1980,7 +2127,7 @@ async def edit_application_command_permissions(
self, application_id: int, guild_id: int, command_id: int, data: List[dict]
) -> dict:
"""
- Edits permissions for an application command
+ Edits permissions for an application command.
:param application_id: Application ID snowflake
:param guild_id: Guild ID snowflake
@@ -2066,6 +2213,7 @@ async def get_original_interaction_response(
) -> dict:
"""
Gets an existing interaction message.
+
:param token: token
:param application_id: Application ID snowflake.
:param message_id: Message ID snowflake. Defaults to `@original` which represents the initial response msg.
@@ -2081,6 +2229,7 @@ async def edit_interaction_response(
) -> dict:
"""
Edits an existing interaction message, but token needs to be manually called.
+
:param data: A dictionary containing the new response.
:param token: the token of the interaction
:param application_id: Application ID snowflake.
@@ -2098,6 +2247,7 @@ async def delete_interaction_response(
) -> None:
"""
Deletes an existing interaction message.
+
:param token: the token of the interaction
:param application_id: Application ID snowflake.
:param message_id: Message ID snowflake. Defaults to `@original` which represents the initial response msg.
@@ -2113,6 +2263,7 @@ async def delete_interaction_response(
async def _post_followup(self, data: dict, token: str, application_id: str) -> None:
"""
Send a followup to an interaction.
+
:param data: the payload to send
:param application_id: the id of the application
:param token: the token of the interaction
@@ -2129,6 +2280,7 @@ async def _post_followup(self, data: dict, token: str, application_id: str) -> N
async def create_webhook(self, channel_id: int, name: str, avatar: Any = None) -> dict:
"""
Create a new webhook.
+
:param channel_id: Channel ID snowflake.
:param name: Name of the webhook (1-80 characters)
:param avatar: The image for the default webhook avatar, if given.
@@ -2142,6 +2294,7 @@ async def create_webhook(self, channel_id: int, name: str, avatar: Any = None) -
async def get_channel_webhooks(self, channel_id: int) -> List[dict]:
"""
Return a list of channel webhook objects.
+
:param channel_id: Channel ID snowflake.
:return:List of webhook objects
"""
@@ -2150,6 +2303,7 @@ async def get_channel_webhooks(self, channel_id: int) -> List[dict]:
async def get_guild_webhooks(self, guild_id: int) -> List[dict]:
"""
Return a list of guild webhook objects.
+
:param guild_id: Guild ID snowflake
:return: List of webhook objects
@@ -2159,6 +2313,7 @@ async def get_guild_webhooks(self, guild_id: int) -> List[dict]:
async def get_webhook(self, webhook_id: int, webhook_token: str = None) -> dict:
"""
Return the new webhook object for the given id.
+
:param webhook_id: Webhook ID snowflake.
:param webhook_token: Webhook Token, if given.
@@ -2178,6 +2333,7 @@ async def modify_webhook(
) -> dict:
"""
Modify a webhook.
+
:param webhook_id: Webhook ID snowflake
:param name: the default name of the webhook
:param avatar: image for the default webhook avatar
@@ -2195,7 +2351,8 @@ async def modify_webhook(
async def delete_webhook(self, webhook_id: int, webhook_token: str = None):
"""
- Delete a webhook
+ Delete a webhook.
+
:param webhook_id: Webhook ID snowflake.
:param webhook_token: The token for the webhook, if given.
"""
@@ -2330,7 +2487,7 @@ async def delete_original_webhook_message(self, webhook_id: int, webhook_token:
Route("DELETE", f"/webhooks/{webhook_id}/{webhook_token}/messages/@original")
)
- # Emoji endpoints, a subset of guild but it should get it's own thing...
+ # Emoji endpoints, a subset of guild but it should get its own thing...
async def get_all_emoji(self, guild_id: int) -> List[Emoji]:
"""
@@ -2344,6 +2501,7 @@ async def get_all_emoji(self, guild_id: int) -> List[Emoji]:
async def get_guild_emoji(self, guild_id: int, emoji_id: int) -> Emoji:
"""
Gets an emote from a guild.
+
:param guild_id: Guild ID snowflake.
:param emoji_id: Emoji ID snowflake.
:return: Emoji object
@@ -2355,6 +2513,7 @@ async def create_guild_emoji(
) -> Emoji:
"""
Creates an emoji.
+
:param guild_id: Guild ID snowflake.
:param data: Emoji parameters.
:param reason: Optionally, give a reason.
@@ -2369,6 +2528,7 @@ async def modify_guild_emoji(
) -> Emoji:
"""
Modifies an emoji.
+
:param guild_id: Guild ID snowflake.
:param emoji_id: Emoji ID snowflake
:param data: Emoji parameters with updated attributes
@@ -2384,6 +2544,7 @@ async def delete_guild_emoji(
) -> None:
"""
Deletes an emoji.
+
:param guild_id: Guild ID snowflake.
:param emoji_id: Emoji ID snowflake
:param reason: Optionally, give a reason.
@@ -2397,6 +2558,7 @@ async def delete_guild_emoji(
async def create_scheduled_event(self, guild_id: Snowflake, data: dict) -> dict:
"""
Creates a scheduled event.
+
:param guild_id: Guild ID snowflake.
:param data: The dictionary containing the parameters and values to edit the associated event.
:return A dictionary containing the new guild scheduled event object on success.
@@ -2407,13 +2569,16 @@ async def create_scheduled_event(self, guild_id: Snowflake, data: dict) -> dict:
"name",
"privacy_level",
"scheduled_start_time",
+ "scheduled_end_time",
+ "entity_metadata",
"description",
"entity_type",
)
payload = {k: v for k, v in data.items() if k in valid_keys}
return await self._req.request(
- Route("POST", "guilds/{guild_id}/scheduled-events/", guild_id=guild_id), json=payload
+ Route("POST", "guilds/{guild_id}/scheduled-events/", guild_id=int(guild_id)),
+ json=payload,
)
async def get_scheduled_event(
@@ -2421,6 +2586,7 @@ async def get_scheduled_event(
) -> dict:
"""
Gets a guild scheduled event.
+
:param guild_id: Guild ID snowflake.
:param guild_scheduled_event_id: Guild Scheduled Event ID snowflake.
:param with_user_count: A boolean to include number of users subscribed to the associated event, if given.
@@ -2444,6 +2610,7 @@ async def get_scheduled_event(
async def get_scheduled_events(self, guild_id: Snowflake, with_user_count: bool) -> List[dict]:
"""
Gets all guild scheduled events in a guild.
+
:param guild_id: Guild ID snowflake.
:param with_user_count: A boolean to include number of users subscribed to the associated event, if given.
:return A List of a dictionary containing the guild scheduled event objects on success.
@@ -2462,6 +2629,7 @@ async def modify_scheduled_event(
) -> dict:
"""
Modifies a scheduled event.
+
:param guild_id: Guild ID snowflake.
:param guild_scheduled_event_id: Guild Scheduled Event ID snowflake.
:param data: The dictionary containing the parameters and values to edit the associated event.
@@ -2473,6 +2641,8 @@ async def modify_scheduled_event(
"name",
"privacy_level",
"scheduled_start_time",
+ "scheduled_end_time",
+ "entity_metadata",
"description",
"entity_type",
)
@@ -2492,6 +2662,7 @@ async def delete_scheduled_event(
) -> None:
"""
Deletes a guild scheduled event.
+
:param guild_id: Guild ID snowflake.
:param guild_scheduled_event_id: Guild Scheduled Event ID snowflake.
:return Nothing on success.
@@ -2518,6 +2689,7 @@ async def get_scheduled_event_users(
) -> dict:
"""
Get the registered users of a scheduled event.
+
:param guild_id: Guild ID snowflake.
:param guild_scheduled_event_id: Guild Scheduled Event snowflake.
:param limit: Limit of how many users to pull from the event. Defaults to 100.
diff --git a/interactions/api/models/__init__.py b/interactions/api/models/__init__.py
index 4e4a670b1..872948aee 100644
--- a/interactions/api/models/__init__.py
+++ b/interactions/api/models/__init__.py
@@ -6,9 +6,9 @@
models for dispatched Gateway events.
"""
from .channel import * # noqa: F401 F403
+from .flags import * # noqa: F401 F403
from .guild import * # noqa: F401 F403
from .gw import * # noqa: F401 F403
-from .intents import * # noqa: F401 F403
from .member import * # noqa: F401 F403
from .message import * # noqa: F401 F403
from .misc import * # noqa: F401 F403
diff --git a/interactions/api/models/channel.py b/interactions/api/models/channel.py
index 38b59488a..700ce5caa 100644
--- a/interactions/api/models/channel.py
+++ b/interactions/api/models/channel.py
@@ -1,5 +1,6 @@
from datetime import datetime
from enum import IntEnum
+from typing import List, Optional
from .misc import DictSerializerMixin, Snowflake
@@ -67,7 +68,18 @@ class ThreadMember(DictSerializerMixin):
:ivar int flags: The bitshift flags for the member in the thread.
"""
- __slots__ = ("_json", "id", "user_id", "join_timestamp", "flags")
+ __slots__ = (
+ "_json",
+ "id",
+ "user_id",
+ "join_timestamp",
+ "flags",
+ # TODO: Document below attributes.
+ "user",
+ "team_id",
+ "membership_state",
+ "permissions",
+ )
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -144,6 +156,10 @@ class Channel(DictSerializerMixin):
"member",
"default_auto_archive_duration",
"permissions",
+ "_client",
+ # TODO: Document banner when Discord officially documents them.
+ "banner",
+ "guild_hashes",
)
def __init__(self, **kwargs):
@@ -164,3 +180,331 @@ def __init__(self, **kwargs):
if self._json.get("last_pin_timestamp")
else None
)
+
+ async def send(
+ self,
+ content: Optional[str] = None,
+ *,
+ tts: Optional[bool] = False,
+ # attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
+ embeds=None,
+ allowed_mentions=None,
+ components=None,
+ ):
+ """
+ Sends a message in the channel.
+
+ :param content?: The contents of the message as a string or string-converted value.
+ :type content: Optional[str]
+ :param tts?: Whether the message utilizes the text-to-speech Discord programme or not.
+ :type tts: Optional[bool]
+ :param embeds?: An embed, or list of embeds for the message.
+ :type embeds: Optional[Union[Embed, List[Embed]]]
+ :param allowed_mentions?: The message interactions/mention limits that the message can refer to.
+ :type allowed_mentions: Optional[MessageInteraction]
+ :param components?: A component, or list of components for the message.
+ :type components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]]
+ :return: The sent message as an object.
+ :rtype: Message
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from ...models.component import ActionRow, Button, SelectMenu
+ from .message import Message
+
+ _content: str = "" if content is None else content
+ _tts: bool = False if tts is None else tts
+ # _file = None if file is None else file
+ # _attachments = [] if attachments else None
+ _embeds: list = []
+ _allowed_mentions: dict = {} if allowed_mentions is None else allowed_mentions
+ _components: List[dict] = [{"type": 1, "components": []}]
+ if embeds:
+ if isinstance(embeds, list):
+ _embeds = [embed._json for embed in embeds]
+ else:
+ _embeds = [embeds._json]
+
+ # TODO: Break this obfuscation pattern down to a "builder" method.
+ if components:
+ if isinstance(components, list) and all(
+ isinstance(action_row, ActionRow) for action_row in components
+ ):
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in action_row.components
+ ],
+ }
+ for action_row in components
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(component, (Button, SelectMenu)) for component in components
+ ):
+ for component in components:
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in component._json["options"]
+ ]
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components
+ ],
+ }
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(action_row, (list, ActionRow)) for action_row in components
+ ):
+ _components = []
+ for action_row in components:
+ for component in (
+ action_row if isinstance(action_row, list) else action_row.components
+ ):
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ option._json for option in component.options
+ ]
+ _components.append(
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id")
+ or component._json.get("url")
+ else []
+ )
+ for component in (
+ action_row
+ if isinstance(action_row, list)
+ else action_row.components
+ )
+ ],
+ }
+ )
+ elif isinstance(components, ActionRow):
+ _components[0]["components"] = [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components.components
+ ]
+ elif isinstance(components, Button):
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ elif isinstance(components, SelectMenu):
+ components._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in components._json["options"]
+ ]
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ else:
+ _components = []
+
+ # TODO: post-v4: Add attachments into Message obj.
+ payload = Message(
+ content=_content,
+ tts=_tts,
+ # file=file,
+ # attachments=_attachments,
+ embeds=_embeds,
+ allowed_mentions=_allowed_mentions,
+ components=_components,
+ )
+
+ res = await self._client.create_message(channel_id=int(self.id), payload=payload._json)
+ return Message(**res, _client=self._client)
+
+ async def delete(self) -> None:
+ """
+ Deletes the channel.
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.delete_channel(channel_id=int(self.id))
+
+ async def modify(
+ self,
+ name: Optional[str] = None,
+ topic: Optional[str] = None,
+ bitrate: Optional[int] = None,
+ user_limit: Optional[int] = None,
+ rate_limit_per_user: Optional[int] = None,
+ position: Optional[int] = None,
+ # permission_overwrites,
+ parent_id: Optional[int] = None,
+ nsfw: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> "Channel":
+ """
+ Edits the channel.
+
+ :param name?: The name of the channel, defaults to the current value of the channel
+ :type name: str
+ :param topic?: The topic of that channel, defaults to the current value of the channel
+ :type topic: Optional[str]
+ :param bitrate?: (voice channel only) The bitrate (in bits) of the voice channel, defaults to the current value of the channel
+ :type bitrate Optional[int]
+ :param user_limit?: (voice channel only) Maximum amount of users in the channel, defaults to the current value of the channel
+ :type user_limit: Optional[int]
+ :param rate_limit_per_use?: Amount of seconds a user has to wait before sending another message (0-21600), defaults to the current value of the channel
+ :type rate_limit_per_user: Optional[int]
+ :param position?: Sorting position of the channel, defaults to the current value of the channel
+ :type position: Optional[int]
+ :param parent_id?: The id of the parent category for a channel, defaults to the current value of the channel
+ :type parent_id: Optional[int]
+ :param nsfw?: Whether the channel is nsfw or not, defaults to the current value of the channel
+ :type nsfw: Optional[bool]
+ :param reason: The reason for the edit
+ :type reason: Optional[str]
+ :return: The modified channel as new object
+ :rtype: Channel
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ _name = self.name if not name else name
+ _topic = self.topic if not topic else topic
+ _bitrate = self.bitrate if not bitrate else bitrate
+ _user_limit = self.user_limit if not user_limit else user_limit
+ _rate_limit_per_user = (
+ self.rate_limit_per_user if not rate_limit_per_user else rate_limit_per_user
+ )
+ _position = self.position if not position else position
+ _parent_id = self.parent_id if not parent_id else parent_id
+ _nsfw = self.nsfw if not nsfw else nsfw
+ _type = self.type
+
+ payload = Channel(
+ name=_name,
+ type=_type,
+ topic=_topic,
+ bitrate=_bitrate,
+ user_limit=_user_limit,
+ rate_limit_per_user=_rate_limit_per_user,
+ position=_position,
+ parent_id=_parent_id,
+ nsfw=_nsfw,
+ )
+ res = await self._client.modify_channel(
+ channel_id=int(self.id),
+ reason=reason,
+ data=payload._json,
+ )
+ return Channel(**res, _client=self._client)
+
+ async def add_member(
+ self,
+ member_id: int,
+ ) -> None:
+ """
+ This adds a member to the channel, if the channel is a thread.
+
+ :param member_id: The id of the member to add to the channel
+ :type member_id: int
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if not self.thread_metadata:
+ raise TypeError(
+ "The Channel you specified is not a thread!"
+ ) # TODO: Move to new error formatter.
+ await self._client.add_member_to_thread(thread_id=int(self.id), user_id=member_id)
+
+ async def pin_message(
+ self,
+ message_id: int,
+ ) -> None:
+ """
+ Pins a message to the channel.
+
+ :param message_id: The id of the message to pin
+ :type message_id: int
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+
+ await self._client.pin_message(channel_id=int(self.id), message_id=message_id)
+
+ async def unpin_message(
+ self,
+ message_id: int,
+ ) -> None:
+ """
+ Unpins a message from the channel.
+
+ :param message_id: The id of the message to unpin
+ :type message_id: int
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+
+ await self._client.unpin_message(channel_id=int(self.id), message_id=message_id)
+
+ async def publish_message(
+ self,
+ message_id: int,
+ ):
+ """Publishes (API calls it crossposts) a message in the channel to any that is followed by.
+
+ :param message_id: The id of the message to publish
+ :type message_id: int
+ :return: The message published
+ :rtype: Message
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from .message import Message
+
+ res = await self._client.publish_message(
+ channel_id=int(self.id), message_id=int(message_id)
+ )
+ return Message(**res, _client=self._client)
+
+ async def get_pinned_messages(self):
+ """
+ Get all pinned messages from the channel.
+
+ :return: A list of pinned message objects.
+ :rtype: List[Message]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from .message import Message
+
+ res = await self._client.get_pinned_messages(int(self.id))
+ messages = [Message(**message, _client=self._client) for message in res]
+ return messages
+
+
+class Thread(Channel):
+ """An object representing a thread.
+
+ .. note::
+ This is a derivation of the base Channel, since a
+ thread can be its own event.
+ """
+
+ ...
diff --git a/interactions/api/models/channel.pyi b/interactions/api/models/channel.pyi
index af7fe21cf..5377ef639 100644
--- a/interactions/api/models/channel.pyi
+++ b/interactions/api/models/channel.pyi
@@ -1,9 +1,12 @@
from datetime import datetime
from enum import IntEnum
-from typing import List, Optional
+from typing import List, Optional, Union
+from .message import Message, Embed, MessageInteraction
+from ...models.component import ActionRow, Button, SelectMenu
from .misc import DictSerializerMixin, Overwrite, Snowflake
from .user import User
+from ..http import HTTPClient
class ChannelType(IntEnum): ...
@@ -26,6 +29,7 @@ class ThreadMember(DictSerializerMixin):
class Channel(DictSerializerMixin):
_json: dict
+ _client: HTTPClient
id: Snowflake
type: ChannelType
guild_id: Optional[Snowflake]
@@ -53,3 +57,47 @@ class Channel(DictSerializerMixin):
default_auto_archive_duration: Optional[int]
permissions: Optional[str]
def __init__(self, **kwargs): ...
+ async def send(
+ self,
+ content: Optional[str] = None,
+ *,
+ tts: Optional[bool] = False,
+ # attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
+ embeds: Optional[Union[Embed, List[Embed]]] = None,
+ allowed_mentions: Optional[MessageInteraction] = None,
+ components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]] = None,
+ ) -> Message: ...
+ async def delete(self) -> None: ...
+ async def modify(
+ self,
+ name: Optional[str] = None,
+ topic: Optional[str] = None,
+ bitrate: Optional[int] = None,
+ user_limit: Optional[int] = None,
+ rate_limit_per_user: Optional[int] = None,
+ position: Optional[int] = None,
+ # permission_overwrites,
+ parent_id: Optional[int] = None,
+ nsfw: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> "Channel": ...
+ async def add_member(
+ self,
+ member_id: int,
+ ) -> None: ...
+ async def pin_message(
+ self,
+ message_id: int,
+ ) -> None: ...
+ async def unpin_message(
+ self,
+ message_id: int,
+ ) -> None: ...
+ async def publish_message(
+ self,
+ message_id: int,
+ ) -> Message: ...
+ async def get_pinned_messages(self) -> List[Message]: ...
+
+
+class Thread(Channel): ...
diff --git a/interactions/api/models/flags.py b/interactions/api/models/flags.py
new file mode 100644
index 000000000..d7ea08b0b
--- /dev/null
+++ b/interactions/api/models/flags.py
@@ -0,0 +1,87 @@
+from enum import IntFlag
+
+
+class Intents(IntFlag):
+ """An integer flag bitshift object representing flags respective for each gateway intent type."""
+
+ GUILDS = 1 << 0
+ GUILD_MEMBERS = 1 << 1
+ GUILD_BANS = 1 << 2
+ GUILD_EMOJIS_AND_STICKERS = 1 << 3
+ GUILD_INTEGRATIONS = 1 << 4
+ GUILD_WEBHOOKS = 1 << 5
+ GUILD_INVITES = 1 << 6
+ GUILD_VOICE_STATES = 1 << 7
+ GUILD_PRESENCES = 1 << 8
+ GUILD_MESSAGES = 1 << 9
+ GUILD_MESSAGE_REACTIONS = 1 << 10
+ GUILD_MESSAGE_TYPING = 1 << 11
+ DIRECT_MESSAGES = 1 << 12
+ DIRECT_MESSAGE_REACTIONS = 1 << 13
+ DIRECT_MESSAGE_TYPING = 1 << 14
+ GUILD_SCHEDULED_EVENTS = 1 << 16
+
+ PRIVILEGED = GUILD_PRESENCES | GUILD_MEMBERS
+ DEFAULT = (
+ GUILDS
+ | GUILD_BANS
+ | GUILD_EMOJIS_AND_STICKERS
+ | GUILD_INTEGRATIONS
+ | GUILD_WEBHOOKS
+ | GUILD_INVITES
+ | GUILD_VOICE_STATES
+ | GUILD_MESSAGES
+ | GUILD_MESSAGE_REACTIONS
+ | GUILD_MESSAGE_TYPING
+ | DIRECT_MESSAGES
+ | DIRECT_MESSAGE_REACTIONS
+ | DIRECT_MESSAGE_TYPING
+ | GUILD_SCHEDULED_EVENTS
+ )
+ ALL = DEFAULT | PRIVILEGED
+
+
+class Permissions(IntFlag):
+ """An integer flag bitshift object representing the different member permissions given by Discord."""
+
+ CREATE_INSTANT_INVITE = 1 << 0
+ KICK_MEMBERS = 1 << 1
+ BAN_MEMBERS = 1 << 2
+ ADMINISTRATOR = 1 << 3
+ MANAGE_CHANNELS = 1 << 4
+ MANAGE_GUILD = 1 << 5
+ ADD_REACTIONS = 1 << 6
+ VIEW_AUDIT_LOG = 1 << 7
+ PRIORITY_SPEAKER = 1 << 8
+ STREAM = 1 << 9
+ VIEW_CHANNEL = 1 << 10
+ SEND_MESSAGES = 1 << 11
+ SEND_TTS_MESSAGES = 1 << 12
+ MANAGE_MESSAGES = 1 << 13
+ EMBED_LINKS = 1 << 14
+ ATTACH_FILES = 1 << 15
+ READ_MESSAGE_HISTORY = 1 << 16
+ MENTION_EVERYONE = 1 << 17
+ USE_EXTERNAL_EMOJIS = 1 << 18
+ VIEW_GUILD_INSIGHTS = 1 << 19
+ CONNECT = 1 << 20
+ SPEAK = 1 << 21
+ MUTE_MEMBERS = 1 << 22
+ DEAFEN_MEMBERS = 1 << 23
+ MOVE_MEMBERS = 1 << 24
+ USE_VAD = 1 << 25
+ CHANGE_NICKNAME = 1 << 26
+ MANAGE_NICKNAMES = 1 << 27
+ MANAGE_ROLES = 1 << 28
+ MANAGE_WEBHOOKS = 1 << 29
+ MANAGE_EMOJIS_AND_STICKERS = 1 << 30
+ USE_APPLICATION_COMMANDS = 1 << 31
+ REQUEST_TO_SPEAK = 1 << 32
+ MANAGE_EVENTS = 1 << 33
+ MANAGE_THREADS = 1 << 34
+ CREATE_PUBLIC_THREADS = 1 << 35
+ CREATE_PRIVATE_THREADS = 1 << 36
+ USE_EXTERNAL_STICKERS = 1 << 37
+ SEND_MESSAGES_IN_THREADS = 1 << 38
+ START_EMBEDDED_ACTIVITIES = 1 << 39
+ MODERATE_MEMBERS = 1 << 40
diff --git a/interactions/api/models/guild.py b/interactions/api/models/guild.py
index 2b2b70b1a..62559bddf 100644
--- a/interactions/api/models/guild.py
+++ b/interactions/api/models/guild.py
@@ -1,12 +1,59 @@
from datetime import datetime
+from enum import IntEnum
+from typing import List, Optional, Union
+from .channel import Channel, ChannelType
+from .member import Member
from .message import Emoji, Sticker
from .misc import DictSerializerMixin, Snowflake
from .presence import PresenceActivity
+from .role import Role
from .team import Application
from .user import User
+class VerificationLevel(IntEnum):
+ """An enumerable object representing the verification level of a guild."""
+
+ NONE = 0
+ LOW = 1
+ MEDIUM = 2
+ HIGH = 3
+ VERY_HIGH = 4
+
+
+class ExplicitContentFilterLevel(IntEnum):
+ """An enumerable object representing the explicit content filter level of a guild."""
+
+ DISABLED = 0
+ MEMBERS_WITHOUT_ROLES = 1
+ ALL_MEMBERS = 2
+
+
+class DefaultMessageNotificationLevel(IntEnum):
+ """An enumerable object representing the default message notification level of a guild."""
+
+ ALL_MESSAGES = 0
+ ONLY_MENTIONS = 1
+
+
+class EntityType(IntEnum):
+ """An enumerable object representing the type of event."""
+
+ STAGE_INSTANCE = 1
+ VOICE = 2
+ EXTERNAL = 3
+
+
+class EventStatus(IntEnum):
+ """An enumerable object representing the status of an event."""
+
+ SCHEDULED = 1
+ ACTIVE = 2
+ COMPLETED = 3
+ CANCELED = 4
+
+
class WelcomeChannels(DictSerializerMixin):
"""
A class object representing a welcome channel on the welcome screen.
@@ -38,7 +85,7 @@ class WelcomeScreen(DictSerializerMixin):
We assume it's for the welcome screen topic.
- :ivar Optional[str] description?: The description of the welcome sceen.
+ :ivar Optional[str] description?: The description of the welcome screen.
:ivar List[WelcomeChannels] welcome_channels: A list of welcome channels of the welcome screen.
"""
@@ -55,7 +102,7 @@ def __init__(self, **kwargs):
class StageInstance(DictSerializerMixin):
"""
- A class object representing an instace of a stage channel in a guild.
+ A class object representing an instance of a stage channel in a guild.
:ivar Snowflake id: The ID of the stage.
:ivar Snowflake guild_id: The guild ID the stage is in.
@@ -119,6 +166,9 @@ class Guild(DictSerializerMixin):
:ivar Optional[bool] large?: Whether the guild is considered "large."
:ivar Optional[bool] unavailable?: Whether the guild is unavailable to access.
:ivar Optional[int] member_count?: The amount of members in the guild.
+ :ivar Optional[List[Member]] members?: The members in the guild.
+ :ivar Optional[List[Channel]] channels?: The channels in the guild.
+ :ivar Optional[List[Thread]] threads?: All known threads in the guild.
:ivar Optional[List[PresenceUpdate]] presences?: The list of presences in the guild.
:ivar Optional[int] max_presences?: The maximum amount of presences allowed in the guild.
:ivar Optional[int] max_members?: The maximum amount of members allowed in the guild.
@@ -141,6 +191,7 @@ class Guild(DictSerializerMixin):
__slots__ = (
"_json",
"id",
+ "_client",
"name",
"icon",
"icon_hash",
@@ -235,6 +286,885 @@ def __init__(self, **kwargs):
if self._json.get("stickers")
else None
)
+ self.members = (
+ [Member(**member, _client=self._client) for member in self.members]
+ if self._json.get("members")
+ else None
+ )
+ if not self.members and self._client:
+
+ if (
+ not len(self._client.cache.self_guilds.view) > 1
+ or not self._client.cache.self_guilds.values[str(self.id)].members
+ ):
+ pass
+ else:
+ members = self._client.cache.self_guilds.values[str(self.id)].members
+ if all(isinstance(member, Member) for member in members):
+ self.members = members
+ else:
+ self.members = [Member(**member, _client=self._client) for member in members]
+
+ async def ban(
+ self,
+ member_id: int,
+ reason: Optional[str] = None,
+ delete_message_days: Optional[int] = 0,
+ ) -> None:
+ """
+ Bans a member from the guild.
+
+ :param member_id: The id of the member to ban
+ :type member_id: int
+ :param reason?: The reason of the ban
+ :type reason: Optional[str]
+ :param delete_message_days?: Number of days to delete messages, from 0 to 7. Defaults to 0
+ :type delete_message_days: Optional[int]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.create_guild_ban(
+ guild_id=int(self.id),
+ user_id=member_id,
+ reason=reason,
+ delete_message_days=delete_message_days,
+ )
+
+ async def remove_ban(
+ self,
+ user_id: int,
+ reason: Optional[str] = None,
+ ) -> None:
+ """
+ Removes the ban of a user.
+
+ :param user_id: The id of the user to remove the ban from
+ :type user_id: int
+ :param reason?: The reason for the removal of the ban
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.remove_guild_ban(
+ guild_id=int(self.id),
+ user_id=user_id,
+ reason=reason,
+ )
+
+ async def kick(
+ self,
+ member_id: int,
+ reason: Optional[str] = None,
+ ) -> None:
+ """
+ Kicks a member from the guild.
+
+ :param member_id: The id of the member to kick
+ :type member_id: int
+ :param reason?: The reason for the kick
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.create_guild_kick(
+ guild_id=int(self.id),
+ user_id=member_id,
+ reason=reason,
+ )
+
+ async def add_member_role(
+ self,
+ role: Union[Role, int],
+ member_id: int,
+ reason: Optional[str],
+ ) -> None:
+ """
+ This method adds a role to a member.
+
+ :param role: The role to add. Either ``Role`` object or role_id
+ :type role Union[Role, int]
+ :param member_id: The id of the member to add the roles to
+ :type member_id: int
+ :param reason?: The reason why the roles are added
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if isinstance(role, Role):
+ await self._client.add_member_role(
+ guild_id=int(self.id),
+ user_id=member_id,
+ role_id=int(role.id),
+ reason=reason,
+ )
+ else:
+ await self._client.add_member_role(
+ guild_id=int(self.id),
+ user_id=member_id,
+ role_id=role,
+ reason=reason,
+ )
+
+ async def remove_member_role(
+ self,
+ role: Union[Role, int],
+ member_id: int,
+ reason: Optional[str],
+ ) -> None:
+ """
+ This method removes a or multiple role(s) from a member.
+
+ :param role: The role to remove. Either ``Role`` object or role_id
+ :type role: Union[Role, int]
+ :param member_id: The id of the member to remove the roles from
+ :type member_id: int
+ :param reason?: The reason why the roles are removed
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if isinstance(role, Role):
+ await self._client.remove_member_role(
+ guild_id=int(self.id),
+ user_id=member_id,
+ role_id=int(role.id),
+ reason=reason,
+ )
+ else:
+ await self._client.remove_member_role(
+ guild_id=int(self.id),
+ user_id=member_id,
+ role_id=role,
+ reason=reason,
+ )
+
+ async def create_role(
+ self,
+ name: str,
+ # permissions,
+ color: Optional[int] = 0,
+ hoist: Optional[bool] = False,
+ # icon,
+ # unicode_emoji,
+ mentionable: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> Role:
+ """
+ Creates a new role in the guild.
+
+ :param name: The name of the role
+ :type name: str
+ :param color?: RGB color value as integer, default ``0``
+ :type color: Optional[int]
+ :param hoist?: Whether the role should be displayed separately in the sidebar, default ``False``
+ :type hoist: Optional[bool]
+ :param mentionable?: Whether the role should be mentionable, default ``False``
+ :type mentionable: Optional[bool]
+ :param reason?: The reason why the role is created, default ``None``
+ :type reason: Optional[str]
+ :return: The created Role
+ :rtype: Role
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ payload = Role(
+ name=name,
+ color=color,
+ hoist=hoist,
+ mentionable=mentionable,
+ )
+ res = await self._client.create_guild_role(
+ guild_id=int(self.id),
+ reason=reason,
+ data=payload._json,
+ )
+ return Role(**res, _client=self._client)
+
+ async def get_member(
+ self,
+ member_id: int,
+ ) -> Member:
+ """
+ Searches for the member with specified id in the guild and returns the member as member object.
+
+ :param member_id: The id of the member to search for
+ :type member_id: int
+ :return: The member searched for
+ :rtype: Member
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = await self._client.get_member(
+ guild_id=int(self.id),
+ member_id=member_id,
+ )
+ return Member(**res, _client=self._client)
+
+ async def delete_channel(
+ self,
+ channel_id: int,
+ ) -> None:
+ """
+ Deletes a channel from the guild.
+
+ :param channel_id: The id of the channel to delete
+ :type channel_id: int
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.delete_channel(
+ channel_id=channel_id,
+ )
+
+ async def delete_role(
+ self,
+ role_id: int,
+ reason: Optional[str] = None,
+ ) -> None:
+ """
+ Deletes a role from the guild.
+
+ :param role_id: The id of the role to delete
+ :type role_id: int
+ :param reason?: The reason of the deletion
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.delete_guild_role(
+ guild_id=int(self.id),
+ role_id=role_id,
+ reason=reason,
+ )
+
+ async def modify_role(
+ self,
+ role_id: int,
+ name: Optional[str] = None,
+ # permissions,
+ color: Optional[int] = None,
+ hoist: Optional[bool] = None,
+ # icon,
+ # unicode_emoji,
+ mentionable: Optional[bool] = None,
+ reason: Optional[str] = None,
+ ) -> Role:
+ """
+ Edits a role in the guild.
+
+ :param role_id: The id of the role to edit
+ :type role_id: int
+ :param name?: The name of the role, defaults to the current value of the role
+ :type name: Optional[str]
+ :param color?: RGB color value as integer, defaults to the current value of the role
+ :type color: Optional[int]
+ :param hoist?: Whether the role should be displayed separately in the sidebar, defaults to the current value of the role
+ :type hoist: Optional[bool]
+ :param mentionable?: Whether the role should be mentionable, defaults to the current value of the role
+ :type mentionable: Optional[bool]
+ :param reason?: The reason why the role is edited, default ``None``
+ :type reason: Optional[str]
+ :return: The modified role object
+ :rtype: Role
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ roles = await self._client.get_all_roles(guild_id=int(self.id))
+ for i in roles:
+ if int(i["id"]) == role_id:
+ role = Role(**i)
+ break
+ _name = role.name if not name else name
+ _color = role.color if not color else color
+ _hoist = role.hoist if not hoist else hoist
+ _mentionable = role.mentionable if mentionable is None else mentionable
+
+ payload = Role(name=_name, color=_color, hoist=_hoist, mentionable=_mentionable)
+
+ res = await self._client.modify_guild_role(
+ guild_id=int(self.id),
+ role_id=role_id,
+ data=payload._json,
+ reason=reason,
+ )
+ return Role(**res, _client=self._client)
+
+ async def create_channel(
+ self,
+ name: str,
+ type: ChannelType,
+ topic: Optional[str] = None,
+ bitrate: Optional[int] = None,
+ user_limit: Optional[int] = None,
+ rate_limit_per_user: Optional[int] = 0,
+ position: Optional[int] = None,
+ # permission_overwrites,
+ parent_id: Optional[int] = None,
+ nsfw: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> Channel:
+ """
+ Creates a channel in the guild.
+
+ :param name: The name of the channel
+ :type name: str
+ :param type: The type of the channel
+ :type type: ChannelType
+ :param topic?: The topic of that channel
+ :type topic: Optional[str]
+ :param bitrate?: (voice channel only) The bitrate (in bits) of the voice channel
+ :type bitrate Optional[int]
+ :param user_limit?: (voice channel only) Maximum amount of users in the channel
+ :type user_limit: Optional[int]
+ :param rate_limit_per_use?: Amount of seconds a user has to wait before sending another message (0-21600)
+ :type rate_limit_per_user: Optional[int]
+ :param position?: Sorting position of the channel
+ :type position: Optional[int]
+ :param parent_id?: The id of the parent category for a channel
+ :type parent_id: Optional[int]
+ :param nsfw?: Whether the channel is nsfw or not, default ``False``
+ :type nsfw: Optional[bool]
+ :param reason: The reason for the creation
+ :type reason: Optional[str]
+ :return: The created channel
+ :rtype: Channel
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if type in [
+ ChannelType.DM,
+ ChannelType.DM.value,
+ ChannelType.GROUP_DM,
+ ChannelType.GROUP_DM.value,
+ ]:
+ raise ValueError(
+ "ChannelType must not be a direct-message when creating Guild Channels!" # TODO: move to custom error formatter
+ )
+
+ payload = Channel(
+ name=name,
+ type=type,
+ topic=topic,
+ bitrate=bitrate,
+ user_limit=user_limit,
+ rate_limit_per_user=rate_limit_per_user,
+ position=position,
+ parent_id=parent_id,
+ nsfw=nsfw,
+ )
+
+ res = await self._client.create_channel(
+ guild_id=int(self.id),
+ reason=reason,
+ payload=payload._json,
+ )
+
+ return Channel(**res, _client=self._client)
+
+ async def modify_channel(
+ self,
+ channel_id: int,
+ name: Optional[str] = None,
+ topic: Optional[str] = None,
+ bitrate: Optional[int] = None,
+ user_limit: Optional[int] = None,
+ rate_limit_per_user: Optional[int] = None,
+ position: Optional[int] = None,
+ # permission_overwrites,
+ parent_id: Optional[int] = None,
+ nsfw: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> Channel:
+ """
+ Edits a channel of the guild.
+
+ :param channel_id: The id of the channel to modify
+ :type channel_id: int
+ :param name?: The name of the channel, defaults to the current value of the channel
+ :type name: str
+ :param topic?: The topic of that channel, defaults to the current value of the channel
+ :type topic: Optional[str]
+ :param bitrate?: (voice channel only) The bitrate (in bits) of the voice channel, defaults to the current value of the channel
+ :type bitrate Optional[int]
+ :param user_limit?: (voice channel only) Maximum amount of users in the channel, defaults to the current value of the channel
+ :type user_limit: Optional[int]
+ :param rate_limit_per_use?: Amount of seconds a user has to wait before sending another message (0-21600), defaults to the current value of the channel
+ :type rate_limit_per_user: Optional[int]
+ :param position?: Sorting position of the channel, defaults to the current value of the channel
+ :type position: Optional[int]
+ :param parent_id?: The id of the parent category for a channel, defaults to the current value of the channel
+ :type parent_id: Optional[int]
+ :param nsfw?: Whether the channel is nsfw or not, defaults to the current value of the channel
+ :type nsfw: Optional[bool]
+ :param reason: The reason for the edit
+ :type reason: Optional[str]
+ :return: The modified channel
+ :rtype: Channel
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ ch = Channel(**await self._client.get_channel(channel_id=channel_id))
+
+ _name = ch.name if not name else name
+ _topic = ch.topic if not topic else topic
+ _bitrate = ch.bitrate if not bitrate else bitrate
+ _user_limit = ch.user_limit if not user_limit else user_limit
+ _rate_limit_per_user = (
+ ch.rate_limit_per_user if not rate_limit_per_user else rate_limit_per_user
+ )
+ _position = ch.position if not position else position
+ _parent_id = ch.parent_id if not parent_id else parent_id
+ _nsfw = ch.nsfw if not nsfw else nsfw
+ _type = ch.type
+
+ payload = Channel(
+ name=_name,
+ type=_type,
+ topic=_topic,
+ bitrate=_bitrate,
+ user_limit=_user_limit,
+ rate_limit_per_user=_rate_limit_per_user,
+ position=_position,
+ parent_id=_parent_id,
+ nsfw=_nsfw,
+ )
+
+ res = await self._client.modify_channel(
+ channel_id=channel_id,
+ reason=reason,
+ data=payload._json,
+ )
+ return Channel(**res, _client=self._client)
+
+ async def modify_member(
+ self,
+ member_id: int,
+ nick: Optional[str] = None,
+ roles: Optional[List[int]] = None,
+ mute: Optional[bool] = None,
+ deaf: Optional[bool] = None,
+ channel_id: Optional[int] = None,
+ communication_disabled_until: Optional[datetime.isoformat] = None,
+ reason: Optional[str] = None,
+ ) -> Member:
+ """
+ Modifies a member of the guild.
+
+ :param member_id: The id of the member to modify
+ :type member_id: int
+ :param nick?: The nickname of the member
+ :type nick: Optional[str]
+ :param roles?: A list of all role ids the member has
+ :type roles: Optional[List[int]]
+ :param mute?: whether the user is muted in voice channels
+ :type mute: Optional[bool]
+ :param deaf?: whether the user is deafened in voice channels
+ :type deaf: Optional[bool]
+ :param channel_id?: id of channel to move user to (if they are connected to voice)
+ :type channel_id: Optional[int]
+ :param communication_disabled_until?: when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future)
+ :type communication_disabled_until: Optional[datetime.isoformat]
+ :param reason?: The reason of the modifying
+ :type reason: Optional[str]
+ :return: The modified member
+ :rtype: Member
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ payload = {}
+ if nick:
+ payload["nick"] = nick
+
+ if roles:
+ payload["roles"] = roles
+
+ if channel_id:
+ payload["channel_id"] = channel_id
+
+ if mute:
+ payload["mute"] = mute
+
+ if deaf:
+ payload["deaf"] = deaf
+
+ if communication_disabled_until:
+ payload["communication_disabled_until"] = communication_disabled_until
+
+ res = await self._client.modify_member(
+ user_id=member_id,
+ guild_id=int(self.id),
+ payload=payload,
+ reason=reason,
+ )
+ return Member(**res, _client=self._client)
+
+ async def get_preview(self) -> "GuildPreview":
+
+ """
+ Get the guild's preview.
+
+ :return: the guild preview as object
+ :rtype: GuildPreview
+ """
+
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+
+ return GuildPreview(**await self._client.get_guild_preview(guild_id=int(self.id)))
+
+ async def leave(self) -> None:
+ """Removes the bot from the guild."""
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.leave_guild(guild_id=int(self.id))
+
+ async def modify(
+ self,
+ name: Optional[str] = None,
+ verification_level: Optional[VerificationLevel] = None,
+ default_message_notifications: Optional[DefaultMessageNotificationLevel] = None,
+ explicit_content_filter: Optional[ExplicitContentFilterLevel] = None,
+ afk_channel_id: Optional[int] = None,
+ afk_timeout: Optional[int] = None,
+ # icon, TODO: implement images
+ owner_id: Optional[int] = None,
+ # splash, TODO: implement images
+ # discovery_splash, TODO: implement images
+ # banner, TODO: implement images
+ system_channel_id: Optional[int] = None,
+ suppress_join_notifications: Optional[bool] = None,
+ suppress_premium_subscriptions: Optional[bool] = None,
+ suppress_guild_reminder_notifications: Optional[bool] = None,
+ suppress_join_notification_replies: Optional[bool] = None,
+ rules_channel_id: Optional[int] = None,
+ public_updates_channel_id: Optional[int] = None,
+ preferred_locale: Optional[str] = None,
+ description: Optional[str] = None,
+ premium_progress_bar_enabled: Optional[bool] = None,
+ reason: Optional[str] = None,
+ ) -> "Guild":
+ """
+ Modifies the current guild.
+
+ :param name?: The new name of the guild
+ :type name: Optional[str]
+ :param verification_level?: The verification level of the guild
+ :type verification_level: Optional[VerificationLevel]
+ :param default_message_notifications?: The default message notification level for members
+ :type default_message_notifications: Optional[DefaultMessageNotificationLevel]
+ :param explicit_content_filter?: The explicit content filter level for media content
+ :type explicit_content_filter: Optional[ExplicitContentFilterLevel]
+ :param afk_channel_id?: The id for the afk voice channel
+ :type afk_channel_id: Optional[int]
+ :param afk_timeout?: Afk timeout in seconds
+ :type afk_timeout: Optional[int]
+ :param owner_id?: The id of the user to transfer the guild ownership to. You must be the owner to perform this
+ :type owner_id: Optional[int]
+ :param system_channel_id?: The id of the channel where guild notices such as welcome messages and boost events are posted
+ :type system_channel_id: Optional[int]
+ :param suppress_join_notifications?: Whether to suppress member join notifications in the system channel or not
+ :type suppress_join_notifications: Optional[bool]
+ :param suppress_premium_subscriptions?: Whether to suppress server boost notifications in the system channel or not
+ :type suppress_premium_subscriptions: Optional[bool]
+ :param suppress_guild_reminder_notifications?: Whether to suppress server setup tips in the system channel or not
+ :type suppress_guild_reminder_notifications: Optional[bool]
+ :param suppress_join_notification_replies?: Whether to hide member join sticker reply buttons in the system channel or not
+ :type suppress_join_notification_replies: Optional[bool]
+ :param rules_channel_id?: The id of the channel where guilds display rules and/or guidelines
+ :type rules_channel_id: Optional[int]
+ :param public_updates_channel_id?: The id of the channel where admins and moderators of community guilds receive notices from Discord
+ :type public_updates_channel_id: Optional[int]
+ :param preferred_locale?: The preferred locale of a community guild used in server discovery and notices from Discord; defaults to "en-US"
+ :type preferred_locale: Optional[str]
+ :param description?: The description for the guild, if the guild is discoverable
+ :type description: Optional[str]
+ :param premium_progress_bar_enabled?: Whether the guild's boost progress bar is enabled
+ :type premium_progress_bar_enabled: Optional[bool]
+ :param reason?: The reason for the modifying
+ :type reason: Optional[str]
+ :return: The modified guild
+ :rtype: Guild
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if (
+ suppress_join_notifications is None
+ and suppress_premium_subscriptions is None
+ and suppress_guild_reminder_notifications is None
+ and suppress_join_notification_replies is None
+ ):
+ system_channel_flags = None
+ else:
+ _suppress_join_notifications = (1 << 0) if suppress_join_notifications else 0
+ _suppress_premium_subscriptions = (1 << 1) if suppress_premium_subscriptions else 0
+ _suppress_guild_reminder_notifications = (
+ (1 << 2) if suppress_guild_reminder_notifications else 0
+ )
+ _suppress_join_notification_replies = (
+ (1 << 3) if suppress_join_notification_replies else 0
+ )
+ system_channel_flags = (
+ _suppress_join_notifications
+ | _suppress_premium_subscriptions
+ | _suppress_guild_reminder_notifications
+ | _suppress_join_notification_replies
+ )
+
+ payload = {}
+
+ if name:
+ payload["name"] = name
+ if verification_level:
+ payload["verification_level"] = verification_level.value
+ if default_message_notifications:
+ payload["default_message_notifications"] = default_message_notifications.value
+ if explicit_content_filter:
+ payload["explicit_content_filter"] = explicit_content_filter.value
+ if afk_channel_id:
+ payload["afk_channel_id"] = afk_channel_id
+ if afk_timeout:
+ payload["afk_timeout"] = afk_timeout
+ if owner_id:
+ payload["owner_id"] = owner_id
+ if system_channel_id:
+ payload["system_channel_id"] = system_channel_id
+ if system_channel_flags:
+ payload["system_channel_flags"] = system_channel_flags
+ if rules_channel_id:
+ payload["rules_channel_id"] = rules_channel_id
+ if public_updates_channel_id:
+ payload["public_updates_channel_id"] = rules_channel_id
+ if preferred_locale:
+ payload["preferred_locale"] = preferred_locale
+ if description:
+ payload["description"] = description
+ if premium_progress_bar_enabled:
+ payload["premium_progress_bar_enabled"] = premium_progress_bar_enabled
+
+ res = await self._client.modify_guild(
+ guild_id=int(self.id),
+ payload=payload,
+ reason=reason,
+ )
+ return Guild(**res, _client=self._client)
+
+ async def create_scheduled_event(
+ self,
+ name: str,
+ entity_type: EntityType,
+ scheduled_start_time: datetime.isoformat,
+ scheduled_end_time: Optional[datetime.isoformat] = None,
+ entity_metadata: Optional["EventMetadata"] = None,
+ channel_id: Optional[int] = None,
+ description: Optional[str] = None,
+ # privacy_level, TODO: implement when more levels available
+ ) -> "ScheduledEvents":
+ """
+ creates a scheduled event for the guild.
+
+ :param name: The name of the event
+ :type name: str
+ :param entity_type: The entity type of the scheduled event
+ :type entity_type: EntityType
+ :param scheduled_start_time: The time to schedule the scheduled event
+ :type scheduled_start_time: datetime.isoformat
+ :param scheduled_end_time?: The time when the scheduled event is scheduled to end
+ :type scheduled_end_time: Optional[datetime.isoformat]
+ :param entity_metadata?: The entity metadata of the scheduled event
+ :type entity_metadata: Optional[EventMetadata]
+ :param channel_id?: The channel id of the scheduled event.
+ :type channel_id: Optional[int]
+ :param description?: The description of the scheduled event
+ :type description: Optional[str]
+ :return: The created event
+ :rtype: ScheduledEvents
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if entity_type != EntityType.EXTERNAL and not channel_id:
+ raise ValueError(
+ "channel_id is required when entity_type is not external!"
+ ) # TODO: replace with custom error formatter
+ if entity_type == EntityType.EXTERNAL and not entity_metadata:
+ raise ValueError(
+ "entity_metadata is required for external events!"
+ ) # TODO: replace with custom error formatter
+
+ payload = {}
+
+ payload["name"] = name
+ payload["entity_type"] = entity_type.value
+ payload["scheduled_start_time"] = scheduled_start_time
+ payload["privacy_level"] = 2
+ if scheduled_end_time:
+ payload["scheduled_end_time"] = scheduled_end_time
+ if entity_metadata:
+ payload["entity_metadata"] = entity_metadata
+ if channel_id:
+ payload["channel_id"] = channel_id
+ if description:
+ payload["description"] = description
+
+ res = await self._client.create_scheduled_event(
+ guild_id=self.id,
+ data=payload,
+ )
+ return ScheduledEvents(**res)
+
+ async def modify_scheduled_event(
+ self,
+ event_id: int,
+ name: Optional[str] = None,
+ entity_type: Optional[EntityType] = None,
+ scheduled_start_time: Optional[datetime.isoformat] = None,
+ scheduled_end_time: Optional[datetime.isoformat] = None,
+ entity_metadata: Optional["EventMetadata"] = None,
+ channel_id: Optional[int] = None,
+ description: Optional[str] = None,
+ status: Optional[EventStatus] = None,
+ # privacy_level, TODO: implement when more levels available
+ ) -> "ScheduledEvents":
+ """
+ Edits a scheduled event of the guild.
+
+ :param event_id: The id of the event to edit
+ :type event_id: int
+ :param name: The name of the event
+ :type name: Optional[str]
+ :param entity_type: The entity type of the scheduled event
+ :type entity_type: Optional[EntityType]
+ :param scheduled_start_time: The time to schedule the scheduled event
+ :type scheduled_start_time: Optional[datetime.isoformat]
+ :param scheduled_end_time?: The time when the scheduled event is scheduled to end
+ :type scheduled_end_time: Optional[datetime.isoformat]
+ :param entity_metadata?: The entity metadata of the scheduled event
+ :type entity_metadata: Optional[EventMetadata]
+ :param channel_id?: The channel id of the scheduled event.
+ :type channel_id: Optional[int]
+ :param description?: The description of the scheduled event
+ :type description: Optional[str]
+ :param status?: The status of the scheduled event
+ :type status: Optional[EventStatus]
+ :return: The modified event
+ :rtype: ScheduledEvents
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if entity_type == EntityType.EXTERNAL and not entity_metadata:
+ raise ValueError(
+ "entity_metadata is required for external events!"
+ ) # TODO: replace with custom error formatter
+ if entity_type == EntityType.EXTERNAL and not scheduled_end_time:
+ raise ValueError(
+ "External events require an end time!"
+ ) # TODO: replace with custom error formatter
+
+ payload = {}
+ if name:
+ payload["name"] = name
+ if channel_id:
+ payload["channel_id"] = channel_id
+ if scheduled_start_time:
+ payload["scheduled_start_time"] = scheduled_start_time
+ if entity_type:
+ payload["entity_type"] = entity_type.value
+ payload["channel_id"] = None
+ if scheduled_end_time:
+ payload["scheduled_end_time"] = scheduled_end_time
+ if entity_metadata:
+ payload["entity_metadata"] = entity_metadata
+ if description:
+ payload["description"] = description
+ if status:
+ payload["status"] = status
+
+ res = await self._client.modify_scheduled_event(
+ guild_id=self.id,
+ guild_scheduled_event_id=Snowflake(event_id),
+ data=payload,
+ )
+ return ScheduledEvents(**res)
+
+ async def delete_scheduled_event(self, event_id: int) -> None:
+ """
+ Deletes a scheduled event of the guild.
+
+ :param event_id: The id of the event to delete
+ :type event_id: int
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.delete_scheduled_event(
+ guild_id=self.id,
+ guild_scheduled_event_id=Snowflake(event_id),
+ )
+
+ async def get_all_channels(self) -> List[Channel]:
+ """
+ Gets all channels of the guild as list.
+
+ :return: The channels of the guild.
+ :rtype: List[Channel]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = self._client.get_all_channels(int(self.id))
+ channels = [Channel(**channel, _client=self._client) for channel in res]
+ return channels
+
+ async def get_all_roles(self) -> List[Role]:
+ """
+ Gets all roles of the guild as list.
+
+ :return: The roles of the guild.
+ :rtype: List[Role]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = self._client.get_all_roles(int(self.id))
+ roles = [Role(**role, _client=self._client) for role in res]
+ return roles
+
+ async def modify_role_position(
+ self,
+ role_id: Union[Role, int],
+ position: int,
+ reason: Optional[str] = None,
+ ) -> List[Role]:
+ """
+ Modifies the position of a role in the guild.
+
+ :param role_id: The id of the role to modify the position of
+ :type role_id: Union[Role, int]
+ :param position: The new position of the role
+ :type position: int
+ :param reason?: The reason for the modifying
+ :type reason: Optional[str]
+ :return: List of guild roles with updated hierarchy
+ :rtype: List[Role]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ _role_id = role_id.id if isinstance(role_id, Role) else role_id
+ res = await self._client.modify_guild_role_position(
+ guild_id=int(self.id), position=position, role_id=_role_id, reason=reason
+ )
+ roles = [Role(**role, _client=self._client) for role in res]
+ return roles
+
+ async def get_bans(self) -> List[dict]:
+ """
+ Gets a list of banned users.
+
+ :return: List of banned users with reasons
+ :rtype: List[dict]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = await self._client.get_guild_bans(int(self.id))
+ for ban in res:
+ ban["user"] = User(**ban["user"])
+ return res
class GuildPreview(DictSerializerMixin):
@@ -337,7 +1267,22 @@ class Invite(DictSerializerMixin):
:ivar datetime created_at: The time when this invite was created.
"""
- __slots__ = ("_json", "uses", "max_uses", "max_age", "temporary", "created_at")
+ __slots__ = (
+ "_json",
+ "_client",
+ "uses",
+ "max_uses",
+ "max_age",
+ "temporary",
+ "created_at",
+ # TODO: Investigate their purposes and document.
+ "types",
+ "inviter",
+ "guild_id",
+ "expires_at",
+ "code",
+ "channel_id",
+ )
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -417,7 +1362,7 @@ class ScheduledEvents(DictSerializerMixin):
:ivar Snowflake id: The ID of the scheduled event.
:ivar Snowflake guild_id: The ID of the guild that this scheduled event belongs to.
- :ivar Optional[Snowflake] channel_id?: The channel ID in wich the scheduled event belongs to, if any.
+ :ivar Optional[Snowflake] channel_id?: The channel ID in which the scheduled event belongs to, if any.
:ivar Optional[Snowflake] creator_id?: The ID of the user that created the scheduled event.
:ivar str name: The name of the scheduled event.
:ivar str description: The description of the scheduled event.
@@ -429,6 +1374,8 @@ class ScheduledEvents(DictSerializerMixin):
:ivar Optional[EventMetadata] entity_metadata?: Additional metadata associated with the scheduled event.
:ivar Optional[User] creator?: The user that created the scheduled event.
:ivar Optional[int] user_count?: The number of users subscribed to the scheduled event.
+ :ivar int status: The status of the scheduled event
+ :ivar Optional[str] image: The hash containing the image of an event, if applicable.
"""
__slots__ = (
@@ -447,6 +1394,8 @@ class ScheduledEvents(DictSerializerMixin):
"entity_metadata",
"creator",
"user_count",
+ "status",
+ "image",
)
def __init__(self, **kwargs):
diff --git a/interactions/api/models/guild.pyi b/interactions/api/models/guild.pyi
index 5fa979dae..1ae419a6c 100644
--- a/interactions/api/models/guild.pyi
+++ b/interactions/api/models/guild.pyi
@@ -1,14 +1,25 @@
from datetime import datetime
-from enum import Enum
-from typing import Any, List, Optional
+from typing import Any, List, Optional, Union
+from enum import IntEnum
-from .channel import Channel
+from .channel import Channel, ChannelType, Thread
from .member import Member
from .message import Emoji, Sticker
from .misc import DictSerializerMixin, Snowflake
from .presence import PresenceUpdate
from .role import Role
from .user import User
+from ..http import HTTPClient
+
+class VerificationLevel(IntEnum): ...
+
+class ExplicitContentFilterLevel(IntEnum): ...
+
+class DefaultMessageNotificationLevel(IntEnum): ...
+
+class EntityType(IntEnum): ...
+
+class EventStatus(IntEnum): ...
class WelcomeChannels(DictSerializerMixin):
_json: dict
@@ -36,6 +47,7 @@ class StageInstance(DictSerializerMixin):
class Guild(DictSerializerMixin):
_json: dict
+ _client: HTTPClient
id: Snowflake
name: str
icon: Optional[str]
@@ -66,7 +78,7 @@ class Guild(DictSerializerMixin):
member_count: Optional[int]
members: Optional[List[Member]]
channels: Optional[List[Channel]]
- threads: Optional[List[Channel]] # threads, because of their metadata
+ threads: Optional[List[Thread]] # threads, because of their metadata
presences: Optional[List[PresenceUpdate]]
max_presences: Optional[int]
max_members: Optional[int]
@@ -96,6 +108,172 @@ class Guild(DictSerializerMixin):
lazy: Any
application_command_counts: Any
def __init__(self, **kwargs): ...
+ async def ban(
+ self,
+ member_id: int,
+ reason: Optional[str] = None,
+ delete_message_days: Optional[int] = 0,
+ ) -> None: ...
+ async def remove_ban(
+ self,
+ user_id: int,
+ reason: Optional[str] = None,
+ ) -> None: ...
+ async def kick(
+ self,
+ member_id: int,
+ reason: Optional[str] = None,
+ ) -> None: ...
+ async def add_member_role(
+ self,
+ role: Union[Role, int],
+ member_id: int,
+ reason: Optional[str],
+ ) -> None: ...
+ async def remove_member_role(
+ self,
+ role: Union[Role, int],
+ member_id: int,
+ reason: Optional[str],
+ ) -> None: ...
+ async def create_role(
+ self,
+ name: str,
+ # permissions,
+ color: Optional[int] = 0,
+ hoist: Optional[bool] = False,
+ # icon,
+ # unicode_emoji,
+ mentionable: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> Role: ...
+ async def get_member(
+ self,
+ member_id: int,
+ ) -> Member: ...
+ async def delete_channel(
+ self,
+ channel_id: int,
+ ) -> None: ...
+ async def delete_role(
+ self,
+ role_id: int,
+ reason: Optional[str],
+ ) -> None: ...
+ async def modify_role(
+ self,
+ role_id: int,
+ name: Optional[str] = None,
+ # permissions,
+ color: Optional[int] = None,
+ hoist: Optional[bool] = None,
+ # icon,
+ # unicode_emoji,
+ mentionable: Optional[bool] = None,
+ reason: Optional[str] = None,
+ ) -> Role: ...
+ async def create_channel(
+ self,
+ name: str,
+ type: ChannelType,
+ topic: Optional[str] = None,
+ bitrate: Optional[int] = None,
+ user_limit: Optional[int] = None,
+ rate_limit_per_user: Optional[int] = 0,
+ position: Optional[int] = None,
+ # permission_overwrites,
+ parent_id: Optional[int] = None,
+ nsfw: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> Channel: ...
+ async def modify_channel(
+ self,
+ channel_id: int,
+ name: Optional[str] = None,
+ topic: Optional[str] = None,
+ bitrate: Optional[int] = None,
+ user_limit: Optional[int] = None,
+ rate_limit_per_user: Optional[int] = None,
+ position: Optional[int] = None,
+ # permission_overwrites,
+ parent_id: Optional[int] = None,
+ nsfw: Optional[bool] = False,
+ reason: Optional[str] = None,
+ ) -> Channel: ...
+ async def modify_member(
+ self,
+ member_id: int,
+ nick: Optional[str] = None,
+ roles: Optional[List[int]] = None,
+ mute: Optional[bool] = None,
+ deaf: Optional[bool] = None,
+ channel_id: Optional[int] = None,
+ communication_disabled_until: Optional[datetime.isoformat] = None,
+ reason: Optional[str] = None,
+ ) -> Member: ...
+ async def get_preview(self) -> GuildPreview: ...
+ async def leave(self) -> None: ...
+ async def modify(
+ self,
+ name: Optional[str] = None,
+ verification_level: Optional[VerificationLevel] = None,
+ default_message_notifications: Optional[DefaultMessageNotificationLevel] = None,
+ explicit_content_filter: Optional[ExplicitContentFilterLevel] = None,
+ afk_channel_id: Optional[int] = None,
+ afk_timeout: Optional[int] = None,
+ # icon, TODO: implement images
+ owner_id: Optional[int] = None,
+ # splash, TODO: implement images
+ # discovery_splash, TODO: implement images
+ # banner, TODO: implement images
+ system_channel_id: Optional[int] = None,
+ suppress_join_notifications: Optional[bool] = None,
+ suppress_premium_subscriptions: Optional[bool] = None,
+ suppress_guild_reminder_notifications: Optional[bool] = None,
+ suppress_join_notification_replies: Optional[bool] = None,
+ rules_channel_id: Optional[int] = None,
+ public_updates_channel_id: Optional[int] = None,
+ preferred_locale: Optional[str] = None,
+ description: Optional[str] = None,
+ premium_progress_bar_enabled: Optional[bool] = None,
+ reason: Optional[str] = None,
+ ) -> "Guild": ...
+ async def create_scheduled_event(
+ self,
+ name: str,
+ entity_type: EntityType,
+ scheduled_start_time: datetime.isoformat,
+ scheduled_end_time: Optional[datetime.isoformat] = None,
+ entity_metadata: Optional["EventMetadata"] = None,
+ channel_id: Optional[int] = None,
+ description: Optional[str] = None,
+ # privacy_level, TODO: implement when more levels available
+ ) -> "ScheduledEvents": ...
+ async def modify_scheduled_event(
+ self,
+ event_id: int,
+ name: Optional[str] = None,
+ entity_type: Optional[EntityType] = None,
+ scheduled_start_time: Optional[datetime.isoformat] = None,
+ scheduled_end_time: Optional[datetime.isoformat] = None,
+ entity_metadata: Optional["EventMetadata"] = None,
+ channel_id: Optional[int] = None,
+ description: Optional[str] = None,
+ # privacy_level, TODO: implement when more levels available
+ ) -> "ScheduledEvents": ...
+ async def delete_scheduled_event(
+ self,
+ event_id: int
+ ) -> None: ...
+ async def get_all_channels(self) -> List[Channel]: ...
+ async def get_all_roles(self) -> List[Role]: ...
+ async def modify_role_position(
+ self,
+ role_id: Union[Role, int],
+ position: int,
+ reason: Optional[str] = None,
+ ) -> List[Role]: ...
+ async def get_bans(self) -> List[dict]: ...
class GuildPreview(DictSerializerMixin):
_json: dict
@@ -112,6 +290,12 @@ class GuildPreview(DictSerializerMixin):
class Invite(DictSerializerMixin):
_json: dict
+ _client: HTTPClient
+ type: str
+ guild_id: Snowflake
+ expires_at: str
+ code: str
+ channel_id: Snowflake
uses: int
max_uses: int
max_age: int
@@ -155,4 +339,6 @@ class ScheduledEvents(DictSerializerMixin):
entity_metadata: Optional[EventMetadata]
creator: Optional[User]
user_count: Optional[int]
+ status: int
+ image: Optional[str]
def __init__(self, **kwargs): ...
diff --git a/interactions/api/models/gw.py b/interactions/api/models/gw.py
index f351677ce..3e58be70d 100644
--- a/interactions/api/models/gw.py
+++ b/interactions/api/models/gw.py
@@ -32,6 +32,37 @@ def __init__(self, **kwargs):
)
+class EmbeddedActivity(DictSerializerMixin):
+ """
+ A class object representing the event ``EMBEDDED_ACTIVITY_UPDATE``.
+
+ .. note::
+ This is entirely undocumented by the API.
+
+ :ivar List[Snowflake] users: The list of users of the event.
+ :ivar Snowflake guild_id: The guild ID of the event.
+ :ivar PresenceActivity embedded_activity: The embedded presence activity of the associated event.
+ :ivar Snowflake channel_id: The channel ID of the event.
+ """
+
+ __slots__ = ("_json", "users", "guild_id", "embedded_activity", "channel_id")
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.users = (
+ [Snowflake(user) for user in self._json.get("users")]
+ if self._json.get("users")
+ else None
+ )
+ self.guild_id = Snowflake(self.guild_id) if self._json.get("guild_id") else None
+ self.embedded_activity = (
+ PresenceActivity(**self.embedded_activity)
+ if self._json.get("embedded_activity")
+ else None
+ )
+ self.channel_id = Snowflake(self.channel_id) if self._json.get("channel_id") else None
+
+
class GuildBan(DictSerializerMixin):
"""
A class object representing the gateway event ``GUILD_BAN_ADD``.
@@ -40,7 +71,7 @@ class GuildBan(DictSerializerMixin):
:ivar User user: The user of the event.
"""
- __slots__ = ("_json", "guild_id", "user")
+ __slots__ = ("_json", "guild_id", "user", "_client")
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -80,6 +111,23 @@ def __init__(self, **kwargs):
self.guild_id = Snowflake(self.guild_id) if self._json.get("guild_id") else None
+class GuildJoinRequest(DictSerializerMixin):
+ """
+ A class object representing the gateway events ``GUILD_JOIN_REQUEST_CREATE``, ``GUILD_JOIN_REQUEST_UPDATE``, and ``GUILD_JOIN_REQUEST_DELETE``
+
+ .. note::
+ This is entirely undocumented by the API.
+
+ :ivar Snowflake user_id: The user ID of the event.
+ :ivar Snowflake guild_id: The guild ID of the event.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.user_id = Snowflake(self.user_id) if self._json.get("user_id") else None
+ self.guild_id = Snowflake(self.guild_id) if self._json.get("guild_id") else None
+
+
class GuildMember(DictSerializerMixin):
"""
A class object representing the gateway events ``GUILD_MEMBER_ADD``, ``GUILD_MEMBER_UPDATE`` and ``GUILD_MEMBER_REMOVE``.
@@ -105,6 +153,8 @@ class GuildMember(DictSerializerMixin):
"avatar",
"joined_at",
"premium_since",
+ "is_pending", # TODO: investigate what this is.
+ "_client",
"communication_disabled_until", # TODO: investigate what this is.
"deaf",
"mute",
@@ -174,7 +224,14 @@ class GuildRole(DictSerializerMixin):
:ivar Optional[Snowflake] role_id?: The role ID of the event.
"""
- __slots__ = ("_json", "guild_id", "role", "role_id")
+ __slots__ = (
+ "_json",
+ "guild_id",
+ "role",
+ "role_id",
+ "_client",
+ "guild_hashes", # TODO: investigate what this is.
+ )
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -272,7 +329,6 @@ class Presence(DictSerializerMixin):
def __init__(self, **kwargs):
super().__init__(**kwargs)
- print(self._json)
self.guild_id = Snowflake(self.guild_id) if self._json.get("guild_id") else None
self.user = User(**self.user) if self._json.get("user") else None
self.activities = (
@@ -285,7 +341,7 @@ def __init__(self, **kwargs):
)
-class Reaction(DictSerializerMixin):
+class MessageReaction(DictSerializerMixin):
"""
A class object representing the gateway event ``MESSAGE_REACTION_ADD``.
@@ -309,7 +365,7 @@ def __init__(self, **kwargs):
self.emoji = Emoji(**self.emoji) if self._json.get("emoji") else None
-class ReactionRemove(Reaction):
+class ReactionRemove(MessageReaction):
"""
A class object representing the gateway events ``MESSAGE_REACTION_REMOVE``, ``MESSAGE_REACTION_REMOVE_ALL`` and ``MESSAGE_REACTION_REMOVE_EMOJI``.
diff --git a/interactions/api/models/gw.pyi b/interactions/api/models/gw.pyi
index a717c0316..2d393f953 100644
--- a/interactions/api/models/gw.pyi
+++ b/interactions/api/models/gw.pyi
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import List, Optional
+from typing import List, Optional, Any
from .channel import Channel, ThreadMember
from .member import Member
@@ -8,6 +8,8 @@ from .misc import ClientStatus, DictSerializerMixin, Snowflake
from .presence import PresenceActivity
from .role import Role
from .user import User
+from .team import Application
+from ..http import HTTPClient
class ChannelPins(DictSerializerMixin):
_json: dict
@@ -16,10 +18,19 @@ class ChannelPins(DictSerializerMixin):
last_pin_timestamp: Optional[datetime]
def __init__(self, **kwargs): ...
+class EmbeddedActivity(DictSerializerMixin):
+ _json: dict
+ users: List[Snowflake]
+ guild_id: Snowflake
+ embedded_activity: PresenceActivity
+ channel_id: Snowflake
+ def __init__(self, **kwargs): ...
+
class GuildBan(DictSerializerMixin):
_json: dict
guild_id: Snowflake
user: User
+ _client: Optional[HTTPClient]
def __init__(self, **kwargs): ...
class GuildEmojis(DictSerializerMixin):
@@ -33,6 +44,12 @@ class GuildIntegrations(DictSerializerMixin):
guild_id: Snowflake
def __init__(self, **kwargs): ...
+class GuildJoinRequest(DictSerializerMixin):
+ _json: dict
+ user_id: Snowflake
+ guild_id: Snowflake
+ def __init__(self, **kwargs): ...
+
class GuildMember(DictSerializerMixin):
_json: dict
guild_id: Snowflake
@@ -45,6 +62,7 @@ class GuildMember(DictSerializerMixin):
deaf: Optional[bool]
mute: Optional[bool]
pending: Optional[bool]
+ _client: Optional[HTTPClient]
def __init__(self, **kwargs): ...
class GuildMembers(DictSerializerMixin):
@@ -54,9 +72,7 @@ class GuildMembers(DictSerializerMixin):
chunk_index: int
chunk_count: int
not_found: Optional[list]
- presences: Optional[
- List["Presence"]
- ]
+ presences: Optional[List["Presence"]]
nonce: Optional[str]
def __init__(self, **kwargs): ...
@@ -65,6 +81,7 @@ class GuildRole(DictSerializerMixin):
guild_id: Snowflake
role: Role
role_id: Optional[Snowflake]
+ _client: Optional[HTTPClient]
def __init__(self, **kwargs): ...
class GuildStickers(DictSerializerMixin):
@@ -73,7 +90,26 @@ class GuildStickers(DictSerializerMixin):
stickers: List[Sticker]
def __init__(self, **kwargs): ...
-class Integration(DictSerializerMixin): ...
+class Integration(DictSerializerMixin):
+ _json: dict
+ id: Snowflake
+ name: str
+ type: str
+ enabled: bool
+ syncing: bool
+ role_id: Snowflake
+ enable_emoticons: bool
+ expire_behavior: int
+ expire_grace_period: int
+ user: User
+ account: Any
+ synced_at: datetime
+ subscriber_count: int
+ revoked: bool
+ application: Application
+ guild_id: Snowflake
+
+ def __init__(self, **kwargs): ...
class Presence(DictSerializerMixin):
_json: dict
@@ -83,7 +119,7 @@ class Presence(DictSerializerMixin):
activities: List[PresenceActivity]
client_status: ClientStatus
-class Reaction(DictSerializerMixin):
+class MessageReaction(DictSerializerMixin):
# There's no official data model for this, so this is pseudo for the most part here.
_json: dict
user_id: Optional[Snowflake]
@@ -94,7 +130,7 @@ class Reaction(DictSerializerMixin):
emoji: Optional[Emoji]
def __init__(self, **kwargs): ...
-class ReactionRemove(Reaction):
+class ReactionRemove(MessageReaction):
# typehinting already subclassed
def __init__(self, **kwargs): ...
diff --git a/interactions/api/models/intents.py b/interactions/api/models/intents.py
deleted file mode 100644
index 3e5eb2e73..000000000
--- a/interactions/api/models/intents.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from enum import IntFlag
-
-
-class Intents(IntFlag):
- """An integer flag bitshift object representing flags respective for each gateway intent type."""
-
- GUILDS = 1 << 0
- GUILD_MEMBERS = 1 << 1
- GUILD_BANS = 1 << 2
- GUILD_EMOJIS_AND_STICKERS = 1 << 3
- GUILD_INTEGRATIONS = 1 << 4
- GUILD_WEBHOOKS = 1 << 5
- GUILD_INVITES = 1 << 6
- GUILD_VOICE_STATES = 1 << 7
- GUILD_PRESENCES = 1 << 8
- GUILD_MESSAGES = 1 << 9
- GUILD_MESSAGE_REACTIONS = 1 << 10
- GUILD_MESSAGE_TYPING = 1 << 11
- DIRECT_MESSAGES = 1 << 12
- DIRECT_MESSAGE_REACTIONS = 1 << 13
- DIRECT_MESSAGE_TYPING = 1 << 14
- GUILD_SCHEDULED_EVENTS = 1 << 16
-
- PRIVILEGED = GUILD_PRESENCES | GUILD_MEMBERS
- DEFAULT = (
- GUILDS
- | GUILD_BANS
- | GUILD_EMOJIS_AND_STICKERS
- | GUILD_INTEGRATIONS
- | GUILD_WEBHOOKS
- | GUILD_INVITES
- | GUILD_VOICE_STATES
- | GUILD_MESSAGES
- | GUILD_MESSAGE_REACTIONS
- | GUILD_MESSAGE_TYPING
- | DIRECT_MESSAGES
- | DIRECT_MESSAGE_REACTIONS
- | DIRECT_MESSAGE_TYPING
- | GUILD_SCHEDULED_EVENTS
- )
- ALL = DEFAULT | PRIVILEGED
diff --git a/interactions/api/models/member.py b/interactions/api/models/member.py
index 07fdd6368..4837ab04d 100644
--- a/interactions/api/models/member.py
+++ b/interactions/api/models/member.py
@@ -1,6 +1,9 @@
from datetime import datetime
+from typing import List, Optional, Union
+from .flags import Permissions
from .misc import DictSerializerMixin
+from .role import Role
from .user import User
@@ -22,7 +25,7 @@ class Member(DictSerializerMixin):
:ivar bool deaf: Whether the member is deafened.
:ivar bool mute: Whether the member is muted.
:ivar Optional[bool] pending?: Whether the member is pending to pass membership screening.
- :ivar Optional[str] permissions?: Whether the member has permissions.
+ :ivar Optional[Permissions] permissions?: Whether the member has permissions.
:ivar Optional[str] communication_disabled_until?: How long until they're unmuted, if any.
"""
@@ -41,6 +44,7 @@ class Member(DictSerializerMixin):
"permissions",
"communication_disabled_until",
"hoisted_role",
+ "_client",
)
def __init__(self, **kwargs):
@@ -56,3 +60,360 @@ def __init__(self, **kwargs):
if self._json.get("premium_since")
else None
)
+
+ self.permissions = (
+ Permissions(int(self._json.get("permissions")))
+ if self._json.get("permissions")
+ else None
+ )
+
+ if not self.avatar and self.user:
+ self.avatar = self.user.avatar
+
+ async def ban(
+ self,
+ guild_id: int,
+ reason: Optional[str] = None,
+ delete_message_days: Optional[int] = 0,
+ ) -> None:
+ """
+ Bans the member from a guild.
+
+ :param guild_id: The id of the guild to ban the member from
+ :type guild_id: int
+ :param reason?: The reason of the ban
+ :type reason: Optional[str]
+ :param delete_message_days?: Number of days to delete messages, from 0 to 7. Defaults to 0
+ :type delete_message_days: Optional[int]
+ """
+ await self._client.create_guild_ban(
+ guild_id=guild_id,
+ user_id=int(self.user.id),
+ reason=reason,
+ delete_message_days=delete_message_days,
+ )
+
+ async def kick(
+ self,
+ guild_id: int,
+ reason: Optional[str] = None,
+ ) -> None:
+ """
+ Kicks the member from a guild.
+
+ :param guild_id: The id of the guild to kick the member from
+ :type guild_id: int
+ :param reason?: The reason for the kick
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.create_guild_kick(
+ guild_id=guild_id,
+ user_id=int(self.user.id),
+ reason=reason,
+ )
+
+ async def add_role(
+ self,
+ role: Union[Role, int],
+ guild_id: int,
+ reason: Optional[str],
+ ) -> None:
+ """
+ This method adds a role to a member.
+
+ :param role: The role to add. Either ``Role`` object or role_id
+ :type role: Union[Role, int]
+ :param guild_id: The id of the guild to add the roles to the member
+ :type guild_id: int
+ :param reason?: The reason why the roles are added
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if isinstance(role, Role):
+ await self._client.add_member_role(
+ guild_id=guild_id,
+ user_id=int(self.user.id),
+ role_id=int(role.id),
+ reason=reason,
+ )
+ else:
+ await self._client.add_member_role(
+ guild_id=guild_id,
+ user_id=int(self.user.id),
+ role_id=role,
+ reason=reason,
+ )
+
+ async def remove_role(
+ self,
+ role: Union[Role, int],
+ guild_id: int,
+ reason: Optional[str],
+ ) -> None:
+ """
+ This method removes a role from a member.
+
+ :param role: The role to remove. Either ``Role`` object or role_id
+ :type role: Union[Role, int]
+ :param guild_id: The id of the guild to remove the roles of the member
+ :type guild_id: int
+ :param reason?: The reason why the roles are removed
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ if isinstance(role, Role):
+ await self._client.remove_member_role(
+ guild_id=guild_id,
+ user_id=int(self.user.id),
+ role_id=int(role.id),
+ reason=reason,
+ )
+ else:
+ await self._client.remove_member_role(
+ guild_id=guild_id,
+ user_id=int(self.user.id),
+ role_id=role,
+ reason=reason,
+ )
+
+ async def send(
+ self,
+ content: Optional[str] = None,
+ *,
+ components=None,
+ tts: Optional[bool] = False,
+ # attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
+ embeds=None,
+ allowed_mentions=None,
+ ):
+ """
+ Sends a DM to the member.
+
+ :param content?: The contents of the message as a string or string-converted value.
+ :type content: Optional[str]
+ :param components?: A component, or list of components for the message.
+ :type components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]]
+ :param tts?: Whether the message utilizes the text-to-speech Discord programme or not.
+ :type tts: Optional[bool]
+ :param embeds?: An embed, or list of embeds for the message.
+ :type embeds: Optional[Union[Embed, List[Embed]]]
+ :param allowed_mentions?: The message interactions/mention limits that the message can refer to.
+ :type allowed_mentions: Optional[MessageInteraction]
+ :return: The sent message as an object.
+ :rtype: Message
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from ...models.component import ActionRow, Button, SelectMenu
+ from .channel import Channel
+ from .message import Message
+
+ _content: str = "" if content is None else content
+ _tts: bool = False if tts is None else tts
+ # _file = None if file is None else file
+ # _attachments = [] if attachments else None
+ _embeds: list = (
+ []
+ if embeds is None
+ else ([embed._json for embed in embeds] if isinstance(embeds, list) else [embeds._json])
+ )
+ _allowed_mentions: dict = {} if allowed_mentions is None else allowed_mentions
+ _components: List[dict] = [{"type": 1, "components": []}]
+
+ # TODO: Break this obfuscation pattern down to a "builder" method.
+ if components:
+ if isinstance(components, list) and all(
+ isinstance(action_row, ActionRow) for action_row in components
+ ):
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in action_row.components
+ ],
+ }
+ for action_row in components
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(component, (Button, SelectMenu)) for component in components
+ ):
+ for component in components:
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in component._json["options"]
+ ]
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components
+ ],
+ }
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(action_row, (list, ActionRow)) for action_row in components
+ ):
+ _components = []
+ for action_row in components:
+ for component in (
+ action_row if isinstance(action_row, list) else action_row.components
+ ):
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ option._json for option in component.options
+ ]
+ _components.append(
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id")
+ or component._json.get("url")
+ else []
+ )
+ for component in (
+ action_row
+ if isinstance(action_row, list)
+ else action_row.components
+ )
+ ],
+ }
+ )
+ elif isinstance(components, ActionRow):
+ _components[0]["components"] = [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components.components
+ ]
+ elif isinstance(components, Button):
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ elif isinstance(components, SelectMenu):
+ components._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in components._json["options"]
+ ]
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ else:
+ _components = []
+
+ # TODO: post-v4: Add attachments into Message obj.
+ payload = Message(
+ content=_content,
+ tts=_tts,
+ # file=file,
+ # attachments=_attachments,
+ embeds=_embeds,
+ components=_components,
+ allowed_mentions=_allowed_mentions,
+ )
+
+ channel = Channel(**await self._client.create_dm(recipient_id=int(self.user.id)))
+ res = await self._client.create_message(channel_id=int(channel.id), payload=payload._json)
+
+ return Message(**res, _client=self._client)
+
+ async def modify(
+ self,
+ guild_id: int,
+ nick: Optional[str] = None,
+ roles: Optional[List[int]] = None,
+ mute: Optional[bool] = None,
+ deaf: Optional[bool] = None,
+ channel_id: Optional[int] = None,
+ communication_disabled_until: Optional[datetime.isoformat] = None,
+ reason: Optional[str] = None,
+ ) -> "Member":
+ """
+ Modifies the member of a guild.
+
+ :param guild_id: The id of the guild to modify the member on
+ :type guild_id: int
+ :param nick?: The nickname of the member
+ :type nick: Optional[str]
+ :param roles?: A list of all role ids the member has
+ :type roles: Optional[List[int]]
+ :param mute?: whether the user is muted in voice channels
+ :type mute: Optional[bool]
+ :param deaf?: whether the user is deafened in voice channels
+ :type deaf: Optional[bool]
+ :param channel_id?: id of channel to move user to (if they are connected to voice)
+ :type channel_id: Optional[int]
+ :param communication_disabled_until?: when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future)
+ :type communication_disabled_until: Optional[datetime.isoformat]
+ :param reason?: The reason of the modifying
+ :type reason: Optional[str]
+ :return: The modified member object
+ :rtype: Member
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ payload = {}
+ if nick:
+ payload["nick"] = nick
+
+ if roles:
+ payload["roles"] = roles
+
+ if channel_id:
+ payload["channel_id"] = channel_id
+
+ if mute:
+ payload["mute"] = mute
+
+ if deaf:
+ payload["deaf"] = deaf
+
+ if communication_disabled_until:
+ payload["communication_disabled_until"] = communication_disabled_until
+
+ res = await self._client.modify_member(
+ user_id=int(self.user.id),
+ guild_id=guild_id,
+ payload=payload,
+ reason=reason,
+ )
+ return Member(**res, _client=self._client)
+
+ async def add_to_thread(
+ self,
+ thread_id: int,
+ ) -> None:
+ """
+ Adds the member to a thread.
+
+ :param thread_id: The id of the thread to add the member to
+ :type thread_id: int
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.add_member_to_thread(
+ user_id=int(self.user.id),
+ thread_id=thread_id,
+ )
diff --git a/interactions/api/models/member.pyi b/interactions/api/models/member.pyi
index b0f2c2f9f..e5537bcc5 100644
--- a/interactions/api/models/member.pyi
+++ b/interactions/api/models/member.pyi
@@ -1,13 +1,19 @@
from datetime import datetime
-from typing import Any, List, Optional
+from typing import Any, List, Optional, Union
from .misc import DictSerializerMixin
from .role import Role
from .user import User
+from .flags import Permissions
+from ..http import HTTPClient
+from .message import Message
+from ...models.component import ActionRow, Button, SelectMenu
+
class Member(DictSerializerMixin):
_json: dict
+ _client: HTTPClient
user: Optional[User]
nick: Optional[str]
avatar: Optional[str]
@@ -18,7 +24,55 @@ class Member(DictSerializerMixin):
mute: bool
is_pending: Optional[bool]
pending: Optional[bool]
- permissions: Optional[str]
- communication_disabled_until: Optional[str]
+ permissions: Optional[Permissions]
+ communication_disabled_until: Optional[datetime.isoformat]
hoisted_role: Any # TODO: post-v4: Investigate what this is for when documented by Discord.
def __init__(self, **kwargs): ...
+ async def ban(
+ self,
+ guild_id: int,
+ reason: Optional[str] = None,
+ delete_message_days: Optional[int] = 0,
+ ) -> None: ...
+ async def kick(
+ self,
+ guild_id: int,
+ reason: Optional[str] = None,
+ ) -> None: ...
+ async def add_role(
+ self,
+ role: Union[Role, int],
+ guild_id: int,
+ reason: Optional[str],
+ ) -> None: ...
+ async def remove_role(
+ self,
+ role: Union[Role, int],
+ guild_id: int,
+ reason: Optional[str],
+ ) -> None: ...
+ async def send(
+ self,
+ content: Optional[str] = None,
+ *,
+ components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]] = None,
+ tts: Optional[bool] = False,
+ # attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
+ embeds=None,
+ allowed_mentions=None,
+ ) -> Message: ...
+ async def modify(
+ self,
+ guild_id: int,
+ nick: Optional[str] = None,
+ roles: Optional[List[int]] = None,
+ mute: Optional[bool] = None,
+ deaf: Optional[bool] = None,
+ channel_id: Optional[int] = None,
+ communication_disabled_until: Optional[datetime.isoformat] = None,
+ reason: Optional[str] = None,
+ ) -> "Member": ...
+ async def add_to_thread(
+ self,
+ thread_id: int,
+ ) -> None: ...
diff --git a/interactions/api/models/message.py b/interactions/api/models/message.py
index 4fa8d9c2f..5a69a5989 100644
--- a/interactions/api/models/message.py
+++ b/interactions/api/models/message.py
@@ -1,5 +1,6 @@
from datetime import datetime
from enum import IntEnum
+from typing import List, Optional, Union
from .channel import Channel, ChannelType
from .member import Member
@@ -162,7 +163,7 @@ class Message(DictSerializerMixin):
:ivar Snowflake id: ID of the message.
:ivar Snowflake channel_id: ID of the channel the message was sent in
- :ivar Optional[Snowflake] guild_id:? ID of the guild the message was sent in, if it exists.
+ :ivar Optional[Snowflake] guild_id?: ID of the guild the message was sent in, if it exists.
:ivar User author: The author of the message.
:ivar Optional[Member] member?: The member object associated with the author, if any.
:ivar str content: Message contents.
@@ -170,7 +171,7 @@ class Message(DictSerializerMixin):
:ivar Optional[datetime] edited_timestamp?: Timestamp denoting when the message was edited, if any.
:ivar bool tts: Status dictating if this was a TTS message or not.
:ivar bool mention_everyone: Status dictating of this message mentions everyone
- :ivar Optional[List[Union[Member, User]]] mentions?: Array of user objects with an addictional partial member field.
+ :ivar Optional[List[Union[Member, User]]] mentions?: Array of user objects with an additional partial member field.
:ivar Optional[List[str]] mention_roles?: Array of roles mentioned in this message
:ivar Optional[List[ChannelMention]] mention_channels?: Channels mentioned in this message, if any.
:ivar List[Attachment] attachments: An array of attachments
@@ -186,9 +187,9 @@ class Message(DictSerializerMixin):
:ivar Optional[Any] allowed_mentions: The allowed mentions of roles attached in the message.
:ivar int flags: Message flags
:ivar Optional[MessageInteraction] interaction?: Message interaction object, if the message is sent by an interaction.
- :ivar Optional[Channel] thread:? The thread that started from this message, if any, with a thread member object embedded.
+ :ivar Optional[Channel] thread?: The thread that started from this message, if any, with a thread member object embedded.
:ivar Optional[Union[Component, List[Component]]] components?: Components associated with this message, if any.
- :ivar Optional[List[PartialSticker"]] sticker_items?: An array of message sticker item objects, if sent with them.
+ :ivar Optional[List[PartialSticker]] sticker_items?: An array of message sticker item objects, if sent with them.
:ivar Optional[List[Sticker]] stickers?: Array of sticker objects sent with the message if any. Deprecated.
"""
@@ -226,6 +227,7 @@ class Message(DictSerializerMixin):
"components",
"sticker_items",
"stickers",
+ "_client",
)
def __init__(self, **kwargs):
@@ -278,6 +280,395 @@ def __init__(self, **kwargs):
)
self.thread = Channel(**self.thread) if self._json.get("thread") else None
+ async def get_channel(self) -> Channel:
+ """
+ Gets the channel where the message was sent.
+
+ :rtype: Channel
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = await self._client.get_channel(channel_id=int(self.channel_id))
+ return Channel(**res, _client=self._client)
+
+ async def get_guild(self):
+ """
+ Gets the guild where the message was sent.
+
+ :rtype: Guild
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from .guild import Guild
+
+ res = await self._client.get_guild(guild_id=int(self.guild_id))
+ return Guild(**res, _client=self._client)
+
+ async def delete(self, reason: Optional[str] = None) -> None:
+ """
+ Deletes the message.
+
+ :param reason: Optional reason to show up in the audit log. Defaults to `None`.
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.delete_message(
+ message_id=int(self.id), channel_id=int(self.channel_id), reason=reason
+ )
+
+ async def edit(
+ self,
+ content: Optional[str] = None,
+ *,
+ tts: Optional[bool] = False,
+ # file: Optional[FileIO] = None,
+ embeds: Optional[Union["Embed", List["Embed"]]] = None,
+ allowed_mentions: Optional["MessageInteraction"] = None,
+ message_reference: Optional["MessageReference"] = None,
+ components=None,
+ ) -> "Message":
+ """
+ This method edits a message. Only available for messages sent by the bot.
+
+ :param content?: The contents of the message as a string or string-converted value.
+ :type content: Optional[str]
+ :param tts?: Whether the message utilizes the text-to-speech Discord programme or not.
+ :type tts: Optional[bool]
+ :param embeds?: An embed, or list of embeds for the message.
+ :type embeds: Optional[Union[Embed, List[Embed]]]
+ :param allowed_mentions?: The message interactions/mention limits that the message can refer to.
+ :type allowed_mentions: Optional[MessageInteraction]
+ :param components?: A component, or list of components for the message. If `[]` the components will be removed
+ :type components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]]
+ :return: The edited message as an object.
+ :rtype: Message
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from ...models.component import ActionRow, Button, SelectMenu
+
+ _content: str = self.content if content is None else content
+ _tts: bool = True if bool(tts) else tts
+ # _file = None if file is None else file
+
+ if embeds is None:
+ _embeds = self.embeds
+ else:
+ _embeds: list = (
+ []
+ if embeds is None
+ else (
+ [embed._json for embed in embeds]
+ if isinstance(embeds, list)
+ else [embeds._json]
+ )
+ )
+ _allowed_mentions: dict = {} if allowed_mentions is None else allowed_mentions
+ _message_reference: dict = {} if message_reference is None else message_reference._json
+ if components == []:
+ _components = []
+ # TODO: Break this obfuscation pattern down to a "builder" method.
+ elif components is not None and components != []:
+ _components = []
+ if isinstance(components, list) and all(
+ isinstance(action_row, ActionRow) for action_row in components
+ ):
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in action_row.components
+ ],
+ }
+ for action_row in components
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(component, (Button, SelectMenu)) for component in components
+ ):
+ for component in components:
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in component._json["options"]
+ ]
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components
+ ],
+ }
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(action_row, (list, ActionRow)) for action_row in components
+ ):
+ _components = []
+ for action_row in components:
+ for component in (
+ action_row if isinstance(action_row, list) else action_row.components
+ ):
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ option._json for option in component.options
+ ]
+ _components.append(
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id")
+ or component._json.get("url")
+ else []
+ )
+ for component in (
+ action_row
+ if isinstance(action_row, list)
+ else action_row.components
+ )
+ ],
+ }
+ )
+ elif isinstance(components, ActionRow):
+ _components[0]["components"] = [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components.components
+ ]
+ elif isinstance(components, Button):
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ elif isinstance(components, SelectMenu):
+ components._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in components._json["options"]
+ ]
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ else:
+ _components = self.components
+
+ payload: Message = Message(
+ content=_content,
+ tts=_tts,
+ # file=file,
+ embeds=_embeds,
+ allowed_mentions=_allowed_mentions,
+ message_reference=_message_reference,
+ components=_components,
+ )
+
+ await self._client.edit_message(
+ channel_id=int(self.channel_id),
+ message_id=int(self.id),
+ payload=payload._json,
+ )
+ return payload
+
+ async def reply(
+ self,
+ content: Optional[str] = None,
+ *,
+ tts: Optional[bool] = False,
+ # attachments: Optional[List[Any]] = None
+ embeds: Optional[Union["Embed", List["Embed"]]] = None,
+ allowed_mentions: Optional["MessageInteraction"] = None,
+ components=None,
+ ) -> "Message":
+ """
+ Sends a new message replying to the old.
+
+ :param content?: The contents of the message as a string or string-converted value.
+ :type content: Optional[str]
+ :param tts?: Whether the message utilizes the text-to-speech Discord programme or not.
+ :type tts: Optional[bool]
+ :param embeds?: An embed, or list of embeds for the message.
+ :type embeds: Optional[Union[Embed, List[Embed]]]
+ :param allowed_mentions?: The message interactions/mention limits that the message can refer to.
+ :type allowed_mentions: Optional[MessageInteraction]
+ :param components?: A component, or list of components for the message.
+ :type components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]]
+ :return: The sent message as an object.
+ :rtype: Message
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ from ...models.component import ActionRow, Button, SelectMenu
+
+ _content: str = "" if content is None else content
+ _tts: bool = True if bool(tts) else tts
+ # _file = None if file is None else file
+ # _attachments = [] if attachments else None
+ _embeds: list = (
+ []
+ if embeds is None
+ else ([embed._json for embed in embeds] if isinstance(embeds, list) else [embeds._json])
+ )
+ _allowed_mentions: dict = {} if allowed_mentions is None else allowed_mentions
+ _message_reference = MessageReference(message_id=int(self.id))._json
+ _components: List[dict] = [{"type": 1, "components": []}]
+
+ # TODO: Break this obfuscation pattern down to a "builder" method.
+ if components:
+ if isinstance(components, list) and all(
+ isinstance(action_row, ActionRow) for action_row in components
+ ):
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in action_row.components
+ ],
+ }
+ for action_row in components
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(component, (Button, SelectMenu)) for component in components
+ ):
+ for component in components:
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in component._json["options"]
+ ]
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components
+ ],
+ }
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(action_row, (list, ActionRow)) for action_row in components
+ ):
+ _components = []
+ for action_row in components:
+ for component in (
+ action_row if isinstance(action_row, list) else action_row.components
+ ):
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ option._json for option in component.options
+ ]
+ _components.append(
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id")
+ or component._json.get("url")
+ else []
+ )
+ for component in (
+ action_row
+ if isinstance(action_row, list)
+ else action_row.components
+ )
+ ],
+ }
+ )
+ elif isinstance(components, ActionRow):
+ _components[0]["components"] = [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components.components
+ ]
+ elif isinstance(components, Button):
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ elif isinstance(components, SelectMenu):
+ components._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in components._json["options"]
+ ]
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ else:
+ _components = []
+
+ # TODO: post-v4: Add attachments into Message obj.
+ payload = Message(
+ content=_content,
+ tts=_tts,
+ # file=file,
+ # attachments=_attachments,
+ embeds=_embeds,
+ message_reference=_message_reference,
+ allowed_mentions=_allowed_mentions,
+ components=_components,
+ )
+
+ res = await self._client.create_message(
+ channel_id=int(self.channel_id), payload=payload._json
+ )
+ return Message(**res, _client=self._client)
+
+ async def pin(self) -> None:
+ """Pins the message to its channel"""
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.pin_message(channel_id=int(self.channel_id), message_id=int(self.id))
+
+ async def unpin(self) -> None:
+ """Unpins the message from its channel"""
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.unpin_message(channel_id=int(self.channel_id), message_id=int(self.id))
+
+ async def publish(self) -> "Message":
+ """Publishes (API calls it crossposts) the message in its channel to any that is followed by.
+
+ :return: message object
+ :rtype: Message
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = await self._client.publish_message(
+ channel_id=int(self.channel_id), message_id=int(self.id)
+ )
+ return Message(**res, _client=self._client)
+
class Emoji(DictSerializerMixin):
"""
@@ -524,7 +915,7 @@ class Embed(DictSerializerMixin):
:ivar Optional[EmbedImageStruct] video?: Video information
:ivar Optional[EmbedProvider] provider?: Provider information
:ivar Optional[EmbedAuthor] author?: Author information
- :ivar Optional[EmbedField] fields?: A list of fields denoting field information
+ :ivar Optional[List[EmbedField]] fields?: A list of fields denoting field information
"""
__slots__ = (
@@ -582,7 +973,26 @@ def __init__(self, **kwargs):
else self._json.get("author")
)
self.fields = (
- [EmbedField(**field) for field in self.fields]
- if isinstance(self._json.get("fields"), dict)
- else self._json.get("fields")
+ [
+ EmbedField(**field) if isinstance(field, dict) else field
+ for field in self._json["fields"]
+ ]
+ if self._json.get("fields")
+ else None
)
+
+ # TODO: Complete partial fix.
+ # The issue seems to be that this itself is not updating
+ # JSON result correctly. After numerous attempts I seem to
+ # have the attribute to do it, but _json won't budge at all.
+ # a genexpr is a poor way to go about this, but I know later
+ # on we'll be refactoring this anyhow. What the fuck is breaking
+ # it?
+ if self.fields:
+ self._json.update({"fields": [field._json for field in self.fields]})
+
+ if self.author:
+ self._json.update({"author": self.author._json})
+
+ if self.footer:
+ self._json.update({"footer": self.footer._json})
diff --git a/interactions/api/models/message.pyi b/interactions/api/models/message.pyi
index 43e7c3563..8f5b5e245 100644
--- a/interactions/api/models/message.pyi
+++ b/interactions/api/models/message.pyi
@@ -7,6 +7,10 @@ from .misc import DictSerializerMixin, Snowflake
from .role import Role
from .team import Application
from .user import User
+from ..http import HTTPClient
+from ...models.component import ActionRow, Button, SelectMenu
+from .guild import Guild
+
class MessageActivity(DictSerializerMixin):
_json: dict
@@ -51,6 +55,7 @@ class ChannelMention(DictSerializerMixin):
def __init__(self, **kwargs): ...
class Message(DictSerializerMixin):
+ _client: HTTPClient
_json: dict
id: Snowflake
channel_id: Snowflake
@@ -86,6 +91,34 @@ class Message(DictSerializerMixin):
sticker_items: Optional[List["PartialSticker"]]
stickers: Optional[List["Sticker"]] # deprecated
def __init__(self, **kwargs): ...
+ async def delete(self, reason: Optional[str] = None) -> None: ...
+ async def edit(
+ self,
+ content: Optional[str] = None,
+ *,
+ tts: Optional[bool] = None,
+ # file: Optional[FileIO] = None,
+ embeds: Optional[Union["Embed", List["Embed"]]] = None,
+ allowed_mentions: Optional["MessageInteraction"] = None,
+ message_reference: Optional["MessageReference"] = None,
+ components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]] = None,
+ ) -> "Message": ...
+
+ async def reply(self,
+ content: Optional[str] = None,
+ *,
+ tts: Optional[bool] = False,
+ # attachments: Optional[List[Any]] = None
+ embeds: Optional[Union["Embed", List["Embed"]]] = None,
+ allowed_mentions: Optional["MessageInteraction"] = None,
+ components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]]=None,
+ ) -> "Message": ...
+ async def get_channel(self) -> Channel: ...
+ async def get_guild(self) -> Guild: ...
+ async def pin(self) -> None: ...
+ async def unpin(self) -> None: ...
+ async def publish(self) -> "Message": ...
+
class Emoji(DictSerializerMixin):
_json: dict
diff --git a/interactions/api/models/misc.py b/interactions/api/models/misc.py
index 0e863abc6..b555cc2b7 100644
--- a/interactions/api/models/misc.py
+++ b/interactions/api/models/misc.py
@@ -4,10 +4,13 @@
# TODO: Reorganise mixins to its own thing, currently placed here because circular import sucks.
# also, it should be serialiser* but idk, fl0w'd say something if I left it like that. /shrug
import datetime
-import logging
+from logging import Logger
+from math import floor
from typing import Union
-log = logging.getLogger("mixin")
+from interactions.base import get_logger
+
+log: Logger = get_logger("mixin")
class DictSerializerMixin(object):
@@ -143,16 +146,16 @@ def process_id(self) -> int:
@property
def epoch(self) -> float:
"""
- This is the "Timestamp" field of the snowflake.
+ This is the Timestamp field of the snowflake.
:return: A float containing the seconds since Discord Epoch.
"""
- return ((int(self._snowflake) >> 22) + 1420070400000) / 1000
+ return floor(((int(self._snowflake) >> 22) + 1420070400000) / 1000)
@property
def timestamp(self) -> datetime.datetime:
"""
- The Datetime object variation of the the "Timestamp" field of the snowflake.
+ The Datetime object variation of the Timestamp field of the snowflake.
:return: The converted Datetime object from the Epoch. This respects UTC.
"""
@@ -202,7 +205,7 @@ class Format:
def stylize(cls, format: str, **kwargs) -> str:
r"""
This takes a format style from the object and
- converts it into a useable string for ease.
+ converts it into a usable string for ease.
:param format: The format string to use.
:type format: str
@@ -214,5 +217,5 @@ def stylize(cls, format: str, **kwargs) -> str:
new: str = f"" # noqa: F541
for kwarg in kwargs:
if format == kwarg:
- new = new % format
+ new %= format
return new
diff --git a/interactions/api/models/presence.py b/interactions/api/models/presence.py
index 56271d6ce..ae16627ea 100644
--- a/interactions/api/models/presence.py
+++ b/interactions/api/models/presence.py
@@ -81,7 +81,6 @@ class PresenceActivity(DictSerializerMixin):
:ivar str name: The activity name
:ivar str type: The activity type
- :ivar str id: The activity ID.
:ivar Optional[str] url?: stream url (if type is 1)
:ivar Snowflake created_at: Unix timestamp of when the activity was created to the User's session
:ivar Optional[PresenceTimestamp] timestamps?: Unix timestamps for start and/or end of the game
@@ -101,7 +100,6 @@ class PresenceActivity(DictSerializerMixin):
"_json",
"name",
"type",
- "id",
"url",
"created_at",
"timestamps",
@@ -116,10 +114,14 @@ class PresenceActivity(DictSerializerMixin):
"flags",
"buttons",
# TODO: document/investigate what these do.
+ "user",
"users",
"status",
"client_status",
"activities",
+ "sync_id",
+ "session_id",
+ "id",
)
def __init__(self, **kwargs):
diff --git a/interactions/api/models/presence.pyi b/interactions/api/models/presence.pyi
index f90b71cb9..c60d1b699 100644
--- a/interactions/api/models/presence.pyi
+++ b/interactions/api/models/presence.pyi
@@ -40,7 +40,6 @@ class PresenceActivity(DictSerializerMixin):
_json: dict
name: str
type: int
- id: str
url: Optional[str]
created_at: Snowflake
timestamps: Optional[PresenceTimestamp]
diff --git a/interactions/api/models/role.py b/interactions/api/models/role.py
index 6bc8f2a55..61f4cc933 100644
--- a/interactions/api/models/role.py
+++ b/interactions/api/models/role.py
@@ -1,3 +1,5 @@
+from typing import List, Optional
+
from .misc import DictSerializerMixin, Snowflake
@@ -53,9 +55,102 @@ class Role(DictSerializerMixin):
"mentionable",
"tags",
"permissions",
+ "_client",
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.id = Snowflake(self.id) if self._json.get("id") else None
self.tags = RoleTags(**self.tags) if self._json.get("tags") else None
+
+ async def delete(
+ self,
+ guild_id: int,
+ reason: Optional[str] = None,
+ ) -> None:
+ """
+ Deletes the role from the guild.
+
+ :param guild_id: The id of the guild to delete the role from
+ :type guild_id: int
+ :param reason: The reason for the deletion
+ :type reason: Optional[str]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ await self._client.delete_guild_role(
+ guild_id=guild_id, role_id=int(self.id), reason=reason
+ ),
+
+ async def modify(
+ self,
+ guild_id: int,
+ name: Optional[str] = None,
+ # permissions,
+ color: Optional[int] = None,
+ hoist: Optional[bool] = None,
+ # icon,
+ # unicode_emoji,
+ mentionable: Optional[bool] = None,
+ reason: Optional[str] = None,
+ ) -> "Role":
+ """
+ Edits the role in a guild.
+
+ :param guild_id: The id of the guild to edit the role on
+ :type guild_id: int
+ :param name?: The name of the role, defaults to the current value of the role
+ :type name: Optional[str]
+ :param color?: RGB color value as integer, defaults to the current value of the role
+ :type color: Optional[int]
+ :param hoist?: Whether the role should be displayed separately in the sidebar, defaults to the current value of the role
+ :type hoist: Optional[bool]
+ :param mentionable?: Whether the role should be mentionable, defaults to the current value of the role
+ :type mentionable: Optional[bool]
+ :param reason?: The reason why the role is edited, default ``None``
+ :type reason: Optional[str]
+ :return: The modified role object
+ :rtype: Role
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ _name = self.name if not name else name
+ _color = self.color if not color else color
+ _hoist = self.hoist if not hoist else hoist
+ _mentionable = self.mentionable if mentionable is None else mentionable
+
+ payload = Role(name=_name, color=_color, hoist=_hoist, mentionable=_mentionable)
+
+ res = await self._client.modify_guild_role(
+ guild_id=guild_id,
+ role_id=int(self.id),
+ data=payload._json,
+ reason=reason,
+ )
+ return Role(**res, _client=self._client)
+
+ async def modify_position(
+ self,
+ guild_id: int,
+ position: int,
+ reason: Optional[str] = None,
+ ) -> List["Role"]:
+ """
+ Modifies the position of a role in the guild.
+
+ :param guild_id: The id of the guild to modify the role position on
+ :type guild_id: int
+ :param position: The new position of the role
+ :type position: int
+ :param reason?: The reason for the modifying
+ :type reason: Optional[str]
+ :return: List of guild roles with updated hierarchy
+ :rtype: List[Role]
+ """
+ if not self._client:
+ raise AttributeError("HTTPClient not found!")
+ res = await self._client.modify_guild_role_position(
+ guild_id=guild_id, position=position, role_id=int(self.id), reason=reason
+ )
+ roles = [Role(**role, _client=self._client) for role in res]
+ return roles
diff --git a/interactions/api/models/role.pyi b/interactions/api/models/role.pyi
index 06f1835a3..7365ace2b 100644
--- a/interactions/api/models/role.pyi
+++ b/interactions/api/models/role.pyi
@@ -1,6 +1,7 @@
-from typing import Any, Optional
+from typing import Any, Optional, List
from .misc import DictSerializerMixin, Snowflake
+from ..http import HTTPClient
class RoleTags(DictSerializerMixin):
_json: dict
@@ -11,6 +12,7 @@ class RoleTags(DictSerializerMixin):
class Role(DictSerializerMixin):
_json: dict
+ _client: HTTPClient
id: Snowflake
name: str
color: int
@@ -23,3 +25,26 @@ class Role(DictSerializerMixin):
mentionable: bool
tags: Optional[RoleTags]
def __init__(self, **kwargs): ...
+ async def delete(
+ self,
+ guild_id: int,
+ reason: Optional[str] = None,
+ ) -> None: ...
+ async def modify(
+ self,
+ guild_id: int,
+ name: Optional[str] = None,
+ # permissions,
+ color: Optional[int] = None,
+ hoist: Optional[bool] = None,
+ # icon,
+ # unicode_emoji,
+ mentionable: Optional[bool] = None,
+ reason: Optional[str] = None,
+ ) -> "Role": ...
+ async def modify_position(
+ self,
+ guild_id: int,
+ position: int,
+ reason: Optional[str] = None,
+ ) -> List["Role"]: ...
diff --git a/interactions/api/models/team.py b/interactions/api/models/team.py
index c0209d937..64c228d1a 100644
--- a/interactions/api/models/team.py
+++ b/interactions/api/models/team.py
@@ -53,7 +53,7 @@ def __init__(self, **kwargs):
class Application(DictSerializerMixin):
"""
- A class object representing an appliation.
+ A class object representing an application.
.. note::
``type`` and ``hook`` are currently undocumented in the API.
@@ -101,6 +101,8 @@ class Application(DictSerializerMixin):
"type",
"hook",
"tags", # TODO: document/investigate what it does.
+ "install_params",
+ "custom_install_url",
)
def __init__(self, **kwargs):
diff --git a/interactions/base.py b/interactions/base.py
index f8191a99d..bae7cd2f0 100644
--- a/interactions/base.py
+++ b/interactions/base.py
@@ -1,5 +1,5 @@
import logging
-from typing import ClassVar
+from typing import ClassVar, List, Optional, Union
from colorama import Fore, Style, init
@@ -18,21 +18,44 @@
class Data:
- """A class representing constants for the library."""
+ """A class representing constants for the library.
- LOGGER: ClassVar[int] = logging.WARNING
+ :ivar LOG_LEVEL ClassVar[int]: The default level of logging as an integer
+ :ivar LOGGERS List[str]: A list of all loggers registered from this library
+ """
+
+ LOG_LEVEL: ClassVar[int] = logging.ERROR
+ LOGGERS: List[str] = []
+
+
+def get_logger(
+ logger: Optional[Union[logging.Logger, str]] = None,
+ handler: Optional[logging.Handler] = logging.StreamHandler(),
+) -> logging.Logger:
+ _logger = logging.getLogger(logger) if isinstance(logger, str) else logger
+ _logger_name = logger if isinstance(logger, str) else logger.name
+ if len(_logger.handlers) > 1:
+ _logger.removeHandler(_logger.handlers[0])
+ _handler = handler
+ _handler.setFormatter(CustomFormatter)
+ _handler.setLevel(Data.LOG_LEVEL)
+ _logger.addHandler(_handler)
+ _logger.propagate = True
+
+ Data.LOGGERS.append(_logger_name)
+ return _logger
class CustomFormatter(logging.Formatter):
"""A class that allows for customized logged outputs from the library."""
- format: str = "%(levelname)s:%(name)s:(ln.%(lineno)d):%(message)s"
+ format_str: str = "%(levelname)s:%(name)s:(ln.%(lineno)d):%(message)s"
formats: dict = {
- logging.DEBUG: Fore.CYAN + format + Fore.RESET,
- logging.INFO: Fore.GREEN + format + Fore.RESET,
- logging.WARNING: Fore.YELLOW + format + Fore.RESET,
- logging.ERROR: Fore.RED + format + Fore.RESET,
- logging.CRITICAL: Style.BRIGHT + Fore.RED + format + Fore.RESET + Style.NORMAL,
+ logging.DEBUG: Fore.CYAN + format_str + Fore.RESET,
+ logging.INFO: Fore.GREEN + format_str + Fore.RESET,
+ logging.WARNING: Fore.YELLOW + format_str + Fore.RESET,
+ logging.ERROR: Fore.RED + format_str + Fore.RESET,
+ logging.CRITICAL: Style.BRIGHT + Fore.RED + format_str + Fore.RESET + Style.NORMAL,
}
def __init__(self):
diff --git a/interactions/client.py b/interactions/client.py
index 36ef37889..c183e7cb1 100644
--- a/interactions/client.py
+++ b/interactions/client.py
@@ -1,34 +1,29 @@
import sys
from asyncio import get_event_loop
-
-# from functools import partial
from importlib import import_module
from importlib.util import resolve_name
-from logging import Logger, StreamHandler, basicConfig, getLogger
+from logging import Logger
from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
from .api.cache import Cache
from .api.cache import Item as Build
-from .api.error import InteractionException, JSONException
+from .api.dispatch import Listener
+from .api.error import InteractionException
from .api.gateway import WebSocket
from .api.http import HTTPClient
+from .api.models.flags import Intents
from .api.models.guild import Guild
-from .api.models.gw import Presence
-from .api.models.intents import Intents
+from .api.models.misc import Snowflake
from .api.models.team import Application
-from .base import CustomFormatter, Data
+from .base import get_logger
from .decor import command
from .decor import component as _component
from .enums import ApplicationCommandType
from .models.command import ApplicationCommand, Option
-from .models.component import Button, Component, Modal, SelectMenu
-
-basicConfig(level=Data.LOGGER)
-log: Logger = getLogger("client")
-stream: StreamHandler = StreamHandler()
-stream.setLevel(Data.LOGGER)
-stream.setFormatter(CustomFormatter())
-log.addHandler(stream)
+from .models.component import Button, Modal, SelectMenu
+from .models.misc import MISSING
+
+log: Logger = get_logger("client")
_token: str = "" # noqa
_cache: Optional[Cache] = None
@@ -37,85 +32,209 @@ class Client:
"""
A class representing the client connection to Discord's gateway and API via. WebSocket and HTTP.
- :ivar AbstractEventLoop loop: The main overall asynchronous coroutine loop in effect.
- :ivar Listener listener: An instance of :class:`interactions.api.dispatch.Listener`.
- :ivar Optional[Union[Intents, List[Intents]]] intents: The application's intents as :class:`interactions.api.models.Intents`.
- :ivar HTTPClient http: An instance of :class:`interactions.api.http.Request`.
- :ivar WebSocket websocket: An instance of :class:`interactions.api.gateway.WebSocket`.
- :ivar str token: The application token.
+ :ivar AbstractEventLoop _loop: The asynchronous event loop of the client.
+ :ivar HTTPClient _http: The user-facing HTTP connection to the Web API, as its own separate client.
+ :ivar WebSocket _websocket: An object-orientation of a websocket server connection to the Gateway.
+ :ivar Intents _intents: The Gateway intents of the application. Defaults to ``Intents.DEFAULT``.
+ :ivar Optional[List[Tuple[int]]] _shard: The list of bucketed shards for the application's connection.
+ :ivar Optional[Presence] _presence: The RPC-like presence shown on an application once connected.
+ :ivar str _token: The token of the application used for authentication when connecting.
+ :ivar Optional[Dict[str, ModuleType]] _extensions: The "extensions" or cog equivalence registered to the main client.
+ :ivar Application me: The application representation of the client.
"""
def __init__(
self,
token: str,
- intents: Optional[Union[Intents, List[Intents]]] = Intents.DEFAULT,
- disable_sync: Optional[bool] = False,
- log_level: Optional[int] = Data.LOGGER,
- shard: Optional[List[int]] = None,
- presence: Optional[Presence] = None,
+ **kwargs,
) -> None:
- """
+ r"""
+ Establishes a client connection to the Web API and Gateway.
+
:param token: The token of the application for authentication and connection.
:type token: str
- :param intents?: The intents you wish to pass through the client. Defaults to :meth:`interactions.api.models.intents.Intents.DEFAULT` or ``513``.
- :type intents: Optional[Union[Intents, List[Intents]]]
- :param disable_sync?: Whether you want to disable automate synchronization or not.
- :type disable_sync: Optional[bool]
- :param log_level?: The logging level to set for the terminal. Defaults to what is set internally.
- :type log_level: Optional[int]
- :param presence?: The presence of the application when connecting.
- :type presence: Optional[Presence]
- """
- if isinstance(intents, list):
- for intent in intents:
- self.intents |= intent
- else:
- self.intents = intents
-
- self.loop = get_event_loop()
- self.http = HTTPClient(token)
- self.websocket = WebSocket(intents=self.intents)
+ :param \**kwargs: Multiple key-word arguments able to be passed through.
+ :type \**kwargs: dict
+ """
+
+ # Arguments
+ # ~~~~~~~~~
+ # token : str
+ # The token of the application for authentication and connection.
+ # intents? : Optional[Intents]
+ # Allows specific control of permissions the application has when connected.
+ # In order to use multiple intents, the | operator is recommended.
+ # Defaults to ``Intents.DEFAULT``.
+ # shards? : Optional[List[Tuple[int]]]
+ # Dictates and controls the shards that the application connects under.
+ # presence? : Optional[Presence]
+ # Sets an RPC-like presence on the application when connected to the Gateway.
+ # disable_sync? : Optional[bool]
+ # Controls whether synchronization in the user-facing API should be automatic or not.
+
+ self._loop = get_event_loop()
+ self._http = HTTPClient(token=token)
+ self._intents = kwargs.get("intents", Intents.DEFAULT)
+ self._websocket = WebSocket(intents=self._intents)
+ self._shard = kwargs.get("shards", [])
+ self._presence = kwargs.get("presence")
+ self._token = token
+ self._extensions = {}
self.me = None
- self.token = token
- self.http.token = token
- self.shard = shard
- self.presence = presence
- self.extensions = {}
- _token = token # noqa: F841
- _cache = self.http.cache # noqa: F841
-
- if disable_sync:
- self.automate_sync = False
+ _token = self._token # noqa: F841
+ _cache = self._http.cache # noqa: F841
+
+ if kwargs.get("disable_sync"):
+ self._automate_sync = False
log.warning(
"Automatic synchronization has been disabled. Interactions may need to be manually synchronized."
)
else:
- self.automate_sync = True
+ self._automate_sync = True
+
+ data = self._loop.run_until_complete(self._http.get_current_bot_information())
+ self.me = Application(**data)
+
+ def start(self) -> None:
+ """Starts the client session."""
+ self._loop.run_until_complete(self._ready())
+
+ def __register_events(self) -> None:
+ """Registers all raw gateway events to the known events."""
+ self._websocket.dispatch.register(self.__raw_socket_create)
+ self._websocket.dispatch.register(self.__raw_channel_create, "on_channel_create")
+ self._websocket.dispatch.register(self.__raw_message_create, "on_message_create")
+ self._websocket.dispatch.register(self.__raw_guild_create, "on_guild_create")
+
+ async def __compare_sync(self, data: dict, pool: List[dict]) -> bool:
+ """
+ Compares an application command during the synchronization process.
+
+ :param data: The application command to compare.
+ :type data: dict
+ :param pool: The "pool" or list of commands to compare from.
+ :type pool: List[dict]
+ :return: Whether the command has changed or not.
+ :rtype: bool
+ """
+ attrs: List[str] = ["type", "name", "description", "options", "guild_id"]
+ log.info(f"Current attributes to compare: {', '.join(attrs)}.")
+ clean: bool = True
+
+ for command in pool:
+ if command["name"] == data["name"]:
+ for attr in attrs:
+ if hasattr(data, attr) and command.get(attr) == data.get(attr):
+ continue
+ else:
+ clean = False
- log_names: list = ["client", "context", "dispatch", "gateway", "http", "mixin"]
- for logger in log_names:
- getLogger(logger).setLevel(log_level)
+ return clean
- if not self.me:
- data = self.loop.run_until_complete(self.http.get_current_bot_information())
- self.me = Application(**data)
+ async def __create_sync(self, data: dict) -> None:
+ """
+ Creates an application command during the synchronization process.
- async def login(self, token: str) -> None:
+ :param data: The application command to create.
+ :type data: dict
"""
- Makes a login with the Discord API.
+ log.info(f"Creating command {data['name']}.")
- :param token: The application token needed for authorization.
- :type token: str
- :return: None
+ command: ApplicationCommand = ApplicationCommand(
+ **(
+ await self._http.create_application_command(
+ application_id=self.me.id, data=data, guild_id=data.get("guild_id")
+ )
+ )
+ )
+ self._http.cache.interactions.add(Build(id=command.name, value=command))
+
+ async def __bulk_update_sync(self, data: List[dict], delete: Optional[bool] = False) -> None:
"""
- while not self.websocket.closed:
- await self.websocket.connect(token, self.shard, self.presence)
+ Bulk updates a list of application commands during the synchronization process.
- def start(self) -> None:
- """Starts the client session."""
- self.loop.run_until_complete(self.ready())
+ The theory behind this is that instead of sending individual ``PATCH``
+ requests to the Web API, we collect the commands needed and do a bulk
+ overwrite instead. This is to mitigate the amount of calls, and hopefully,
+ chances of hitting rate limits during the readying state.
- async def ready(self) -> None:
+ :param data: The application commands to update.
+ :type data: List[dict]
+ :param delete?: Whether these commands are being deleted or not.
+ :type delete: Optional[bool]
+ """
+ guild_commands: dict = {}
+ global_commands: List[dict] = []
+
+ for command in data:
+ if command.get("guild_id"):
+ if guild_commands.get(command["guild_id"]):
+ guild_commands[command["guild_id"]].append(command)
+ else:
+ guild_commands[command["guild_id"]] = [command]
+ else:
+ global_commands.append(command)
+
+ self._http.cache.interactions.add(
+ Build(id=command["name"], value=ApplicationCommand(**command))
+ )
+
+ for guild, commands in guild_commands.items():
+ log.info(
+ f"Guild commands {', '.join(command['name'] for command in commands)} under ID {guild} have been {'deleted' if delete else 'synced'}."
+ )
+ await self._http.overwrite_application_command(
+ application_id=self.me.id,
+ data=[] if delete else commands,
+ guild_id=guild,
+ )
+
+ if global_commands:
+ log.info(
+ f"Global commands {', '.join(command['name'] for command in global_commands)} have been {'deleted' if delete else 'synced'}."
+ )
+ await self._http.overwrite_application_command(
+ application_id=self.me.id, data=[] if delete else global_commands
+ )
+
+ async def _synchronize(self, payload: Optional[dict] = None) -> None:
+ """
+ Synchronizes a command from the client-facing API to the Web API.
+
+ :ivar payload?: The application command to synchronize. Defaults to ``None`` where a global synchronization process begins.
+ :type payload: Optional[dict]
+ """
+ cache: Optional[List[dict]] = self._http.cache.interactions.view
+
+ if cache:
+ log.info("A command cache was detected, using for synchronization instead.")
+ commands: List[dict] = cache
+ else:
+ log.info("No command cache was found present, retrieving from Web API instead.")
+ commands: Optional[Union[dict, List[dict]]] = await self._http.get_application_command(
+ application_id=self.me.id, guild_id=payload.get("guild_id") if payload else None
+ )
+
+ names: List[str] = [command["name"] for command in commands] if commands else []
+ to_sync: list = []
+ to_delete: list = []
+
+ if payload:
+ log.info(f"Checking command {payload['name']}.")
+ if payload["name"] in names:
+ if not await self.__compare_sync(payload, commands):
+ to_sync.append(payload)
+ else:
+ await self.__create_sync(payload)
+ else:
+ for command in commands:
+ if command not in cache:
+ to_delete.append(command)
+
+ await self.__bulk_update_sync(to_sync)
+ await self.__bulk_update_sync(to_delete, delete=True)
+
+ async def _ready(self) -> None:
"""
Prepares the client with an internal "ready" check to ensure
that all conditions have been met in a chronological order:
@@ -136,163 +255,49 @@ async def ready(self) -> None:
"""
ready: bool = False
- def register_events() -> None:
- self.websocket.dispatch.register(self.raw_socket_create)
- self.websocket.dispatch.register(self.raw_channel_create, "on_channel_create")
- self.websocket.dispatch.register(self.raw_message_create, "on_message_create")
- self.websocket.dispatch.register(self.raw_message_create, "on_message_update")
- self.websocket.dispatch.register(self.raw_guild_create, "on_guild_create")
-
try:
- register_events()
- await self.synchronize()
+ self.__register_events()
+ if self._automate_sync:
+ await self._synchronize()
ready = True
except Exception as error:
log.critical(f"Could not prepare the client: {error}")
finally:
if ready:
log.debug("Client is now ready.")
- await self.login(self.token)
-
- async def synchronize(self, payload: Optional[ApplicationCommand] = None) -> None:
- """
- Synchronizes the command specified by checking through the
- currently registered application commands on the API and
- modifying if there is a detected chagne in structure.
-
- .. warning::
- This internal call does not need to be manually triggered,
- as it will automatically be done for you. Additionally,
- this will not delete unused commands for you.
-
- :param payload?: The payload/object of the command.
- :type payload: Optional[ApplicationCommand]
- """
- _guild = None
- if payload:
- _guild = str(payload.guild_id)
-
- commands: List[dict] = await self.http.get_application_command(
- application_id=self.me.id, guild_id=_guild
- )
- command_names: List[str] = [command["name"] for command in commands]
-
- async def create(data: ApplicationCommand) -> None:
- """
- Creates a new application command in the API if one does not exist for it.
- :param data: The data of the command to create.
- :type data: ApplicationCommand
- """
- log.debug(
- f"Command {data.name} was not found in the API, creating and adding to the cache."
- )
-
- request = await self.http.create_application_command(
- application_id=self.me.id, data=data._json, guild_id=data.guild_id
- )
-
- if request.get("code"):
- raise JSONException(request["code"])
- else:
- self.http.cache.interactions.add(Build(id=data.name, value=data))
-
- if commands:
- log.debug("Commands were found, checking for sync.")
- for command in commands:
- result: ApplicationCommand = ApplicationCommand(
- application_id=command.get("application_id"),
- id=command.get("id"),
- type=command.get("type"),
- guild_id=str(command["guild_id"]) if command.get("guild_id") else None,
- name=command.get("name"),
- description=command.get("description", ""),
- default_permission=command.get("default_permission", False),
- default_member_permissions=command.get("default_member_permissions", None),
- version=command.get("version"),
- name_localizations=command.get("name_localizations"),
- description_localizations=command.get("description_localizations"),
- )
+ await self._login()
- if payload:
- if payload.name in command_names:
- log.debug(f"Checking command {payload.name} for syncing.")
-
- if payload.name == result.name:
- payload_name: str = payload.name
-
- del result._json["name"]
- del payload._json["name"]
-
- if result._json != payload._json:
- log.debug(
- f"Command {result.name} found unsynced, editing in the API and updating the cache."
- )
- payload._json["name"] = payload_name
- request = await self.http.edit_application_command(
- application_id=self.me.id,
- data=payload._json,
- command_id=result.id,
- guild_id=result._json.get("guild_id"),
- )
- self.http.cache.interactions.add(
- Build(id=payload.name, value=payload)
- )
-
- if request.get("code"):
- raise JSONException(request["code"])
- break
- else:
- await create(payload)
- else:
- log.debug(f"Adding command {result.name} to cache.")
- self.http.cache.interactions.add(Build(id=result.name, value=result))
- else:
- if payload:
- await create(payload)
-
- cached_commands: List[dict] = [command for command in self.http.cache.interactions.view]
- cached_command_names = [command["name"] for command in cached_commands]
-
- if cached_commands:
- for command in commands:
- if command["name"] not in cached_command_names:
- log.debug(
- f"Command {command['name']} was found in the API but never cached, deleting from the API and cache."
- )
- request = await self.http.delete_application_command(
- application_id=self.me.id,
- command_id=command["id"],
- guild_id=command.get("guild_id"),
- )
+ async def _login(self) -> None:
+ """Makes a login with the Discord API."""
+ while not self._websocket.closed:
+ await self._websocket.connect(self._token, self._shard, self._presence)
- if request:
- if request.get("code"):
- raise JSONException(request["code"])
-
- def event(self, coro: Coroutine, name: Optional[str] = None) -> Callable[..., Any]:
+ def event(self, coro: Coroutine, name: Optional[str] = MISSING) -> Callable[..., Any]:
"""
- A decorator for listening to dispatched events from the
- gateway.
+ A decorator for listening to events dispatched from the
+ Gateway.
:param coro: The coroutine of the event.
:type coro: Coroutine
- :param name?: The name of the event.
+ :param name(?): The name of the event. If not given, this defaults to the coroutine's name.
:type name: Optional[str]
:return: A callable response.
:rtype: Callable[..., Any]
"""
- self.websocket.dispatch.register(coro, name=name)
+ self._websocket.dispatch.register(coro, name if name is not MISSING else coro.__name__)
return coro
def command(
self,
*,
type: Optional[Union[int, ApplicationCommandType]] = ApplicationCommandType.CHAT_INPUT,
- name: Optional[str] = None,
- description: Optional[str] = None,
- scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None,
- options: Optional[Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]] = None,
- default_permission: Optional[bool] = None,
+ name: Optional[str] = MISSING,
+ description: Optional[str] = MISSING,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ options: Optional[
+ Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]
+ ] = MISSING,
+ default_permission: Optional[bool] = MISSING,
) -> Callable[..., Any]:
"""
A decorator for registering an application command to the Discord API,
@@ -336,10 +341,10 @@ async def message_command(ctx):
"""
def decorator(coro: Coroutine) -> Callable[..., Any]:
- if not name:
+ if name is MISSING:
raise InteractionException(11, message="Your command must have a name.")
- if type == ApplicationCommandType.CHAT_INPUT and not description:
+ if type == ApplicationCommandType.CHAT_INPUT and description is MISSING:
raise InteractionException(
11, message="Chat-input commands must have a description."
)
@@ -348,12 +353,11 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
raise InteractionException(
11, message="Your command needs at least one argument to return context."
)
- if options:
- if (len(coro.__code__.co_varnames) + 1) < len(options):
- raise InteractionException(
- 11,
- message="You must have the same amount of arguments as the options of the command.",
- )
+ if options is not MISSING and len(coro.__code__.co_varnames) + 1 < len(options):
+ raise InteractionException(
+ 11,
+ message="You must have the same amount of arguments as the options of the command.",
+ )
commands: List[ApplicationCommand] = command(
type=type,
@@ -364,14 +368,122 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
default_permission=default_permission,
)
- if self.automate_sync:
- [self.loop.run_until_complete(self.synchronize(command)) for command in commands]
+ if self._automate_sync:
+ [self._loop.run_until_complete(self._synchronize(command)) for command in commands]
+
+ return self.event(coro, name=f"command_{name}")
+
+ return decorator
+
+ def message_command(
+ self,
+ *,
+ name: str,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ default_permission: Optional[bool] = MISSING,
+ ) -> Callable[..., Any]:
+ """
+ A decorator for registering a message context menu to the Discord API,
+ as well as being able to listen for ``INTERACTION_CREATE`` dispatched
+ gateway events.
+
+ The structure of a message context menu:
+
+ .. code-block:: python
+
+ @message_command(name="Context menu name")
+ async def context_menu_name(ctx):
+ ...
+
+ The ``scope`` kwarg field may also be used to designate the command in question
+ applicable to a guild or set of guilds.
+
+ :param name: The name of the application command.
+ :type name: Optional[str]
+ :param scope?: The "scope"/applicable guilds the application command applies to. Defaults to ``None``.
+ :type scope: Optional[Union[int, Guild, List[int], List[Guild]]]
+ :param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``.
+ :type default_permission: Optional[bool]
+ :return: A callable response.
+ :rtype: Callable[..., Any]
+ """
+
+ def decorator(coro: Coroutine) -> Callable[..., Any]:
+ if not len(coro.__code__.co_varnames):
+ raise InteractionException(
+ 11,
+ message="Your command needs at least one argument to return context.",
+ )
+
+ commands: List[ApplicationCommand] = command(
+ type=ApplicationCommandType.MESSAGE,
+ name=name,
+ scope=scope,
+ default_permission=default_permission,
+ )
+
+ if self._automate_sync:
+ [self._loop.run_until_complete(self._synchronize(command)) for command in commands]
- return self.event(coro, name=name)
+ return self.event(coro, name=f"command_{name}")
return decorator
- def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]:
+ def user_command(
+ self,
+ *,
+ name: str,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ default_permission: Optional[bool] = MISSING,
+ ) -> Callable[..., Any]:
+ """
+ A decorator for registering a user context menu to the Discord API,
+ as well as being able to listen for ``INTERACTION_CREATE`` dispatched
+ gateway events.
+
+ The structure of a user context menu:
+
+ .. code-block:: python
+
+ @user_command(name="Context menu name")
+ async def context_menu_name(ctx):
+ ...
+
+ The ``scope`` kwarg field may also be used to designate the command in question
+ applicable to a guild or set of guilds.
+
+ :param name: The name of the application command.
+ :type name: Optional[str]
+ :param scope?: The "scope"/applicable guilds the application command applies to. Defaults to ``None``.
+ :type scope: Optional[Union[int, Guild, List[int], List[Guild]]]
+ :param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``.
+ :type default_permission: Optional[bool]
+ :return: A callable response.
+ :rtype: Callable[..., Any]
+ """
+
+ def decorator(coro: Coroutine) -> Callable[..., Any]:
+ if not len(coro.__code__.co_varnames):
+ raise InteractionException(
+ 11,
+ message="Your command needs at least one argument to return context.",
+ )
+
+ commands: List[ApplicationCommand] = command(
+ type=ApplicationCommandType.USER,
+ name=name,
+ scope=scope,
+ default_permission=default_permission,
+ )
+
+ if self._automate_sync:
+ [self._loop.run_until_complete(self._synchronize(command)) for command in commands]
+
+ return self.event(coro, name=f"command_{name}")
+
+ return decorator
+
+ def component(self, component: Union[str, Button, SelectMenu]) -> Callable[..., Any]:
"""
A decorator for listening to ``INTERACTION_CREATE`` dispatched gateway
events involving components.
@@ -380,6 +492,7 @@ def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]:
.. code-block:: python
+ # Method 1
@component(interactions.Button(
style=interactions.ButtonStyle.PRIMARY,
label="click me!",
@@ -388,24 +501,33 @@ def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]:
async def button_response(ctx):
...
+ # Method 2
+ @component("custom_id")
+ async def button_response(ctx):
+ ...
+
The context of the component callback decorator inherits the same
as of the command decorator.
:param component: The component you wish to callback for.
- :type component: Union[Button, SelectMenu]
+ :type component: Union[str, Button, SelectMenu]
:return: A callable response.
:rtype: Callable[..., Any]
"""
def decorator(coro: Coroutine) -> Any:
payload: str = (
- _component(component).custom_id if isinstance(component, Component) else component
+ _component(component).custom_id
+ if isinstance(component, (Button, SelectMenu))
+ else component
)
- return self.event(coro, name=payload)
+ return self.event(coro, name=f"component_{payload}")
return decorator
- def autocomplete(self, name: str) -> Callable[..., Any]:
+ def autocomplete(
+ self, name: str, command: Union[ApplicationCommand, int]
+ ) -> Callable[..., Any]:
"""
A decorator for listening to ``INTERACTION_CREATE`` dispatched gateway
events involving autocompletion fields.
@@ -415,17 +537,22 @@ def autocomplete(self, name: str) -> Callable[..., Any]:
.. code-block:: python
@autocomplete("option_name")
- async def autocomplete_choice_list(ctx):
+ async def autocomplete_choice_list(ctx, user_input: str = ""):
await ctx.populate([...])
:param name: The name of the option to autocomplete.
:type name: str
+ :param command: The command or commnd ID with the option.
+ :type command: Union[ApplicationCommand, int]
:return: A callable response.
:rtype: Callable[..., Any]
"""
+ _command: Union[Snowflake, int] = (
+ command.id if isinstance(command, ApplicationCommand) else command
+ )
def decorator(coro: Coroutine) -> Any:
- return self.event(coro, name=f"autocomplete_{name}")
+ return self.event(coro, name=f"autocomplete_{_command}_{name}")
return decorator
@@ -436,7 +563,7 @@ def modal(self, modal: Modal) -> Callable[..., Any]:
.. error::
This feature is currently under experimental/**beta access**
- to those whitelisted for tetsing. Currently using this will
+ to those whitelisted for testing. Currently using this will
present you with an error with the modal not working.
The structure for a modal callback:
@@ -480,7 +607,7 @@ def load(self, name: str, package: Optional[str] = None) -> None:
"""
_name: str = resolve_name(name, package)
- if _name in self.extensions:
+ if _name in self._extensions:
log.error(f"Extension {name} has already been loaded. Skipping.")
module = import_module(name, package)
@@ -493,7 +620,7 @@ def load(self, name: str, package: Optional[str] = None) -> None:
log.error(f"Could not load {name}: {error}. Skipping.")
else:
log.debug(f"Loaded extension {name}.")
- self.extensions[_name] = module
+ self._extensions[_name] = module
def remove(self, name: str, package: Optional[str] = None) -> None:
"""
@@ -505,20 +632,14 @@ def remove(self, name: str, package: Optional[str] = None) -> None:
:type package: Optional[str]
"""
_name: str = resolve_name(name, package)
- module = self.extensions.get(_name)
+ module = self._extensions.get(_name)
- if module not in self.extensions:
+ if module not in self._extensions:
log.error(f"Extension {name} has not been loaded before. Skipping.")
- try:
- teardown = getattr(module, "teardown")
- teardown()
- except AttributeError:
- pass
- else:
- log.debug(f"Removed extension {name}.")
- del sys.modules[_name]
- del self.extensions[_name]
+ log.debug(f"Removed extension {name}.")
+ del sys.modules[_name]
+ del self._extensions[_name]
def reload(self, name: str, package: Optional[str] = None) -> None:
"""
@@ -530,7 +651,7 @@ def reload(self, name: str, package: Optional[str] = None) -> None:
:type package: Optional[str]
"""
_name: str = resolve_name(name, package)
- module = self.extensions.get(_name)
+ module = self._extensions.get(_name)
if module is None:
log.warning(f"Extension {name} could not be reloaded because it was never loaded.")
@@ -539,7 +660,7 @@ def reload(self, name: str, package: Optional[str] = None) -> None:
self.remove(name, package)
self.load(name, package)
- async def raw_socket_create(self, data: Dict[Any, Any]) -> Dict[Any, Any]:
+ async def __raw_socket_create(self, data: Dict[Any, Any]) -> Dict[Any, Any]:
"""
This is an internal function that takes any gateway socket event
and then returns the data purely based off of what it does in
@@ -553,7 +674,7 @@ async def raw_socket_create(self, data: Dict[Any, Any]) -> Dict[Any, Any]:
return data
- async def raw_channel_create(self, channel) -> dict:
+ async def __raw_channel_create(self, channel) -> dict:
"""
This is an internal function that caches the channel creates when dispatched.
@@ -562,11 +683,11 @@ async def raw_channel_create(self, channel) -> dict:
:return: The channel as a dictionary of raw data.
:rtype: dict
"""
- self.http.cache.channels.add(Build(id=channel.id, value=channel))
+ self._http.cache.channels.add(Build(id=channel.id, value=channel))
return channel._json
- async def raw_message_create(self, message) -> dict:
+ async def __raw_message_create(self, message) -> dict:
"""
This is an internal function that caches the message creates when dispatched.
@@ -575,11 +696,11 @@ async def raw_message_create(self, message) -> dict:
:return: The message as a dictionary of raw data.
:rtype: dict
"""
- self.http.cache.messages.add(Build(id=message.id, value=message))
+ self._http.cache.messages.add(Build(id=message.id, value=message))
return message._json
- async def raw_guild_create(self, guild) -> dict:
+ async def __raw_guild_create(self, guild) -> dict:
"""
This is an internal function that caches the guild creates on ready.
@@ -588,47 +709,52 @@ async def raw_guild_create(self, guild) -> dict:
:return: The guild as a dictionary of raw data.
:rtype: dict
"""
- self.http.cache.guilds.add(Build(id=str(guild.id), value=guild))
+ self._http.cache.self_guilds.add(Build(id=str(guild.id), value=guild))
return guild._json
# TODO: Implement the rest of cog behaviour when possible.
-# class Extension:
-# """
-# A class that allows you to represent "extensions" of your code, or
-# essentially cogs that can be ran independent of the root file in
-# an object-oriented structure.
-
-# The structure of an extension:
-
-# .. code-block:: python
-
-# class CoolCode(interactions.Extension):
-# def __init__(self, client):
-# self.client = client
-
-# @command(
-# type=interactions.ApplicationCommandType.USER,
-# name="User command in cog",
-# )
-# async def cog_user_cmd(self, ctx):
-# ...
-
-# def setup(bot):
-# CoolCode(bot)
-# """
-
-# client: Client
-# commands: Optional[List[ApplicationCommand]]
-# listeners: Optional[List[Listener]]
-
-# def __new__(cls, bot: Client) -> None:
-# cls.client = bot
-# cls.commands = []
-
-# for _, content in cls.__dict__.items():
-# content = content if isinstance(content.callback, partial) else None
-# if isinstance(content, ApplicationCommand):
-# cls.commands.append(content)
-# bot.command(**content)
+class Extension:
+ """
+ A class that allows you to represent "extensions" of your code, or
+ essentially cogs that can be ran independent of the root file in
+ an object-oriented structure.
+
+ The structure of an extension:
+
+ .. code-block:: python
+
+ class CoolCode(interactions.Extension):
+ def __init__(self, client):
+ self.client = client
+
+ @command(
+ type=interactions.ApplicationCommandType.USER,
+ name="User command in cog",
+ )
+ async def cog_user_cmd(self, ctx):
+ ...
+
+ def setup(bot):
+ CoolCode(bot)
+ """
+
+ client: Client
+ commands: Optional[List[ApplicationCommand]]
+ listeners: Optional[List[Listener]]
+
+ def __new__(cls, bot: Client) -> None:
+ cls.client = bot
+ cls.commands = []
+ cls.listeners = []
+
+ for _, content in cls.__dict__.items():
+ if not content.startswith("__") or content.startswith("_"):
+ if "on_" in content:
+ cls.listeners.append(content)
+ else:
+ cls.commands.append(content)
+
+ for _command in cls.commands:
+ cls.client.command(**_command)
diff --git a/interactions/client.pyi b/interactions/client.pyi
index 1e8114d60..8bec5e6b3 100644
--- a/interactions/client.pyi
+++ b/interactions/client.pyi
@@ -1,13 +1,15 @@
from asyncio import AbstractEventLoop
-from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
+from types import ModuleType
+from typing import Any, Callable, Coroutine, Dict, List, NoReturn, Optional, Tuple, Union
-from interactions.api.models.gw import Presence
+from .api.models.gw import Presence
+from .models.misc import MISSING
from .api.cache import Cache
from .api.gateway import WebSocket
from .api.http import HTTPClient
from .api.models.guild import Guild
-from .api.models.intents import Intents
+from .api.models.flags import Intents
from .api.models.team import Application
from .enums import ApplicationCommandType
from .models.command import ApplicationCommand, Option
@@ -17,39 +19,55 @@ _token: str = "" # noqa
_cache: Optional[Cache] = None
class Client:
- loop: AbstractEventLoop
- intents: Optional[Union[Intents, List[Intents]]]
- http: HTTPClient
- websocket: WebSocket
+ _loop: AbstractEventLoop
+ _http: HTTPClient
+ _websocket: WebSocket
+ _intents: Intents
+ _shard: Optional[List[Tuple[int]]]
+ _presence: Optional[Presence]
+ _token: str
+ _automate_sync: bool
+ _extensions: Optional[Dict[str, ModuleType]]
me: Optional[Application]
- token: str
- automate_sync: Optional[bool]
- shard: Optional[List[int]]
- presence: Optional[Presence]
- extensions: Optional[Any]
def __init__(
self,
token: str,
- intents: Optional[Union[Intents, List[Intents]]] = Intents.DEFAULT,
- disable_sync: Optional[bool] = None,
- log_level: Optional[int] = None,
- shard: Optional[List[int]] = None,
- presence: Optional[Presence] = None,
+ **kwargs,
) -> None: ...
- async def login(self, token: str) -> None: ...
def start(self) -> None: ...
- async def ready(self) -> None: ...
- async def synchronize(self, payload: Optional[ApplicationCommand] = None) -> None: ...
+ def __register_events(self) -> None: ...
+ async def __compare_sync(self, data: dict) -> None: ...
+ async def __create_sync(self, data: dict) -> None: ...
+ async def __bulk_update_sync(
+ self, data: List[dict], delete: Optional[bool] = False
+ ) -> None: ...
+ async def _synchronize(self, payload: Optional[dict] = None) -> None: ...
+ async def _ready(self) -> None: ...
+ async def _login(self) -> None: ...
def event(self, coro: Coroutine, name: Optional[str] = None) -> Callable[..., Any]: ...
def command(
self,
*,
type: Optional[Union[str, int, ApplicationCommandType]] = ApplicationCommandType.CHAT_INPUT,
- name: Optional[str] = None,
- description: Optional[str] = None,
- scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None,
- options: Optional[List[Option]] = None,
- default_permission: Optional[bool] = None,
+ name: Optional[str] = MISSING,
+ description: Optional[str] = MISSING,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ options: Optional[List[Option]] = MISSING,
+ default_permission: Optional[bool] = MISSING,
+ ) -> Callable[..., Any]: ...
+ def message_command(
+ self,
+ *,
+ name: str,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ default_permission: Optional[bool] = MISSING,
+ ) -> Callable[..., Any]: ...
+ def user_command(
+ self,
+ *,
+ name: str,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ default_permission: Optional[bool] = MISSING,
) -> Callable[..., Any]: ...
def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]: ...
def autocomplete(self, name: str) -> Callable[..., Any]: ...
diff --git a/interactions/context.py b/interactions/context.py
index 5f342ec93..d8f65820e 100644
--- a/interactions/context.py
+++ b/interactions/context.py
@@ -1,4 +1,4 @@
-from logging import Logger, StreamHandler, basicConfig, getLogger
+from logging import Logger
from typing import List, Optional, Union
from .api.models.channel import Channel
@@ -7,18 +7,13 @@
from .api.models.message import Embed, Message, MessageInteraction, MessageReference
from .api.models.misc import DictSerializerMixin, Snowflake
from .api.models.user import User
-from .base import CustomFormatter, Data
+from .base import get_logger
from .enums import InteractionCallbackType, InteractionType
from .models.command import Choice
-from .models.component import ActionRow, Button, Component, Modal, SelectMenu
+from .models.component import ActionRow, Button, Modal, SelectMenu
from .models.misc import InteractionData
-basicConfig(level=Data.LOGGER)
-log: Logger = getLogger("context")
-stream: StreamHandler = StreamHandler()
-stream.setLevel(Data.LOGGER)
-stream.setFormatter(CustomFormatter())
-log.addHandler(stream)
+log: Logger = get_logger("context")
class Context(DictSerializerMixin):
@@ -40,10 +35,16 @@ class Context(DictSerializerMixin):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
- self.message = Message(**self.message) if self._json.get("message") else None
- self.member = Member(**self.member) if self._json.get("member") else None
+ self.message = (
+ Message(**self.message, _client=self.client) if self._json.get("message") else None
+ )
+ self.member = (
+ Member(**self.member, _client=self.client) if self._json.get("member") else None
+ )
self.author = self.member
self.user = User(**self.user) if self._json.get("user") else None
+
+ # TODO: The below attributes are always None because they aren't by API return.
self.channel = Channel(**self.channel) if self._json.get("channel") else None
self.guild = Guild(**self.guild) if self._json.get("guild") else None
@@ -73,6 +74,8 @@ class CommandContext(Context):
:ivar Optional[List[Option]] options?: The options of the command in the interaction, if any.
:ivar InteractionData data: The application command data.
:ivar str token: The token of the interaction response.
+ :ivar Snowflake channel_id: The ID of the current channel.
+ :ivar Snowflake guild_id: The ID of the current guild.
:ivar bool responded: Whether an original response was made or not.
:ivar bool deferred: Whether the response was deferred or not.
"""
@@ -97,6 +100,9 @@ class CommandContext(Context):
"channel_id",
"responded",
"deferred",
+ #
+ "locale",
+ "guild_locale",
)
def __init__(self, **kwargs) -> None:
@@ -153,7 +159,9 @@ async def send(
# attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
embeds: Optional[Union[Embed, List[Embed]]] = None,
allowed_mentions: Optional[MessageInteraction] = None,
- components: Optional[Union[Component, List[Component]]] = None,
+ components: Optional[
+ Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]
+ ] = None,
ephemeral: Optional[bool] = False,
) -> Message:
"""
@@ -169,33 +177,138 @@ async def send(
:param allowed_mentions?: The message interactions/mention limits that the message can refer to.
:type allowed_mentions: Optional[MessageInteraction]
:param components?: A component, or list of components for the message.
- :type components: Optional[Union[Component, List[Component]]]
+ :type components: Optional[Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]]
:param ephemeral?: Whether the response is hidden or not.
:type ephemeral: Optional[bool]
:return: The sent message as an object.
:rtype: Message
"""
- _content: str = "" if content is None else content
+ if (
+ content is None
+ and self.message
+ and self.callback == InteractionCallbackType.DEFERRED_UPDATE_MESSAGE
+ ):
+ _content = self.message.content
+ else:
+ _content: str = "" if content is None else content
_tts: bool = False if tts is None else tts
# _file = None if file is None else file
# _attachments = [] if attachments else None
- _embeds: list = (
- []
- if embeds is None
- else ([embed._json for embed in embeds] if isinstance(embeds, list) else [embeds._json])
- )
+ if embeds is None and self.message:
+ _embeds = self.message.embeds
+ else:
+ _embeds: list = (
+ []
+ if embeds is None
+ else (
+ [embed._json for embed in embeds]
+ if isinstance(embeds, list)
+ else [embeds._json]
+ )
+ )
_allowed_mentions: dict = {} if allowed_mentions is None else allowed_mentions
- _components: list = [{"type": 1, "components": []}]
-
- if isinstance(components, ActionRow):
- _components[0]["components"] = [component._json for component in components.components]
- elif isinstance(components, Button):
- _components[0]["components"] = [] if components is None else [components._json]
- elif isinstance(components, SelectMenu):
- components._json["options"] = [option._json for option in components.options]
- _components[0]["components"] = [] if components is None else [components._json]
+ _components: List[dict] = [{"type": 1, "components": []}]
+
+ # TODO: Break this obfuscation pattern down to a "builder" method.
+ if components:
+ if isinstance(components, list) and all(
+ isinstance(action_row, ActionRow) for action_row in components
+ ):
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in action_row.components
+ ],
+ }
+ for action_row in components
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(component, (Button, SelectMenu)) for component in components
+ ):
+ for component in components:
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in component._json["options"]
+ ]
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components
+ ],
+ }
+ ]
+ elif isinstance(components, list) and all(
+ isinstance(action_row, (list, ActionRow)) for action_row in components
+ ):
+ _components = []
+ for action_row in components:
+ for component in (
+ action_row if isinstance(action_row, list) else action_row.components
+ ):
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ option._json for option in component.options
+ ]
+ _components.append(
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id")
+ or component._json.get("url")
+ else []
+ )
+ for component in (
+ action_row
+ if isinstance(action_row, list)
+ else action_row.components
+ )
+ ],
+ }
+ )
+ elif isinstance(components, ActionRow):
+ _components[0]["components"] = [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components.components
+ ]
+ elif isinstance(components, Button):
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ elif isinstance(components, SelectMenu):
+ components._json["options"] = [
+ options._json if not isinstance(options, dict) else options
+ for options in components._json["options"]
+ ]
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ elif components is None and self.message:
+ _components = self.message.components
else:
- _components = [] if components is None else [components]
+ _components = []
_ephemeral: int = (1 << 6) if ephemeral else 0
@@ -221,6 +334,7 @@ async def send(
flags=_ephemeral,
)
self.message = payload
+ self.message._client = self.client
_payload: dict = {"type": self.callback.value, "data": payload._json}
async def func():
@@ -242,7 +356,7 @@ async def func():
application_id=str(self.application_id),
)
self.responded = True
- self.message = Message(**res)
+ self.message = Message(**res, _client=self.client)
else:
await self.client._post_followup(
data=payload._json,
@@ -269,7 +383,9 @@ async def edit(
embeds: Optional[Union[Embed, List[Embed]]] = None,
allowed_mentions: Optional[MessageInteraction] = None,
message_reference: Optional[MessageReference] = None,
- components: Optional[Union[ActionRow, Button, SelectMenu]] = None,
+ components: Optional[
+ Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]
+ ] = None,
) -> Message:
"""
This allows the invocation state described in the "context"
@@ -280,28 +396,121 @@ async def edit(
:return: The edited message as an object.
:rtype: Message
"""
- _content: str = "" if content is None else content
+ _content: str = self.message.content if content is None else content
_tts: bool = False if tts is None else tts
# _file = None if file is None else file
- _embeds: list = (
- []
- if embeds is None
- else ([embed._json for embed in embeds] if isinstance(embeds, list) else [embeds._json])
- )
+
+ if embeds is None:
+ _embeds = self.message.embeds
+ else:
+ _embeds: list = (
+ []
+ if embeds is None
+ else (
+ [embed._json for embed in embeds]
+ if isinstance(embeds, list)
+ else [embeds._json]
+ )
+ )
_allowed_mentions: dict = {} if allowed_mentions is None else allowed_mentions
_message_reference: dict = {} if message_reference is None else message_reference._json
- _components: list = [{"type": 1, "components": []}]
-
- if isinstance(components, ActionRow):
- _components[0]["components"] = [component._json for component in components.components]
- elif isinstance(components, Button):
- _components[0]["components"] = [] if components is None else [components._json]
- elif isinstance(components, SelectMenu):
- components._json["options"] = [option._json for option in components.options]
- _components[0]["components"] = [] if components is None else [components._json]
- else:
+ if components is None:
+ _components = self.message.components
+ elif components == []:
_components = []
+ else:
+ _components: list = [{"type": 1, "components": []}]
+ if (
+ isinstance(components, list)
+ and components
+ and all(isinstance(action_row, ActionRow) for action_row in components)
+ ):
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in action_row.components
+ ],
+ }
+ for action_row in components
+ ]
+ elif (
+ isinstance(components, list)
+ and components
+ and all(isinstance(component, (Button, SelectMenu)) for component in components)
+ ):
+ if isinstance(components[0], SelectMenu):
+ components[0]._json["options"] = [
+ option._json for option in components[0].options
+ ]
+ _components = [
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components
+ ],
+ }
+ ]
+ elif (
+ isinstance(components, list)
+ and components
+ and all(isinstance(action_row, (list, ActionRow)) for action_row in components)
+ ):
+ _components = []
+ for action_row in components:
+ for component in (
+ action_row if isinstance(action_row, list) else action_row.components
+ ):
+ if isinstance(component, SelectMenu):
+ component._json["options"] = [
+ option._json for option in component.options
+ ]
+ _components.append(
+ {
+ "type": 1,
+ "components": [
+ (
+ component._json
+ if component._json.get("custom_id")
+ or component._json.get("url")
+ else []
+ )
+ for component in (
+ action_row
+ if isinstance(action_row, list)
+ else action_row.components
+ )
+ ],
+ }
+ )
+ elif isinstance(components, ActionRow):
+ _components[0]["components"] = [
+ (
+ component._json
+ if component._json.get("custom_id") or component._json.get("url")
+ else []
+ )
+ for component in components.components
+ ]
+ elif isinstance(components, (Button, SelectMenu)):
+ _components[0]["components"] = (
+ [components._json]
+ if components._json.get("custom_id") or components._json.get("url")
+ else []
+ )
+ else:
+ _components = []
payload: Message = Message(
content=_content,
@@ -314,7 +523,16 @@ async def edit(
)
async def func():
- if self.deferred:
+ if not self.deferred and self.type == InteractionType.MESSAGE_COMPONENT:
+ self.callback = InteractionCallbackType.UPDATE_MESSAGE
+ await self.client.create_interaction_response(
+ data={"type": self.callback.value, "data": payload._json},
+ token=self.token,
+ application_id=int(self.id),
+ )
+ self.message = payload
+ self.responded = True
+ elif self.deferred:
if (
self.type == InteractionType.MESSAGE_COMPONENT
and self.callback != InteractionCallbackType.DEFERRED_UPDATE_MESSAGE
@@ -324,38 +542,38 @@ async def func():
token=self.token,
application_id=str(self.application_id),
)
+ elif (
+ self.callback == InteractionCallbackType.DEFERRED_UPDATE_MESSAGE
+ and self.type == InteractionType.MESSAGE_COMPONENT
+ ):
+ res = await self.client.edit_interaction_response(
+ data=payload._json,
+ token=self.token,
+ application_id=str(self.application_id),
+ )
+ self.responded = True
+ self.message = Message(**res, _client=self.client)
+ elif hasattr(self.message, "id") and self.message.id is not None:
+ res = await self.client.edit_message(
+ int(self.channel_id), int(self.message.id), payload=payload._json
+ )
+ self.message = Message(**res, _client=self.client)
else:
- if (
- self.callback == InteractionCallbackType.DEFERRED_UPDATE_MESSAGE
- and self.type == InteractionType.MESSAGE_COMPONENT
- ):
- res = await self.client.edit_interaction_response(
- data=payload._json,
- token=self.token,
- application_id=str(self.application_id),
- )
- self.responded = True
- self.message = Message(**res)
- elif hasattr(self.message, "id") and self.message.id is not None:
- res = await self.client.edit_message(
- int(self.channel_id), int(self.message.id), payload=payload._json
- )
- self.message = Message(**res)
+ res = await self.client.edit_interaction_response(
+ token=self.token,
+ application_id=str(self.id),
+ data={"type": self.callback.value, "data": payload._json},
+ message_id=self.message.id if self.message else "@original",
+ )
+ if res["flags"] == 64:
+ log.warning("You can't edit hidden messages.")
+ self.message = payload
+ self.message._client = self.client
else:
- res = await self.client.edit_interaction_response(
- token=self.token,
- application_id=str(self.id),
- data={"type": self.callback.value, "data": payload._json},
- message_id=self.message.id if self.message else "@original",
+ await self.client.edit_message(
+ int(self.channel_id), res["id"], payload=payload._json
)
- if res["flags"] == 64:
- log.warning("You can't edit hidden messages.")
- self.message = payload
- else:
- await self.client.edit_message(
- int(self.channel_id), res["id"], payload=payload._json
- )
- self.message = Message(**res)
+ self.message = Message(**res, _client=self.client)
else:
self.callback = (
InteractionCallbackType.UPDATE_MESSAGE
@@ -373,7 +591,7 @@ async def func():
await self.client.edit_message(
int(self.channel_id), res["id"], payload=payload._json
)
- self.message = Message(**res)
+ self.message = Message(**res, _client=self.client)
await func()
return payload
@@ -415,12 +633,11 @@ async def func():
_choices: list = []
if all(isinstance(choice, Choice) for choice in choices):
_choices = [choice._json for choice in choices]
- # elif all(isinstance(choice, Dict[str, Any]) for choice in choices):
elif all(
isinstance(choice, dict) and all(isinstance(x, str) for x in choice)
for choice in choices
):
- _choices = [choice for choice in choices]
+ _choices = list(choices)
elif isinstance(choices, Choice):
_choices = [choices._json]
else:
@@ -490,6 +707,9 @@ class ComponentContext(CommandContext):
"channel_id",
"responded",
"deferred",
+ #
+ "locale",
+ "guild_locale",
)
def __init__(self, **kwargs) -> None:
@@ -501,7 +721,7 @@ async def defer(
self, ephemeral: Optional[bool] = False, edit_origin: Optional[bool] = False
) -> None:
"""
- This "defers" an component response, allowing up
+ This "defers" a component response, allowing up
to a 15-minute delay between invocation and responding.
:param ephemeral?: Whether the deferred state is hidden or not.
@@ -512,10 +732,11 @@ async def defer(
self.deferred = True
_ephemeral: int = (1 << 6) if bool(ephemeral) else 0
# ephemeral doesn't change callback typings. just data json
- if self.type == InteractionType.MESSAGE_COMPONENT and edit_origin:
- self.callback = InteractionCallbackType.DEFERRED_UPDATE_MESSAGE
- elif self.type == InteractionType.MESSAGE_COMPONENT and not edit_origin:
- self.callback = InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
+ if self.type == InteractionType.MESSAGE_COMPONENT:
+ if edit_origin:
+ self.callback = InteractionCallbackType.DEFERRED_UPDATE_MESSAGE
+ else:
+ self.callback = InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
await self.client.create_interaction_response(
token=self.token,
diff --git a/interactions/context.pyi b/interactions/context.pyi
index aaa397a26..ab622c865 100644
--- a/interactions/context.pyi
+++ b/interactions/context.pyi
@@ -35,6 +35,8 @@ class CommandContext(Context):
channel_id: Snowflake
responded: bool
deferred: bool
+ locale: str
+ guild_locale: str
def __init__(self, **kwargs) -> None: ...
async def defer(self, ephemeral: Optional[bool] = None) -> None: ...
async def send(
@@ -45,7 +47,9 @@ class CommandContext(Context):
# attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
embeds: Optional[Union[Embed, List[Embed]]] = None,
allowed_mentions: Optional[MessageInteraction] = None,
- components: Optional[Union[Component, List[Component]]] = None,
+ components: Optional[
+ Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]
+ ] = None,
ephemeral: Optional[bool] = False,
) -> Message: ...
async def edit(
@@ -56,7 +60,9 @@ class CommandContext(Context):
# attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
embeds: Optional[Union[Embed, List[Embed]]] = None,
allowed_mentions: Optional[MessageInteraction] = None,
- components: Optional[Union[Component, List[Component]]] = None,
+ components: Optional[
+ Union[ActionRow, Button, SelectMenu, List[Union[ActionRow, Button, SelectMenu]]]
+ ] = None,
) -> Message: ...
async def delete(self) -> None: ...
async def popup(self, modal: Modal): ...
diff --git a/interactions/decor.py b/interactions/decor.py
index 56ddd8f8b..e33da6b52 100644
--- a/interactions/decor.py
+++ b/interactions/decor.py
@@ -1,27 +1,26 @@
from typing import Any, Dict, List, Optional, Union
-from interactions.models.component import Button, SelectMenu
-
from .api.models.guild import Guild
from .enums import ApplicationCommandType
from .models.command import ApplicationCommand, Option
-from .models.component import Component
+from .models.component import Button, Component, SelectMenu
+from .models.misc import MISSING
def command(
*,
type: Optional[Union[int, ApplicationCommandType]] = ApplicationCommandType.CHAT_INPUT,
- name: Optional[str] = None,
- description: Optional[str] = None,
- scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None,
- options: Optional[Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]] = None,
- default_permission: Optional[bool] = None,
+ name: Optional[str] = MISSING,
+ description: Optional[str] = MISSING,
+ scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING,
+ options: Optional[Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]] = MISSING,
+ default_permission: Optional[bool] = MISSING,
) -> List[ApplicationCommand]:
"""
A wrapper designed to interpret the client-facing API for
how a command is to be created and used.
- :return: A list of command paylods.
+ :return: A list of command payloads.
:rtype: List[ApplicationCommand]
"""
_type: int = 0
@@ -30,46 +29,28 @@ def command(
else:
_type: int = ApplicationCommandType(type).value
- _description: str = "" if description is None else description
+ _description: str = "" if description is MISSING else description
_options: list = []
- if options:
+ if options is not MISSING:
if all(isinstance(option, Option) for option in options):
_options = [option._json for option in options]
elif all(
isinstance(option, dict) and all(isinstance(value, str) for value in option)
for option in options
):
- _options = [option for option in options]
+ _options = list(options)
elif isinstance(options, Option):
_options = [options._json]
else:
_options = [options]
- _default_permission: bool = True if default_permission is None else default_permission
-
- # TODO: Implement permission building and syncing.
- # _permissions: list = []
-
- # if permissions:
- # if all(isinstance(permission, Permission) for permission in permissions):
- # _permissions = [permission._json for permission in permissions]
- # elif all(
- # isinstance(permission, dict)
- # and all(isinstance(value, str) for value in permission)
- # for permission in permissions
- # ):
- # _permissions = [permission for permission in permissions]
- # elif isinstance(permissions, Permission):
- # _permissions = [permissions._json]
- # else:
- # _permissions = [permissions]
-
+ _default_permission: bool = True if default_permission is MISSING else default_permission
_scope: list = []
payloads: list = []
- if scope:
+ if scope is not MISSING:
if isinstance(scope, list):
if all(isinstance(guild, Guild) for guild in scope):
[_scope.append(guild.id) for guild in scope]
@@ -88,7 +69,7 @@ def command(
options=_options,
default_permission=_default_permission,
)
- payloads.append(payload)
+ payloads.append(payload._json)
else:
payload: ApplicationCommand = ApplicationCommand(
type=_type,
@@ -97,7 +78,7 @@ def command(
options=_options,
default_permission=_default_permission,
)
- payloads.append(payload)
+ payloads.append(payload._json)
return payloads
@@ -110,5 +91,4 @@ def component(component: Union[Button, SelectMenu]) -> Component:
:return: A component.
:rtype: Component
"""
- payload: Component = Component(**component._json)
- return payload
+ return Component(**component._json)
diff --git a/interactions/enums.py b/interactions/enums.py
index b05d597df..1c5ce22ce 100644
--- a/interactions/enums.py
+++ b/interactions/enums.py
@@ -1,4 +1,4 @@
-from enum import IntEnum
+from enum import Enum, IntEnum
class ApplicationCommandType(IntEnum):
@@ -99,7 +99,7 @@ class PermissionType(IntEnum):
class ComponentType(IntEnum):
"""
- An numerable object representing the types of a component.
+ An enumerable object representing the types of a component.
:ivar ACTION_ROW: 1
:ivar BUTTON: 2
@@ -141,3 +141,16 @@ class TextStyleType(IntEnum):
SHORT = 1
PARAGRAPH = 2
+
+
+# TODO: Move this to flags.py after # 420
+class StatusType(str, Enum):
+ """
+ A string enum representing Discord status icons that a user may have.
+ """
+
+ ONLINE = "online"
+ DND = "dnd"
+ IDLE = "idle"
+ INVISIBLE = "invisible"
+ OFFLINE = "offline"
diff --git a/interactions/models/command.py b/interactions/models/command.py
index 86d067db7..bec5d4afd 100644
--- a/interactions/models/command.py
+++ b/interactions/models/command.py
@@ -50,7 +50,7 @@ class Option(DictSerializerMixin):
interactions.Option(
type=interactions.OptionType.STRING,
name="option_name",
- description="i'm a meaningless option in your life. (depressed noisese)",
+ description="i'm a meaningless option in your life. (depressed noises)",
required=True,
choices=[interactions.Choice(...)], # optional
)
@@ -63,7 +63,7 @@ class Option(DictSerializerMixin):
:ivar Optional[str] value?: The value that's currently typed out, if autocompleting.
:ivar Optional[List[Choice]] choices?: The list of choices to select from.
:ivar Optional[List[Option]] options?: The list of subcommand options included.
- :ivar Optional[List[ChannelType] channel_types?: Restrictive shown channel types, if given.
+ :ivar Optional[List[ChannelType]] channel_types?: Restrictive shown channel types, if given.
:ivar Optional[int] min_value?: The minimum value supported by the option.
:ivar Optional[int] max_value?: The maximum value supported by the option.
:ivar Optional[bool] autocomplete?: A status denoting whether this option is an autocomplete option.
@@ -109,14 +109,20 @@ def __init__(self, **kwargs) -> None:
self._json.update({"type": self.type.value})
if self._json.get("options"):
if all(isinstance(option, dict) for option in self.options):
- self._json["options"] = [option for option in self.options]
+ self._json["options"] = list(self.options)
else:
- self._json["options"] = [option._json for option in self.options]
- if self._json.get("choices"):
- if isinstance(self._json.get("choices"), dict):
- self._json["choices"] = [choice for choice in self.choices]
- else:
- self._json["choices"] = [choice._json for choice in self.choices]
+ self._json["options"] = [
+ option if isinstance(option, dict) else option._json for option in self.options
+ ]
+ if self.choices:
+ if all(isinstance(choice, dict) for choice in self.choices):
+ if isinstance(self._json.get("choices"), dict):
+ self._json["choices"] = list(self.choices)
+ else:
+ self._json["choices"] = [
+ choice if isinstance(choice, dict) else choice._json
+ for choice in self.choices
+ ]
class Permission(DictSerializerMixin):
diff --git a/interactions/models/component.py b/interactions/models/component.py
index 1377426a1..b45cd26e7 100644
--- a/interactions/models/component.py
+++ b/interactions/models/component.py
@@ -85,7 +85,12 @@ def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.type = ComponentType.SELECT
self.options = (
- [SelectOption(**option._json) for option in self.options]
+ [
+ SelectOption(**option._json)
+ if isinstance(option, SelectOption)
+ else SelectOption(**option)
+ for option in self.options
+ ]
if self._json.get("options")
else None
)
@@ -109,7 +114,7 @@ class Button(DictSerializerMixin):
:ivar ComponentType type: The type of button. Always defaults to ``2``.
:ivar ButtonStyle style: The style of the button.
:ivar str label: The label of the button.
- :ivar Optional[Emoji] emoji?: The emoji used alongside the laebl of the button.
+ :ivar Optional[Emoji] emoji?: The emoji used alongside the label of the button.
:ivar Optional[str] custom_id?: The customized "ID" of the button.
:ivar Optional[str] url?: The URL route/path of the button.
:ivar Optional[bool] disabled?: Whether the button is unable to be used.
@@ -192,7 +197,14 @@ def __init__(self, **kwargs) -> None:
self.type = ComponentType(self.type)
self.style = ButtonStyle(self.style) if self._json.get("style") else None
self.options = (
- [SelectMenu(**option) for option in self.options] if self._json.get("options") else None
+ [
+ SelectOption(**option._json)
+ if isinstance(option, SelectOption)
+ else SelectOption(**option)
+ for option in self.options
+ ]
+ if self._json.get("options")
+ else None
)
if self._json.get("components"):
self._json["components"] = [component._json for component in self.components]
@@ -327,7 +339,7 @@ def __init__(self, **kwargs) -> None:
self.type = ComponentType.ACTION_ROW
for component in self.components:
if isinstance(component, SelectMenu):
- component._json["options"] = [option._json for option in component.options]
+ component._json["options"] = [option._json for option in component._json["options"]]
self.components = (
[Component(**component._json) for component in self.components]
if self._json.get("components")
diff --git a/interactions/models/misc.py b/interactions/models/misc.py
index 1393cd1d3..435dbb5bf 100644
--- a/interactions/models/misc.py
+++ b/interactions/models/misc.py
@@ -8,7 +8,6 @@
from ..api.models.user import User
from ..enums import ApplicationCommandType, ComponentType, InteractionType
from ..models.command import Option
-from ..models.component import SelectOption
class InteractionResolvedData(DictSerializerMixin):
@@ -37,26 +36,36 @@ def __init__(self, **kwargs):
self.users.update({user: User(**self.users[user])})
for user in self._json.get("users")
]
+ else:
+ self.users = {}
if self._json.get("members"):
[
self.members.update({member: Member(**self.members[member])})
for member in self._json.get("members")
]
+ else:
+ self.members = {}
if self._json.get("roles"):
[
self.roles.update({role: Role(**self.roles[role])})
for role in self._json.get("roles")
]
+ else:
+ self.roles = {}
if self._json.get("channels"):
[
self.channels.update({channel: Channel(**self.channels[channel])})
for channel in self._json.get("channels")
]
+ else:
+ self.channels = {}
if self._json.get("messages"):
[
self.messages.update({message: Message(**self.messages[message])})
for message in self._json.get("messages")
]
+ else:
+ self.messages = {}
class InteractionData(DictSerializerMixin):
@@ -70,7 +79,7 @@ class InteractionData(DictSerializerMixin):
:ivar Optional[Option, List[Option]] options?: The options of the interaction.
:ivar Optional[str] custom_id?: The custom ID of the interaction.
:ivar Optional[ComponentType] component_type?: The type of component from the interaction.
- :ivar Optional[List[SelectOption]] values?: The values of the selected options in the interaction.
+ :ivar Optional[List[str]] values?: The values of the selected options in the interaction.
:ivar Optional[str] target_id?: The targeted ID of the interaction.
"""
@@ -81,7 +90,7 @@ class InteractionData(DictSerializerMixin):
options: Optional[List[Option]]
custom_id: Optional[str]
component_type: Optional[ComponentType]
- values: Optional[List[SelectOption]]
+ values: Optional[List[str]]
target_id: Optional[Snowflake]
__slots__ = (
@@ -113,9 +122,7 @@ def __init__(self, **kwargs):
self.options = (
[Option(**option) for option in self.options] if self._json.get("options") else None
)
- self.values = (
- [SelectOption(**value) for value in self.values] if self._json.get("values") else None
- )
+ self.values = self.values if self._json.get("values") else None
if self._json.get("component_type"):
self.component_type = ComponentType(self.component_type)
self._json.update({"component_type": self.component_type.value})
@@ -177,3 +184,9 @@ def __init__(self, **kwargs):
self.channel_id = Snowflake(self.channel_id) if self._json.get("channel_id") else None
self.member = Member(**self.member) if self._json.get("member") else None
self.user = User(**self.user) if self._json.get("user") else None
+
+
+class MISSING:
+ """A pseudosentinel based from an empty object. This does violate PEP, but, I don't care."""
+
+ ...
diff --git a/requirements-docs.txt b/requirements-docs.txt
new file mode 100644
index 000000000..8d734362a
--- /dev/null
+++ b/requirements-docs.txt
@@ -0,0 +1,2 @@
+Sphinx==4.1.2
+sphinx-hoverxref==1.0.0
diff --git a/requirements-lint.txt b/requirements-lint.txt
new file mode 100644
index 000000000..ac84521b7
--- /dev/null
+++ b/requirements-lint.txt
@@ -0,0 +1,4 @@
+black==21.11b1
+flake8==3.9.2
+isort==5.9.3
+pre-commit==2.16.0
diff --git a/requirements.txt b/requirements.txt
index dcf0c6da8..5ba54d59d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,3 @@
aiohttp==3.7.4.post0
-black==21.11b1
colorama==0.4.4
-flake8==3.9.2
-isort==5.9.3
orjson==3.6.3
-pre-commit==2.16.0
-Sphinx==4.1.2
-sphinx-hoverxref==1.0.0
diff --git a/setup.py b/setup.py
index 84e8899c4..7a074db10 100644
--- a/setup.py
+++ b/setup.py
@@ -12,14 +12,18 @@
with open(path.join(HERE, PACKAGE_NAME, "base.py"), encoding="utf-8") as fp:
VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1)
+
+def read_requirements(filename):
+ with open(filename, "r", encoding="utf-8") as fp:
+ return fp.read().strip().splitlines()
+
+
extras = {
- "lint": ["black", "flake8", "isort"],
- "readthedocs": ["sphinx", "karma-sphinx-theme"],
+ "lint": read_requirements("requirements-lint.txt"),
+ "readthedocs": read_requirements("requirements-docs.txt"),
}
-extras["lint"] += extras["readthedocs"]
extras["dev"] = extras["lint"] + extras["readthedocs"]
-
-requirements = open("requirements.txt").read().split("\n")[:-1]
+requirements = read_requirements("requirements.txt")
setup(
name="discord-py-interactions",
diff --git a/simple_bot.py b/simple_bot.py
index c79ee8d36..fe31a0287 100644
--- a/simple_bot.py
+++ b/simple_bot.py
@@ -1,6 +1,11 @@
+import logging
+
import interactions
-bot = interactions.Client(token=open("bot.token").read(), log_level=-1)
+logging.basicConfig(level=logging.DEBUG)
+
+
+bot = interactions.Client(token=open("bot.token").read(), disable_sync=True)
@bot.event
@@ -8,13 +13,18 @@ async def on_ready():
print("bot is now online.")
+@bot.event
+async def on_message_create(message: interactions.Message):
+ await bot._http.send_message(channel_id=852402668294766615, content=message.content)
+
+
@bot.command(
- name="global-command",
- description="ever wanted a global command? well, here it is!",
+ type=interactions.ApplicationCommandType.MESSAGE,
+ name="simple testing command",
+ scope=852402668294766612,
)
-async def basic_command(ctx):
- await ctx.send("Global commands are back in action, baby!")
+async def simple_testing_command(ctx):
+ await ctx.send("Hello world!")
-# bot.load("simple_cog")
bot.start()
diff --git a/simple_cog.py b/simple_cog.py
index 068157204..6f40ba342 100644
--- a/simple_cog.py
+++ b/simple_cog.py
@@ -3,16 +3,16 @@
class SimpleCog(interactions.Extension):
def __init__(self, client) -> None:
- super().__init__(client)
+ self.client = client
- # @command(
+ # @interactions.command(
# name="cog-command",
# description="wanna be in a cog? :)) ok.",
# scope=852402668294766612,
# )
- # async def cog_command(self, ctx):
- # await ctx.send("we're a cog in the machine!")
+ async def cog_command(self, ctx):
+ await ctx.send("we're a cog in the machine!")
-def startup(client):
+def setup(client):
SimpleCog(client)