commit ac49705f3e59de97ad12ecad4d29f0cdc2ca9844
Author: March 7th <71698422+aiko-chan-ai@users.noreply.github.com>
Date: Sat Mar 19 17:37:45 2022 +0700
Initial commit
diff --git a/ - Copy.gitignore b/ - Copy.gitignore
new file mode 100644
index 00000000..e9eb19e
--- /dev/null
+++ b/ - Copy.gitignore
@@ -0,0 +1,82 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Packages
+node_modules/
+djs/
+
+# Log files
+logs/
+*.log
+npm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Env
+.env
+test/auth.json
+test/auth.js
+docs/deploy/deploy_key
+docs/deploy/deploy_key.pub
+deploy/deploy_key
+deploy/deploy_key.pub
+
+# Dist
+dist/
+docs/docs.json
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Env
+.env
+test/
+docs/deploy/deploy_key
+docs/deploy/deploy_key.pub
+deploy/deploy_key
+deploy/deploy_key.pub
+
+# Miscellaneous
+.tmp/
+.idea/
+.DS_Store
+
+# Custom
+data/
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..20b0c92
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,127 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..e62ec04
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ 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.md b/README.md
new file mode 100644
index 00000000..59fb366
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+
+
+## About
+
+dsb is a [Node.js](https://nodejs.org) module that allows user accounts to interact with the discord api
+
+I am in no way responsible for what happens to your account. What you do is on you!
+
+## Installation
+
+**Node.js 16.9.0 or newer is required**
+
+```sh-session
+npm install dsb.js
+```
+
+## Example
+
+```js
+const { Client } = require('dsb.js');
+const client = new Client(); // intents and partials are already set so you don't have to define them
+
+client.on('ready', async () => {
+ console.log(`${client.user.username} >> [${client.guilds.cache.size}] guilds || [${client.friends.cache.size}] friends`);
+})
+
+client.login('token');
+```
+
+## Links
+- [Documentation](https://discord.js.org/#/docs/discord.js/stable/general/welcome)
+- [GitHub](https://github.com/TheDevYellowy/discordjs-selfbot)
+- [Discord](https://discord.gg/3makcFd2m4)
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..90101ce
--- /dev/null
+++ b/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "discord.js-selfbot-v13",
+ "version": "1.1.3",
+ "description": "A unofficial discord.js fork for creating selfbots [Based on discord.js v13]",
+ "main": "./src/index.js",
+ "types": "./typings/index.d.ts",
+ "files": [
+ "src",
+ "typings"
+ ],
+ "directories": {
+ "lib": "src",
+ "test": "test"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/aiko-chan-ai/discord.js-selfbot-v13.git"
+ },
+ "keywords": [
+ "discord.js",
+ "selfbot",
+ "djs",
+ "api",
+ "bot",
+ "node"
+ ],
+ "author": "aiko-chan-ai",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues"
+ },
+ "contributors": [
+ "TheDevYellowy "
+ ],
+ "homepage": "https://github.com/aiko-chan-ai/discord.js-selfbot-v13#readme",
+ "dependencies": {
+ "@discordjs/builders": "^0.12.0",
+ "@discordjs/collection": "^0.5.0",
+ "@sapphire/async-queue": "^1.3.0",
+ "@sapphire/snowflake": "^3.2.0",
+ "@types/ws": "^8.5.2",
+ "crypto": "^1.0.1",
+ "discord-api-types": "^0.27.3",
+ "form-data": "^4.0.0",
+ "lodash.snakecase": "^4.1.1",
+ "node-fetch": "^3.2.2",
+ "undici": "^4.15.0",
+ "ws": "^8.5.0"
+ },
+ "engines": {
+ "node": ">=16.9.0",
+ "npm": ">=7.0.0"
+ }
+}
\ No newline at end of file
diff --git a/src/WebSocket.js b/src/WebSocket.js
new file mode 100644
index 00000000..efbd6f9
--- /dev/null
+++ b/src/WebSocket.js
@@ -0,0 +1,39 @@
+'use strict';
+
+let erlpack;
+const { Buffer } = require('node:buffer');
+
+try {
+ erlpack = require('erlpack');
+ if (!erlpack.pack) erlpack = null;
+} catch {} // eslint-disable-line no-empty
+
+exports.WebSocket = require('ws');
+
+const ab = new TextDecoder();
+
+exports.encoding = erlpack ? 'etf' : 'json';
+
+exports.pack = erlpack ? erlpack.pack : JSON.stringify;
+
+exports.unpack = (data, type) => {
+ if (exports.encoding === 'json' || type === 'json') {
+ if (typeof data !== 'string') {
+ data = ab.decode(data);
+ }
+ return JSON.parse(data);
+ }
+ if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data));
+ return erlpack.unpack(data);
+};
+
+exports.create = (gateway, query = {}, ...args) => {
+ const [g, q] = gateway.split('?');
+ query.encoding = exports.encoding;
+ query = new URLSearchParams(query);
+ if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v));
+ const ws = new exports.WebSocket(`${g}?${query}`, ...args);
+ return ws;
+};
+
+for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state];
diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js
new file mode 100644
index 00000000..cdedff8
--- /dev/null
+++ b/src/client/BaseClient.js
@@ -0,0 +1,78 @@
+'use strict';
+
+const EventEmitter = require('node:events');
+const RESTManager = require('../rest/RESTManager');
+const { TypeError } = require('../errors');
+const Options = require('../util/Options');
+const Util = require('../util/Util');
+
+/**
+ * The base class for all clients.
+ * @extends {EventEmitter}
+ */
+class BaseClient extends EventEmitter {
+ constructor(options = {}) {
+ super({ captureRejections: true });
+
+ if (typeof options !== 'object' || options === null) {
+ throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ }
+
+ /**
+ * The options the client was instantiated with
+ * @type {ClientOptions}
+ */
+ this.options = Util.mergeDefault(Options.createDefault(), options);
+
+ /**
+ * The REST manager of the client
+ * @type {REST}
+ */
+ this.rest = new RESTManager(this);
+ }
+
+ get api() {
+ return this.rest.api;
+ }
+
+ /**
+ * Destroys all assets used by the base client.
+ * @returns {void}
+ */
+ destroy() {
+ if(this.rest.sweepInterval) clearInterval(this.rest.sweepInterval);
+ }
+
+ /**
+ * Increments max listeners by one, if they are not zero.
+ * @private
+ */
+ incrementMaxListeners() {
+ const maxListeners = this.getMaxListeners();
+ if (maxListeners !== 0) {
+ this.setMaxListeners(maxListeners + 1);
+ }
+ }
+
+ /**
+ * Decrements max listeners by one, if they are not zero.
+ * @private
+ */
+ decrementMaxListeners() {
+ const maxListeners = this.getMaxListeners();
+ if (maxListeners !== 0) {
+ this.setMaxListeners(maxListeners - 1);
+ }
+ }
+
+ toJSON(...props) {
+ return Util.flatten(this, { domain: false }, ...props);
+ }
+}
+
+module.exports = BaseClient;
+
+/**
+ * @external REST
+ * @see {@link https://discord.js.org/#/docs/rest/main/class/REST}
+ */
diff --git a/src/client/Client.js b/src/client/Client.js
new file mode 100644
index 00000000..56979a5
--- /dev/null
+++ b/src/client/Client.js
@@ -0,0 +1,565 @@
+'use strict';
+
+const process = require('node:process');
+const { Collection } = require('@discordjs/collection');
+const { OAuth2Scopes, Routes } = require('discord-api-types/v9');
+const BaseClient = require('./BaseClient');
+const ActionsManager = require('./actions/ActionsManager');
+const ClientVoiceManager = require('./voice/ClientVoiceManager');
+const WebSocketManager = require('./websocket/WebSocketManager');
+const { Error, TypeError, RangeError } = require('../errors');
+const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
+const ChannelManager = require('../managers/ChannelManager');
+const GuildManager = require('../managers/GuildManager');
+const UserManager = require('../managers/UserManager');
+const FriendsManager = require('../managers/FriendsManager');
+const BlockedManager = require('../managers/BlockedManager');
+const ClientUserSettingManager = require('../managers/ClientUserSettingManager');
+const ShardClientUtil = require('../sharding/ShardClientUtil');
+const ClientPresence = require('../structures/ClientPresence');
+const GuildPreview = require('../structures/GuildPreview');
+const GuildTemplate = require('../structures/GuildTemplate');
+const Invite = require('../structures/Invite');
+const { Sticker } = require('../structures/Sticker');
+const StickerPack = require('../structures/StickerPack');
+const VoiceRegion = require('../structures/VoiceRegion');
+const Webhook = require('../structures/Webhook');
+const Widget = require('../structures/Widget');
+const DataResolver = require('../util/DataResolver');
+const Events = require('../util/Events');
+const IntentsBitField = require('../util/IntentsBitField');
+const Options = require('../util/Options');
+const PermissionsBitField = require('../util/PermissionsBitField');
+const Status = require('../util/Status');
+const Sweepers = require('../util/Sweepers');
+
+/**
+ * The main hub for interacting with the Discord API, and the starting point for any bot.
+ * @extends {BaseClient}
+ */
+class Client extends BaseClient {
+ /**
+ * @param {ClientOptions} options Options for the client
+ */
+ constructor(options) {
+ super(options);
+
+ const data = require('node:worker_threads').workerData ?? process.env;
+ const defaults = Options.createDefault();
+
+ if (this.options.shards === defaults.shards) {
+ if ('SHARDS' in data) {
+ this.options.shards = JSON.parse(data.SHARDS);
+ }
+ }
+
+ if (this.options.shardCount === defaults.shardCount) {
+ if ('SHARD_COUNT' in data) {
+ this.options.shardCount = Number(data.SHARD_COUNT);
+ } else if (Array.isArray(this.options.shards)) {
+ this.options.shardCount = this.options.shards.length;
+ }
+ }
+
+ const typeofShards = typeof this.options.shards;
+
+ if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') {
+ this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i);
+ }
+
+ if (typeofShards === 'number') this.options.shards = [this.options.shards];
+
+ if (Array.isArray(this.options.shards)) {
+ this.options.shards = [
+ ...new Set(
+ this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)),
+ ),
+ ];
+ }
+
+ this._validateOptions();
+
+ /**
+ * The WebSocket manager of the client
+ * @type {WebSocketManager}
+ */
+ this.ws = new WebSocketManager(this);
+
+ /**
+ * The action manager of the client
+ * @type {ActionsManager}
+ * @private
+ */
+ this.actions = new ActionsManager(this);
+
+ /**
+ * The voice manager of the client
+ * @type {ClientVoiceManager}
+ */
+ this.voice = new ClientVoiceManager(this);
+
+ /**
+ * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
+ * @type {?ShardClientUtil}
+ */
+ this.shard = process.env.SHARDING_MANAGER
+ ? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
+ : null;
+
+ /**
+ * All of the {@link User} objects that have been cached at any point, mapped by their ids
+ * @type {UserManager}
+ */
+ this.users = new UserManager(this);
+ this.friends = new FriendsManager(this);
+ this.blocked = new BlockedManager(this);
+ this.setting = new ClientUserSettingManager(this);
+ /**
+ * All of the guilds the client is currently handling, mapped by their ids -
+ * as long as sharding isn't being used, this will be *every* guild the bot is a member of
+ * @type {GuildManager}
+ */
+ this.guilds = new GuildManager(this);
+
+ /**
+ * All of the {@link Channel}s that the client is currently handling, mapped by their ids -
+ * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot
+ * is a member of. Note that DM channels will not be initially cached, and thus not be present
+ * in the Manager without their explicit fetching or use.
+ * @type {ChannelManager}
+ */
+ this.channels = new ChannelManager(this);
+
+ /**
+ * The sweeping functions and their intervals used to periodically sweep caches
+ * @type {Sweepers}
+ */
+ this.sweepers = new Sweepers(this, this.options.sweepers);
+
+ /**
+ * The presence of the Client
+ * @private
+ * @type {ClientPresence}
+ */
+ this.presence = new ClientPresence(this, this.options.presence);
+
+ Object.defineProperty(this, 'token', { writable: true });
+ if (!this.token && 'DISCORD_TOKEN' in process.env) {
+ /**
+ * Authorization token for the logged in bot.
+ * If present, this defaults to `process.env.DISCORD_TOKEN` when instantiating the client
+ * This should be kept private at all times.
+ * @type {?string}
+ */
+ this.token = process.env.DISCORD_TOKEN;
+ } else {
+ this.token = null;
+ }
+
+ /**
+ * used for interacitons
+ * @type {?String}
+ */
+ this.session_id = null;
+
+ /**
+ * User that the client is logged in as
+ * @type {?ClientUser}
+ */
+ this.user = null;
+
+ /**
+ * The application of this bot
+ * @type {?ClientApplication}
+ */
+ this.application = null;
+ this.bot = null;
+
+ /**
+ * Timestamp of the time the client was last `READY` at
+ * @type {?number}
+ */
+ this.readyTimestamp = null;
+ }
+
+ /**
+ * All custom emojis that the client has access to, mapped by their ids
+ * @type {BaseGuildEmojiManager}
+ * @readonly
+ */
+ get emojis() {
+ const emojis = new BaseGuildEmojiManager(this);
+ for (const guild of this.guilds.cache.values()) {
+ if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
+ }
+ return emojis;
+ }
+
+ /**
+ * Time at which the client was last regarded as being in the `READY` state
+ * (each time the client disconnects and successfully reconnects, this will be overwritten)
+ * @type {?Date}
+ * @readonly
+ */
+ get readyAt() {
+ return this.readyTimestamp && new Date(this.readyTimestamp);
+ }
+
+ /**
+ * How long it has been since the client last entered the `READY` state in milliseconds
+ * @type {?number}
+ * @readonly
+ */
+ get uptime() {
+ return this.readyTimestamp && Date.now() - this.readyTimestamp;
+ }
+
+ /**
+ * Logs the client in, establishing a WebSocket connection to Discord.
+ * @param {string} [token=this.token] Token of the account to log in with
+ * @param {Boolean} [bot=false] Wether the token used is a bot account or not
+ * @returns {Promise} Token of the account used
+ * @example
+ * client.login('my token');
+ */
+ async login(token = this.token, bot = false) {
+ if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
+ this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
+ this.bot = bot;
+ this.emit(
+ Events.Debug,
+ `Provided token: ${token
+ .split('.')
+ .map((val, i) => (i > 1 ? val.replace(/./g, '*') : val))
+ .join('.')}`,
+ );
+
+ if (this.options.presence) {
+ this.options.ws.presence = this.presence._parse(this.options.presence);
+ }
+
+ this.emit(Events.Debug, 'Preparing to connect to the gateway...');
+
+ try {
+ await this.ws.connect();
+ return this.token;
+ } catch (error) {
+ this.destroy();
+ throw error;
+ }
+ }
+
+ /**
+ * Returns whether the client has logged in, indicative of being able to access
+ * properties such as `user` and `application`.
+ * @returns {boolean}
+ */
+ isReady() {
+ return this.ws.status === Status.Ready;
+ }
+
+ /**
+ * Logs out, terminates the connection to Discord, and destroys the client.
+ * @returns {void}
+ */
+ destroy() {
+ super.destroy();
+
+ this.sweepers.destroy();
+ this.ws.destroy();
+ this.token = null;
+ //this.rest.setToken(null);
+ }
+
+ /**
+ * Options used when fetching an invite from Discord.
+ * @typedef {Object} ClientFetchInviteOptions
+ * @property {Snowflake} [guildScheduledEventId] The id of the guild scheduled event to include with
+ * the invite
+ */
+
+ /**
+ * Obtains an invite from Discord.
+ * @param {InviteResolvable} invite Invite code or URL
+ * @param {ClientFetchInviteOptions} [options] Options for fetching the invite
+ * @returns {Promise}
+ * @example
+ * client.fetchInvite('https://discord.gg/djs')
+ * .then(invite => console.log(`Obtained invite with code: ${invite.code}`))
+ * .catch(console.error);
+ */
+ async fetchInvite(invite, options) {
+ const code = DataResolver.resolveInviteCode(invite);
+ const query = new URLSearchParams({
+ with_counts: true,
+ with_expiration: true,
+ });
+ if (options?.guildScheduledEventId) {
+ query.set('guild_scheduled_event_id', options.guildScheduledEventId);
+ }
+ const data = await this.api.invites(code).get({ query });
+ return new Invite(this, data);
+ }
+
+ /**
+ * Obtains a template from Discord.
+ * @param {GuildTemplateResolvable} template Template code or URL
+ * @returns {Promise}
+ * @example
+ * client.fetchGuildTemplate('https://discord.new/FKvmczH2HyUf')
+ * .then(template => console.log(`Obtained template with code: ${template.code}`))
+ * .catch(console.error);
+ */
+ async fetchGuildTemplate(template) {
+ const code = DataResolver.resolveGuildTemplateCode(template);
+ const data = await this.api.guilds.templates(code).get();
+ return new GuildTemplate(this, data);
+ }
+
+ /**
+ * Obtains a webhook from Discord.
+ * @param {Snowflake} id The webhook's id
+ * @param {string} [token] Token for the webhook
+ * @returns {Promise}
+ * @example
+ * client.fetchWebhook('id', 'token')
+ * .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`))
+ * .catch(console.error);
+ */
+ async fetchWebhook(id, token) {
+ const data = await this.api.webhook(id, token).get();
+ return new Webhook(this, { token, ...data });
+ }
+
+ /**
+ * Obtains the available voice regions from Discord.
+ * @returns {Promise>}
+ * @example
+ * client.fetchVoiceRegions()
+ * .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`))
+ * .catch(console.error);
+ */
+ async fetchVoiceRegions() {
+ const apiRegions = await this.api.voice.regions.get();
+ const regions = new Collection();
+ for (const region of apiRegions) regions.set(region.id, new VoiceRegion(region));
+ return regions;
+ }
+
+ /**
+ * Obtains a sticker from Discord.
+ * @param {Snowflake} id The sticker's id
+ * @returns {Promise}
+ * @example
+ * client.fetchSticker('id')
+ * .then(sticker => console.log(`Obtained sticker with name: ${sticker.name}`))
+ * .catch(console.error);
+ */
+ async fetchSticker(id) {
+ const data = await this.api.stickers(id).get();
+ return new Sticker(this, data);
+ }
+
+ /**
+ * Obtains the list of sticker packs available to Nitro subscribers from Discord.
+ * @returns {Promise>}
+ * @example
+ * client.fetchPremiumStickerPacks()
+ * .then(packs => console.log(`Available sticker packs are: ${packs.map(pack => pack.name).join(', ')}`))
+ * .catch(console.error);
+ */
+ async fetchPremiumStickerPacks() {
+ const data = await this.api('sticker-packs').get();
+ return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)]));
+ }
+
+ /**
+ * Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
+ * @param {GuildResolvable} guild The guild to fetch the preview for
+ * @returns {Promise}
+ */
+ async fetchGuildPreview(guild) {
+ const id = this.guilds.resolveId(guild);
+ if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
+ const data = await this.api.guilds(id).preview.get();
+ return new GuildPreview(this, data);
+ }
+
+ /**
+ * Obtains the widget data of a guild from Discord, available for guilds with the widget enabled.
+ * @param {GuildResolvable} guild The guild to fetch the widget data for
+ * @returns {Promise}
+ */
+ async fetchGuildWidget(guild) {
+ const id = this.guilds.resolveId(guild);
+ if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
+ const data = await this.api.guilds(id, 'widget.json').get();
+ return new Widget(this, data);
+ }
+
+ /**
+ * Options for {@link Client#generateInvite}.
+ * @typedef {Object} InviteGenerationOptions
+ * @property {OAuth2Scopes[]} scopes Scopes that should be requested
+ * @property {PermissionResolvable} [permissions] Permissions to request
+ * @property {GuildResolvable} [guild] Guild to preselect
+ * @property {boolean} [disableGuildSelect] Whether to disable the guild selection
+ */
+
+ /**
+ * Generates a link that can be used to invite the bot to a guild.
+ * @param {InviteGenerationOptions} [options={}] Options for the invite
+ * @returns {string}
+ * @example
+ * const link = client.generateInvite({
+ * scopes: [OAuth2Scopes.ApplicationsCommands],
+ * });
+ * console.log(`Generated application invite link: ${link}`);
+ * @example
+ * const link = client.generateInvite({
+ * permissions: [
+ * PermissionFlagsBits.SendMessages,
+ * PermissionFlagsBits.ManageGuild,
+ * PermissionFlagsBits.MentionEveryone,
+ * ],
+ * scopes: [OAuth2Scopes.Bot],
+ * });
+ * console.log(`Generated bot invite link: ${link}`);
+ */
+ generateInvite(options = {}) {
+ if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ if (!this.application) throw new Error('CLIENT_NOT_READY', 'generate an invite link');
+
+ const query = new URLSearchParams({
+ client_id: this.application.id,
+ });
+
+ const { scopes } = options;
+ if (typeof scopes === 'undefined') {
+ throw new TypeError('INVITE_MISSING_SCOPES');
+ }
+ if (!Array.isArray(scopes)) {
+ throw new TypeError('INVALID_TYPE', 'scopes', 'Array of Invite Scopes', true);
+ }
+ if (!scopes.some(scope => [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands].includes(scope))) {
+ throw new TypeError('INVITE_MISSING_SCOPES');
+ }
+ const validScopes = Object.values(OAuth2Scopes);
+ const invalidScope = scopes.find(scope => !validScopes.includes(scope));
+ if (invalidScope) {
+ throw new TypeError('INVALID_ELEMENT', 'Array', 'scopes', invalidScope);
+ }
+ query.set('scope', scopes.join(' '));
+
+ if (options.permissions) {
+ const permissions = PermissionsBitField.resolve(options.permissions);
+ if (permissions) query.set('permissions', permissions);
+ }
+
+ if (options.disableGuildSelect) {
+ query.set('disable_guild_select', true);
+ }
+
+ if (options.guild) {
+ const guildId = this.guilds.resolveId(options.guild);
+ if (!guildId) throw new TypeError('INVALID_TYPE', 'options.guild', 'GuildResolvable');
+ query.set('guild_id', guildId);
+ }
+
+ return `${this.options.rest.api}${Routes.oauth2Authorization()}?${query}`;
+ }
+
+ toJSON() {
+ return super.toJSON({
+ readyAt: false,
+ });
+ }
+
+ /**
+ * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
+ * with the client as `this`.
+ * @param {string} script Script to eval
+ * @returns {*}
+ * @private
+ */
+ _eval(script) {
+ return eval(script);
+ }
+
+ /**
+ * Validates the client options.
+ * @param {ClientOptions} [options=this.options] Options to validate
+ * @private
+ */
+ _validateOptions(options = this.options) {
+ if (typeof options.intents === 'undefined') {
+ throw new TypeError('CLIENT_MISSING_INTENTS');
+ } else {
+ options.intents = IntentsBitField.resolve(options.intents);
+ }
+ if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1');
+ }
+ if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'shards', "'auto', a number or array of numbers");
+ }
+ if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS');
+ if (typeof options.makeCache !== 'function') {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function');
+ }
+ if (typeof options.sweepers !== 'object' || options.sweepers === null) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object');
+ }
+ if (!Array.isArray(options.partials)) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
+ }
+ if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number');
+ }
+ if (typeof options.failIfNotExists !== 'boolean') {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'failIfNotExists', 'a boolean');
+ }
+ }
+}
+
+module.exports = Client;
+
+/**
+ * A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake},
+ * except the epoch is 2015-01-01T00:00:00.000Z.
+ *
+ * If we have a snowflake '266241948824764416' we can represent it as binary:
+ * ```
+ * 64 22 17 12 0
+ * 000000111011000111100001101001000101000000 00001 00000 000000000000
+ * number of milliseconds since Discord epoch worker pid increment
+ * ```
+ * @typedef {string} Snowflake
+ */
+
+/**
+ * Emitted for general debugging information.
+ * @event Client#debug
+ * @param {string} info The debug information
+ */
+
+/**
+ * Emitted for general warnings.
+ * @event Client#warn
+ * @param {string} info The warning
+ */
+
+/**
+ * @external Collection
+ * @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
+ */
+
+/**
+ * @external ImageURLOptions
+ * @see {@link https://discord.js.org/#/docs/rest/main/typedef/ImageURLOptions}
+ */
+
+/**
+ * @external BaseImageURLOptions
+ * @see {@link https://discord.js.org/#/docs/rest/main/typedef/BaseImageURLOptions}
+ */
diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js
new file mode 100644
index 00000000..1c66194
--- /dev/null
+++ b/src/client/WebhookClient.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const BaseClient = require('./BaseClient');
+const { Error } = require('../errors');
+const Webhook = require('../structures/Webhook');
+
+/**
+ * The webhook client.
+ * @implements {Webhook}
+ * @extends {BaseClient}
+ */
+class WebhookClient extends BaseClient {
+ /**
+ * The data for the webhook client containing either an id and token or just a URL
+ * @typedef {Object} WebhookClientData
+ * @property {Snowflake} [id] The id of the webhook
+ * @property {string} [token] The token of the webhook
+ * @property {string} [url] The full URL for the webhook client
+ */
+
+ /**
+ * @param {WebhookClientData} data The data of the webhook
+ * @param {ClientOptions} [options] Options for the client
+ */
+ constructor(data, options) {
+ super(options);
+ Object.defineProperty(this, 'client', { value: this });
+ let { id, token } = data;
+
+ if ('url' in data) {
+ const url = data.url.match(
+ // eslint-disable-next-line no-useless-escape
+ /https?:\/\/(?:ptb\.|canary\.)?discord\.com\/api(?:\/v\d{1,2})?\/webhooks\/(\d{17,19})\/([\w-]{68})/i,
+ );
+
+ if (!url || url.length <= 1) throw new Error('WEBHOOK_URL_INVALID');
+
+ [, id, token] = url;
+ }
+
+ this.id = id;
+ Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
+ }
+
+ // These are here only for documentation purposes - they are implemented by Webhook
+ /* eslint-disable no-empty-function */
+ send() {}
+ sendSlackMessage() {}
+ fetchMessage() {}
+ edit() {}
+ editMessage() {}
+ delete() {}
+ deleteMessage() {}
+ get createdTimestamp() {}
+ get createdAt() {}
+ get url() {}
+}
+
+Webhook.applyToClass(WebhookClient);
+
+module.exports = WebhookClient;
diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js
new file mode 100644
index 00000000..f70d3ca
--- /dev/null
+++ b/src/client/actions/Action.js
@@ -0,0 +1,115 @@
+'use strict';
+
+const Partials = require('../../util/Partials');
+
+/*
+
+ABOUT ACTIONS
+
+Actions are similar to WebSocket Packet Handlers, but since introducing
+the REST API methods, in order to prevent rewriting code to handle data,
+"actions" have been introduced. They're basically what Packet Handlers
+used to be but they're strictly for manipulating data and making sure
+that WebSocket events don't clash with REST methods.
+
+*/
+
+class GenericAction {
+ constructor(client) {
+ this.client = client;
+ }
+
+ handle(data) {
+ return data;
+ }
+
+ getPayload(data, manager, id, partialType, cache) {
+ const existing = manager.cache.get(id);
+ if (!existing && this.client.options.partials.includes(partialType)) {
+ return manager._add(data, cache);
+ }
+ return existing;
+ }
+
+ getChannel(data) {
+ const id = data.channel_id ?? data.id;
+ return (
+ data.channel ??
+ this.getPayload(
+ {
+ id,
+ guild_id: data.guild_id,
+ recipients: [data.author ?? data.user ?? { id: data.user_id }],
+ },
+ this.client.channels,
+ id,
+ Partials.Channel,
+ )
+ );
+ }
+
+ getMessage(data, channel, cache) {
+ const id = data.message_id ?? data.id;
+ return (
+ data.message ??
+ this.getPayload(
+ {
+ id,
+ channel_id: channel.id,
+ guild_id: data.guild_id ?? channel.guild?.id,
+ },
+ channel.messages,
+ id,
+ Partials.Message,
+ cache,
+ )
+ );
+ }
+
+ getReaction(data, message, user) {
+ const id = data.emoji.id ?? decodeURIComponent(data.emoji.name);
+ return this.getPayload(
+ {
+ emoji: data.emoji,
+ count: message.partial ? null : 0,
+ me: user?.id === this.client.user.id,
+ },
+ message.reactions,
+ id,
+ Partials.Reaction,
+ );
+ }
+
+ getMember(data, guild) {
+ return this.getPayload(data, guild.members, data.user.id, Partials.GuildMember);
+ }
+
+ getUser(data) {
+ const id = data.user_id;
+ return data.user ?? this.getPayload({ id }, this.client.users, id, Partials.User);
+ }
+
+ getUserFromMember(data) {
+ if (data.guild_id && data.member?.user) {
+ const guild = this.client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ return guild.members._add(data.member).user;
+ } else {
+ return this.client.users._add(data.member.user);
+ }
+ }
+ return this.getUser(data);
+ }
+
+ getScheduledEvent(data, guild) {
+ const id = data.guild_scheduled_event_id ?? data.id;
+ return this.getPayload(
+ { id, guild_id: data.guild_id ?? guild.id },
+ guild.scheduledEvents,
+ id,
+ Partials.GuildScheduledEvent,
+ );
+ }
+}
+
+module.exports = GenericAction;
diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js
new file mode 100644
index 00000000..5841777
--- /dev/null
+++ b/src/client/actions/ActionsManager.js
@@ -0,0 +1,66 @@
+'use strict';
+
+class ActionsManager {
+ constructor(client) {
+ this.client = client;
+
+ this.register(require('./ChannelCreate'));
+ this.register(require('./ChannelDelete'));
+ this.register(require('./ChannelUpdate'));
+ this.register(require('./GuildBanAdd'));
+ this.register(require('./GuildBanRemove'));
+ this.register(require('./GuildChannelsPositionUpdate'));
+ this.register(require('./GuildDelete'));
+ this.register(require('./GuildEmojiCreate'));
+ this.register(require('./GuildEmojiDelete'));
+ this.register(require('./GuildEmojiUpdate'));
+ this.register(require('./GuildEmojisUpdate'));
+ this.register(require('./GuildIntegrationsUpdate'));
+ this.register(require('./GuildMemberRemove'));
+ this.register(require('./GuildMemberUpdate'));
+ this.register(require('./GuildRoleCreate'));
+ this.register(require('./GuildRoleDelete'));
+ this.register(require('./GuildRoleUpdate'));
+ this.register(require('./GuildRolesPositionUpdate'));
+ this.register(require('./GuildScheduledEventCreate'));
+ this.register(require('./GuildScheduledEventDelete'));
+ this.register(require('./GuildScheduledEventUpdate'));
+ this.register(require('./GuildScheduledEventUserAdd'));
+ this.register(require('./GuildScheduledEventUserRemove'));
+ this.register(require('./GuildStickerCreate'));
+ this.register(require('./GuildStickerDelete'));
+ this.register(require('./GuildStickerUpdate'));
+ this.register(require('./GuildStickersUpdate'));
+ this.register(require('./GuildUpdate'));
+ this.register(require('./InteractionCreate'));
+ this.register(require('./InviteCreate'));
+ this.register(require('./InviteDelete'));
+ this.register(require('./MessageCreate'));
+ this.register(require('./MessageDelete'));
+ this.register(require('./MessageDeleteBulk'));
+ this.register(require('./MessageReactionAdd'));
+ this.register(require('./MessageReactionRemove'));
+ this.register(require('./MessageReactionRemoveAll'));
+ this.register(require('./MessageReactionRemoveEmoji'));
+ this.register(require('./MessageUpdate'));
+ this.register(require('./PresenceUpdate'));
+ this.register(require('./StageInstanceCreate'));
+ this.register(require('./StageInstanceDelete'));
+ this.register(require('./StageInstanceUpdate'));
+ this.register(require('./ThreadCreate'));
+ this.register(require('./ThreadDelete'));
+ this.register(require('./ThreadListSync'));
+ this.register(require('./ThreadMemberUpdate'));
+ this.register(require('./ThreadMembersUpdate'));
+ this.register(require('./TypingStart'));
+ this.register(require('./UserUpdate'));
+ this.register(require('./VoiceStateUpdate'));
+ this.register(require('./WebhooksUpdate'));
+ }
+
+ register(Action) {
+ this[Action.name.replace(/Action$/, '')] = new Action(this.client);
+ }
+}
+
+module.exports = ActionsManager;
diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js
new file mode 100644
index 00000000..fdf8ddd
--- /dev/null
+++ b/src/client/actions/ChannelCreate.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ChannelCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const existing = client.channels.cache.has(data.id);
+ const channel = client.channels._add(data);
+ if (!existing && channel) {
+ /**
+ * Emitted whenever a guild channel is created.
+ * @event Client#channelCreate
+ * @param {GuildChannel} channel The channel that was created
+ */
+ client.emit(Events.ChannelCreate, channel);
+ }
+ return { channel };
+ }
+}
+
+module.exports = ChannelCreateAction;
diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js
new file mode 100644
index 00000000..acf03d9
--- /dev/null
+++ b/src/client/actions/ChannelDelete.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ChannelDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.channels.cache.get(data.id);
+
+ if (channel) {
+ client.channels._remove(channel.id);
+ /**
+ * Emitted whenever a channel is deleted.
+ * @event Client#channelDelete
+ * @param {DMChannel|GuildChannel} channel The channel that was deleted
+ */
+ client.emit(Events.ChannelDelete, channel);
+ }
+ }
+}
+
+module.exports = ChannelDeleteAction;
diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js
new file mode 100644
index 00000000..88ee7f1
--- /dev/null
+++ b/src/client/actions/ChannelUpdate.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const Action = require('./Action');
+const { Channel } = require('../../structures/Channel');
+
+class ChannelUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ let channel = client.channels.cache.get(data.id);
+ if (channel) {
+ const old = channel._update(data);
+
+ if (channel.type !== data.type) {
+ const newChannel = Channel.create(this.client, data, channel.guild);
+ for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message);
+ channel = newChannel;
+ this.client.channels.cache.set(channel.id, channel);
+ }
+
+ return {
+ old,
+ updated: channel,
+ };
+ } else {
+ client.channels._add(data);
+ }
+
+ return {};
+ }
+}
+
+module.exports = ChannelUpdateAction;
diff --git a/src/client/actions/GuildBanAdd.js b/src/client/actions/GuildBanAdd.js
new file mode 100644
index 00000000..2ef4b11
--- /dev/null
+++ b/src/client/actions/GuildBanAdd.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildBanAdd extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ /**
+ * Emitted whenever a member is banned from a guild.
+ * @event Client#guildBanAdd
+ * @param {GuildBan} ban The ban that occurred
+ */
+ if (guild) client.emit(Events.GuildBanAdd, guild.bans._add(data));
+ }
+}
+
+module.exports = GuildBanAdd;
diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js
new file mode 100644
index 00000000..8048efd
--- /dev/null
+++ b/src/client/actions/GuildBanRemove.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const Action = require('./Action');
+const GuildBan = require('../../structures/GuildBan');
+const Events = require('../../util/Events');
+
+class GuildBanRemove extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ /**
+ * Emitted whenever a member is unbanned from a guild.
+ * @event Client#guildBanRemove
+ * @param {GuildBan} ban The ban that was removed
+ */
+ if (guild) {
+ const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild);
+ guild.bans.cache.delete(ban.user.id);
+ client.emit(Events.GuildBanRemove, ban);
+ }
+ }
+}
+
+module.exports = GuildBanRemove;
diff --git a/src/client/actions/GuildChannelsPositionUpdate.js b/src/client/actions/GuildChannelsPositionUpdate.js
new file mode 100644
index 00000000..a393167
--- /dev/null
+++ b/src/client/actions/GuildChannelsPositionUpdate.js
@@ -0,0 +1,21 @@
+'use strict';
+
+const Action = require('./Action');
+
+class GuildChannelsPositionUpdate extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ for (const partialChannel of data.channels) {
+ const channel = guild.channels.cache.get(partialChannel.id);
+ if (channel) channel.rawPosition = partialChannel.position;
+ }
+ }
+
+ return { guild };
+ }
+}
+
+module.exports = GuildChannelsPositionUpdate;
diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js
new file mode 100644
index 00000000..eb0a44d
--- /dev/null
+++ b/src/client/actions/GuildDelete.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ let guild = client.guilds.cache.get(data.id);
+ if (guild) {
+ if (data.unavailable) {
+ // Guild is unavailable
+ guild.available = false;
+
+ /**
+ * Emitted whenever a guild becomes unavailable, likely due to a server outage.
+ * @event Client#guildUnavailable
+ * @param {Guild} guild The guild that has become unavailable
+ */
+ client.emit(Events.GuildUnavailable, guild);
+
+ // Stops the GuildDelete packet thinking a guild was actually deleted,
+ // handles emitting of event itself
+ return;
+ }
+
+ for (const channel of guild.channels.cache.values()) this.client.channels._remove(channel.id);
+ client.voice.adapters.get(data.id)?.destroy();
+
+ // Delete guild
+ client.guilds.cache.delete(guild.id);
+
+ /**
+ * Emitted whenever a guild kicks the client or the guild is deleted/left.
+ * @event Client#guildDelete
+ * @param {Guild} guild The guild that was deleted
+ */
+ client.emit(Events.GuildDelete, guild);
+ }
+ }
+}
+
+module.exports = GuildDeleteAction;
diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js
new file mode 100644
index 00000000..61858cf
--- /dev/null
+++ b/src/client/actions/GuildEmojiCreate.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildEmojiCreateAction extends Action {
+ handle(guild, createdEmoji) {
+ const already = guild.emojis.cache.has(createdEmoji.id);
+ const emoji = guild.emojis._add(createdEmoji);
+ /**
+ * Emitted whenever a custom emoji is created in a guild.
+ * @event Client#emojiCreate
+ * @param {GuildEmoji} emoji The emoji that was created
+ */
+ if (!already) this.client.emit(Events.GuildEmojiCreate, emoji);
+ return { emoji };
+ }
+}
+
+module.exports = GuildEmojiCreateAction;
diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js
new file mode 100644
index 00000000..e3373c2
--- /dev/null
+++ b/src/client/actions/GuildEmojiDelete.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildEmojiDeleteAction extends Action {
+ handle(emoji) {
+ emoji.guild.emojis.cache.delete(emoji.id);
+ /**
+ * Emitted whenever a custom emoji is deleted in a guild.
+ * @event Client#emojiDelete
+ * @param {GuildEmoji} emoji The emoji that was deleted
+ */
+ this.client.emit(Events.GuildEmojiDelete, emoji);
+ return { emoji };
+ }
+}
+
+module.exports = GuildEmojiDeleteAction;
diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js
new file mode 100644
index 00000000..6bf9657
--- /dev/null
+++ b/src/client/actions/GuildEmojiUpdate.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildEmojiUpdateAction extends Action {
+ handle(current, data) {
+ const old = current._update(data);
+ /**
+ * Emitted whenever a custom emoji is updated in a guild.
+ * @event Client#emojiUpdate
+ * @param {GuildEmoji} oldEmoji The old emoji
+ * @param {GuildEmoji} newEmoji The new emoji
+ */
+ this.client.emit(Events.GuildEmojiUpdate, old, current);
+ return { emoji: current };
+ }
+}
+
+module.exports = GuildEmojiUpdateAction;
diff --git a/src/client/actions/GuildEmojisUpdate.js b/src/client/actions/GuildEmojisUpdate.js
new file mode 100644
index 00000000..7829db1
--- /dev/null
+++ b/src/client/actions/GuildEmojisUpdate.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const Action = require('./Action');
+
+class GuildEmojisUpdateAction extends Action {
+ handle(data) {
+ const guild = this.client.guilds.cache.get(data.guild_id);
+ if (!guild?.emojis) return;
+
+ const deletions = new Map(guild.emojis.cache);
+
+ for (const emoji of data.emojis) {
+ // Determine type of emoji event
+ const cachedEmoji = guild.emojis.cache.get(emoji.id);
+ if (cachedEmoji) {
+ deletions.delete(emoji.id);
+ if (!cachedEmoji.equals(emoji)) {
+ // Emoji updated
+ this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
+ }
+ } else {
+ // Emoji added
+ this.client.actions.GuildEmojiCreate.handle(guild, emoji);
+ }
+ }
+
+ for (const emoji of deletions.values()) {
+ // Emoji deleted
+ this.client.actions.GuildEmojiDelete.handle(emoji);
+ }
+ }
+}
+
+module.exports = GuildEmojisUpdateAction;
diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js
new file mode 100644
index 00000000..28b9bbb
--- /dev/null
+++ b/src/client/actions/GuildIntegrationsUpdate.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildIntegrationsUpdate extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+ /**
+ * Emitted whenever a guild integration is updated
+ * @event Client#guildIntegrationsUpdate
+ * @param {Guild} guild The guild whose integrations were updated
+ */
+ if (guild) client.emit(Events.GuildIntegrationsUpdate, guild);
+ }
+}
+
+module.exports = GuildIntegrationsUpdate;
diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js
new file mode 100644
index 00000000..646f4ec
--- /dev/null
+++ b/src/client/actions/GuildMemberRemove.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+const Status = require('../../util/Status');
+
+class GuildMemberRemoveAction extends Action {
+ handle(data, shard) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+ let member = null;
+ if (guild) {
+ member = this.getMember({ user: data.user }, guild);
+ guild.memberCount--;
+ if (member) {
+ guild.members.cache.delete(member.id);
+ /**
+ * Emitted whenever a member leaves a guild, or is kicked.
+ * @event Client#guildMemberRemove
+ * @param {GuildMember} member The member that has left/been kicked from the guild
+ */
+ if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member);
+ }
+ guild.voiceStates.cache.delete(data.user.id);
+ }
+ return { guild, member };
+ }
+}
+
+module.exports = GuildMemberRemoveAction;
diff --git a/src/client/actions/GuildMemberUpdate.js b/src/client/actions/GuildMemberUpdate.js
new file mode 100644
index 00000000..dc41a79
--- /dev/null
+++ b/src/client/actions/GuildMemberUpdate.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+const Status = require('../../util/Status');
+
+class GuildMemberUpdateAction extends Action {
+ handle(data, shard) {
+ const { client } = this;
+ if (data.user.username) {
+ const user = client.users.cache.get(data.user.id);
+ if (!user) {
+ client.users._add(data.user);
+ } else if (!user._equals(data.user)) {
+ client.actions.UserUpdate.handle(data.user);
+ }
+ }
+
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ const member = this.getMember({ user: data.user }, guild);
+ if (member) {
+ const old = member._update(data);
+ /**
+ * Emitted whenever a guild member changes - i.e. new role, removed role, nickname.
+ * @event Client#guildMemberUpdate
+ * @param {GuildMember} oldMember The member before the update
+ * @param {GuildMember} newMember The member after the update
+ */
+ if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member);
+ } else {
+ const newMember = guild.members._add(data);
+ /**
+ * Emitted whenever a member becomes available in a large guild.
+ * @event Client#guildMemberAvailable
+ * @param {GuildMember} member The member that became available
+ */
+ this.client.emit(Events.GuildMemberAvailable, newMember);
+ }
+ }
+ }
+}
+
+module.exports = GuildMemberUpdateAction;
diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js
new file mode 100644
index 00000000..461443b
--- /dev/null
+++ b/src/client/actions/GuildRoleCreate.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildRoleCreate extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+ let role;
+ if (guild) {
+ const already = guild.roles.cache.has(data.role.id);
+ role = guild.roles._add(data.role);
+ /**
+ * Emitted whenever a role is created.
+ * @event Client#roleCreate
+ * @param {Role} role The role that was created
+ */
+ if (!already) client.emit(Events.GuildRoleCreate, role);
+ }
+ return { role };
+ }
+}
+
+module.exports = GuildRoleCreate;
diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js
new file mode 100644
index 00000000..e043a1a
--- /dev/null
+++ b/src/client/actions/GuildRoleDelete.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildRoleDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+ let role;
+
+ if (guild) {
+ role = guild.roles.cache.get(data.role_id);
+ if (role) {
+ guild.roles.cache.delete(data.role_id);
+ /**
+ * Emitted whenever a guild role is deleted.
+ * @event Client#roleDelete
+ * @param {Role} role The role that was deleted
+ */
+ client.emit(Events.GuildRoleDelete, role);
+ }
+ }
+
+ return { role };
+ }
+}
+
+module.exports = GuildRoleDeleteAction;
diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js
new file mode 100644
index 00000000..b0632c5
--- /dev/null
+++ b/src/client/actions/GuildRoleUpdate.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildRoleUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ if (guild) {
+ let old = null;
+
+ const role = guild.roles.cache.get(data.role.id);
+ if (role) {
+ old = role._update(data.role);
+ /**
+ * Emitted whenever a guild role is updated.
+ * @event Client#roleUpdate
+ * @param {Role} oldRole The role before the update
+ * @param {Role} newRole The role after the update
+ */
+ client.emit(Events.GuildRoleUpdate, old, role);
+ }
+
+ return {
+ old,
+ updated: role,
+ };
+ }
+
+ return {
+ old: null,
+ updated: null,
+ };
+ }
+}
+
+module.exports = GuildRoleUpdateAction;
diff --git a/src/client/actions/GuildRolesPositionUpdate.js b/src/client/actions/GuildRolesPositionUpdate.js
new file mode 100644
index 00000000..d7abca9
--- /dev/null
+++ b/src/client/actions/GuildRolesPositionUpdate.js
@@ -0,0 +1,21 @@
+'use strict';
+
+const Action = require('./Action');
+
+class GuildRolesPositionUpdate extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ for (const partialRole of data.roles) {
+ const role = guild.roles.cache.get(partialRole.id);
+ if (role) role.rawPosition = partialRole.position;
+ }
+ }
+
+ return { guild };
+ }
+}
+
+module.exports = GuildRolesPositionUpdate;
diff --git a/src/client/actions/GuildScheduledEventCreate.js b/src/client/actions/GuildScheduledEventCreate.js
new file mode 100644
index 00000000..0a2fb9b
--- /dev/null
+++ b/src/client/actions/GuildScheduledEventCreate.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildScheduledEventCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ const guildScheduledEvent = guild.scheduledEvents._add(data);
+
+ /**
+ * Emitted whenever a guild scheduled event is created.
+ * @event Client#guildScheduledEventCreate
+ * @param {GuildScheduledEvent} guildScheduledEvent The created guild scheduled event
+ */
+ client.emit(Events.GuildScheduledEventCreate, guildScheduledEvent);
+
+ return { guildScheduledEvent };
+ }
+
+ return {};
+ }
+}
+
+module.exports = GuildScheduledEventCreateAction;
diff --git a/src/client/actions/GuildScheduledEventDelete.js b/src/client/actions/GuildScheduledEventDelete.js
new file mode 100644
index 00000000..636bfc5
--- /dev/null
+++ b/src/client/actions/GuildScheduledEventDelete.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildScheduledEventDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ if (guild) {
+ const guildScheduledEvent = this.getScheduledEvent(data, guild);
+ if (guildScheduledEvent) {
+ guild.scheduledEvents.cache.delete(guildScheduledEvent.id);
+
+ /**
+ * Emitted whenever a guild scheduled event is deleted.
+ * @event Client#guildScheduledEventDelete
+ * @param {GuildScheduledEvent} guildScheduledEvent The deleted guild scheduled event
+ */
+ client.emit(Events.GuildScheduledEventDelete, guildScheduledEvent);
+
+ return { guildScheduledEvent };
+ }
+ }
+
+ return {};
+ }
+}
+
+module.exports = GuildScheduledEventDeleteAction;
diff --git a/src/client/actions/GuildScheduledEventUpdate.js b/src/client/actions/GuildScheduledEventUpdate.js
new file mode 100644
index 00000000..7cabd85
--- /dev/null
+++ b/src/client/actions/GuildScheduledEventUpdate.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildScheduledEventUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ if (guild) {
+ const oldGuildScheduledEvent = guild.scheduledEvents.cache.get(data.id)?._clone() ?? null;
+ const newGuildScheduledEvent = guild.scheduledEvents._add(data);
+
+ /**
+ * Emitted whenever a guild scheduled event gets updated.
+ * @event Client#guildScheduledEventUpdate
+ * @param {?GuildScheduledEvent} oldGuildScheduledEvent The guild scheduled event object before the update
+ * @param {GuildScheduledEvent} newGuildScheduledEvent The guild scheduled event object after the update
+ */
+ client.emit(Events.GuildScheduledEventUpdate, oldGuildScheduledEvent, newGuildScheduledEvent);
+
+ return { oldGuildScheduledEvent, newGuildScheduledEvent };
+ }
+
+ return {};
+ }
+}
+
+module.exports = GuildScheduledEventUpdateAction;
diff --git a/src/client/actions/GuildScheduledEventUserAdd.js b/src/client/actions/GuildScheduledEventUserAdd.js
new file mode 100644
index 00000000..03520db
--- /dev/null
+++ b/src/client/actions/GuildScheduledEventUserAdd.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildScheduledEventUserAddAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ if (guild) {
+ const guildScheduledEvent = this.getScheduledEvent(data, guild);
+ const user = this.getUser(data);
+
+ if (guildScheduledEvent && user) {
+ /**
+ * Emitted whenever a user subscribes to a guild scheduled event
+ * @event Client#guildScheduledEventUserAdd
+ * @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
+ * @param {User} user The user who subscribed
+ */
+ client.emit(Events.GuildScheduledEventUserAdd, guildScheduledEvent, user);
+
+ return { guildScheduledEvent, user };
+ }
+ }
+
+ return {};
+ }
+}
+
+module.exports = GuildScheduledEventUserAddAction;
diff --git a/src/client/actions/GuildScheduledEventUserRemove.js b/src/client/actions/GuildScheduledEventUserRemove.js
new file mode 100644
index 00000000..2a04849
--- /dev/null
+++ b/src/client/actions/GuildScheduledEventUserRemove.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildScheduledEventUserRemoveAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+
+ if (guild) {
+ const guildScheduledEvent = this.getScheduledEvent(data, guild);
+ const user = this.getUser(data);
+
+ if (guildScheduledEvent && user) {
+ /**
+ * Emitted whenever a user unsubscribes from a guild scheduled event
+ * @event Client#guildScheduledEventUserRemove
+ * @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
+ * @param {User} user The user who unsubscribed
+ */
+ client.emit(Events.GuildScheduledEventUserRemove, guildScheduledEvent, user);
+
+ return { guildScheduledEvent, user };
+ }
+ }
+
+ return {};
+ }
+}
+
+module.exports = GuildScheduledEventUserRemoveAction;
diff --git a/src/client/actions/GuildStickerCreate.js b/src/client/actions/GuildStickerCreate.js
new file mode 100644
index 00000000..7d81de9
--- /dev/null
+++ b/src/client/actions/GuildStickerCreate.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildStickerCreateAction extends Action {
+ handle(guild, createdSticker) {
+ const already = guild.stickers.cache.has(createdSticker.id);
+ const sticker = guild.stickers._add(createdSticker);
+ /**
+ * Emitted whenever a custom sticker is created in a guild.
+ * @event Client#stickerCreate
+ * @param {Sticker} sticker The sticker that was created
+ */
+ if (!already) this.client.emit(Events.GuildStickerCreate, sticker);
+ return { sticker };
+ }
+}
+
+module.exports = GuildStickerCreateAction;
diff --git a/src/client/actions/GuildStickerDelete.js b/src/client/actions/GuildStickerDelete.js
new file mode 100644
index 00000000..7fd6b57
--- /dev/null
+++ b/src/client/actions/GuildStickerDelete.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildStickerDeleteAction extends Action {
+ handle(sticker) {
+ sticker.guild.stickers.cache.delete(sticker.id);
+ /**
+ * Emitted whenever a custom sticker is deleted in a guild.
+ * @event Client#stickerDelete
+ * @param {Sticker} sticker The sticker that was deleted
+ */
+ this.client.emit(Events.GuildStickerDelete, sticker);
+ return { sticker };
+ }
+}
+
+module.exports = GuildStickerDeleteAction;
diff --git a/src/client/actions/GuildStickerUpdate.js b/src/client/actions/GuildStickerUpdate.js
new file mode 100644
index 00000000..5561c7e
--- /dev/null
+++ b/src/client/actions/GuildStickerUpdate.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildStickerUpdateAction extends Action {
+ handle(current, data) {
+ const old = current._update(data);
+ /**
+ * Emitted whenever a custom sticker is updated in a guild.
+ * @event Client#stickerUpdate
+ * @param {Sticker} oldSticker The old sticker
+ * @param {Sticker} newSticker The new sticker
+ */
+ this.client.emit(Events.GuildStickerUpdate, old, current);
+ return { sticker: current };
+ }
+}
+
+module.exports = GuildStickerUpdateAction;
diff --git a/src/client/actions/GuildStickersUpdate.js b/src/client/actions/GuildStickersUpdate.js
new file mode 100644
index 00000000..ccf1d63
--- /dev/null
+++ b/src/client/actions/GuildStickersUpdate.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const Action = require('./Action');
+
+class GuildStickersUpdateAction extends Action {
+ handle(data) {
+ const guild = this.client.guilds.cache.get(data.guild_id);
+ if (!guild?.stickers) return;
+
+ const deletions = new Map(guild.stickers.cache);
+
+ for (const sticker of data.stickers) {
+ // Determine type of sticker event
+ const cachedSticker = guild.stickers.cache.get(sticker.id);
+ if (cachedSticker) {
+ deletions.delete(sticker.id);
+ if (!cachedSticker.equals(sticker)) {
+ // Sticker updated
+ this.client.actions.GuildStickerUpdate.handle(cachedSticker, sticker);
+ }
+ } else {
+ // Sticker added
+ this.client.actions.GuildStickerCreate.handle(guild, sticker);
+ }
+ }
+
+ for (const sticker of deletions.values()) {
+ // Sticker deleted
+ this.client.actions.GuildStickerDelete.handle(sticker);
+ }
+ }
+}
+
+module.exports = GuildStickersUpdateAction;
diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js
new file mode 100644
index 00000000..ef1f51b
--- /dev/null
+++ b/src/client/actions/GuildUpdate.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class GuildUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.cache.get(data.id);
+ if (guild) {
+ const old = guild._update(data);
+ /**
+ * Emitted whenever a guild is updated - e.g. name change.
+ * @event Client#guildUpdate
+ * @param {Guild} oldGuild The guild before the update
+ * @param {Guild} newGuild The guild after the update
+ */
+ client.emit(Events.GuildUpdate, old, guild);
+ return {
+ old,
+ updated: guild,
+ };
+ }
+
+ return {
+ old: null,
+ updated: null,
+ };
+ }
+}
+
+module.exports = GuildUpdateAction;
diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js
new file mode 100644
index 00000000..8c36ec9
--- /dev/null
+++ b/src/client/actions/InteractionCreate.js
@@ -0,0 +1,76 @@
+'use strict';
+
+const { InteractionType, ComponentType, ApplicationCommandType } = require('discord-api-types/v9');
+const Action = require('./Action');
+const AutocompleteInteraction = require('../../structures/AutocompleteInteraction');
+const ButtonInteraction = require('../../structures/ButtonInteraction');
+const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction');
+const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction');
+const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
+const UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction');
+const Events = require('../../util/Events');
+
+class InteractionCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ // Resolve and cache partial channels for Interaction#channel getter
+ this.getChannel(data);
+
+ let InteractionClass;
+ switch (data.type) {
+ case InteractionType.ApplicationCommand:
+ switch (data.data.type) {
+ case ApplicationCommandType.ChatInput:
+ InteractionClass = ChatInputCommandInteraction;
+ break;
+ case ApplicationCommandType.User:
+ InteractionClass = UserContextMenuCommandInteraction;
+ break;
+ case ApplicationCommandType.Message:
+ InteractionClass = MessageContextMenuCommandInteraction;
+ break;
+ default:
+ client.emit(
+ Events.Debug,
+ `[INTERACTION] Received application command interaction with unknown type: ${data.data.type}`,
+ );
+ return;
+ }
+ break;
+ case InteractionType.MessageComponent:
+ switch (data.data.component_type) {
+ case ComponentType.Button:
+ InteractionClass = ButtonInteraction;
+ break;
+ case ComponentType.SelectMenu:
+ InteractionClass = SelectMenuInteraction;
+ break;
+ default:
+ client.emit(
+ Events.Debug,
+ `[INTERACTION] Received component interaction with unknown type: ${data.data.component_type}`,
+ );
+ return;
+ }
+ break;
+ case InteractionType.ApplicationCommandAutocomplete:
+ InteractionClass = AutocompleteInteraction;
+ break;
+ default:
+ client.emit(Events.Debug, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
+ return;
+ }
+
+ const interaction = new InteractionClass(client, data);
+
+ /**
+ * Emitted when an interaction is created.
+ * @event Client#interactionCreate
+ * @param {Interaction} interaction The interaction which was created
+ */
+ client.emit(Events.InteractionCreate, interaction);
+ }
+}
+
+module.exports = InteractionCreateAction;
diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js
new file mode 100644
index 00000000..2dc3019
--- /dev/null
+++ b/src/client/actions/InviteCreate.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class InviteCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.channels.cache.get(data.channel_id);
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (!channel) return false;
+
+ const inviteData = Object.assign(data, { channel, guild });
+ const invite = guild.invites._add(inviteData);
+
+ /**
+ * Emitted when an invite is created.
+ * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
+ * or `MANAGE_CHANNELS` permissions for the channel.
+ * @event Client#inviteCreate
+ * @param {Invite} invite The invite that was created
+ */
+ client.emit(Events.InviteCreate, invite);
+ return { invite };
+ }
+}
+
+module.exports = InviteCreateAction;
diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js
new file mode 100644
index 00000000..37b8143
--- /dev/null
+++ b/src/client/actions/InviteDelete.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const Action = require('./Action');
+const Invite = require('../../structures/Invite');
+const Events = require('../../util/Events');
+
+class InviteDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.channels.cache.get(data.channel_id);
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (!channel) return false;
+
+ const inviteData = Object.assign(data, { channel, guild });
+ const invite = new Invite(client, inviteData);
+ guild.invites.cache.delete(invite.code);
+
+ /**
+ * Emitted when an invite is deleted.
+ * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
+ * or `MANAGE_CHANNELS` permissions for the channel.
+ * @event Client#inviteDelete
+ * @param {Invite} invite The invite that was deleted
+ */
+ client.emit(Events.InviteDelete, invite);
+ return { invite };
+ }
+}
+
+module.exports = InviteDeleteAction;
diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js
new file mode 100644
index 00000000..9a099e2
--- /dev/null
+++ b/src/client/actions/MessageCreate.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class MessageCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = this.getChannel(data);
+ if (channel) {
+ if (!channel.isTextBased()) return {};
+
+ const existing = channel.messages.cache.get(data.id);
+ if (existing) return { message: existing };
+ const message = channel.messages._add(data);
+ channel.lastMessageId = data.id;
+
+ /**
+ * Emitted whenever a message is created.
+ * @event Client#messageCreate
+ * @param {Message} message The created message
+ */
+ client.emit(Events.MessageCreate, message);
+
+ return { message };
+ }
+
+ return {};
+ }
+}
+
+module.exports = MessageCreateAction;
diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js
new file mode 100644
index 00000000..cb55c67
--- /dev/null
+++ b/src/client/actions/MessageDelete.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class MessageDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = this.getChannel(data);
+ let message;
+ if (channel) {
+ if (!channel.isTextBased()) return {};
+
+ message = this.getMessage(data, channel);
+ if (message) {
+ channel.messages.cache.delete(message.id);
+ /**
+ * Emitted whenever a message is deleted.
+ * @event Client#messageDelete
+ * @param {Message} message The deleted message
+ */
+ client.emit(Events.MessageDelete, message);
+ }
+ }
+
+ return { message };
+ }
+}
+
+module.exports = MessageDeleteAction;
diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js
new file mode 100644
index 00000000..148e665
--- /dev/null
+++ b/src/client/actions/MessageDeleteBulk.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class MessageDeleteBulkAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.channels.cache.get(data.channel_id);
+
+ if (channel) {
+ if (!channel.isTextBased()) return {};
+
+ const ids = data.ids;
+ const messages = new Collection();
+ for (const id of ids) {
+ const message = this.getMessage(
+ {
+ id,
+ guild_id: data.guild_id,
+ },
+ channel,
+ false,
+ );
+ if (message) {
+ messages.set(message.id, message);
+ channel.messages.cache.delete(id);
+ }
+ }
+
+ /**
+ * Emitted whenever messages are deleted in bulk.
+ * @event Client#messageDeleteBulk
+ * @param {Collection} messages The deleted messages, mapped by their id
+ */
+ if (messages.size > 0) client.emit(Events.MessageBulkDelete, messages);
+ return { messages };
+ }
+ return {};
+ }
+}
+
+module.exports = MessageDeleteBulkAction;
diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js
new file mode 100644
index 00000000..ea97bd6
--- /dev/null
+++ b/src/client/actions/MessageReactionAdd.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+const Partials = require('../../util/Partials');
+
+/*
+{ user_id: 'id',
+ message_id: 'id',
+ emoji: { name: '�', id: null },
+ channel_id: 'id',
+ // If originating from a guild
+ guild_id: 'id',
+ member: { ..., user: { ... } } }
+*/
+
+class MessageReactionAdd extends Action {
+ handle(data, fromStructure = false) {
+ if (!data.emoji) return false;
+
+ const user = this.getUserFromMember(data);
+ if (!user) return false;
+
+ // Verify channel
+ const channel = this.getChannel(data);
+ if (!channel?.isTextBased()) return false;
+
+ // Verify message
+ const message = this.getMessage(data, channel);
+ if (!message) return false;
+
+ // Verify reaction
+ const includePartial = this.client.options.partials.includes(Partials.Reaction);
+ if (message.partial && !includePartial) return false;
+ const reaction = message.reactions._add({
+ emoji: data.emoji,
+ count: message.partial ? null : 0,
+ me: user.id === this.client.user.id,
+ });
+ if (!reaction) return false;
+ reaction._add(user);
+ if (fromStructure) return { message, reaction, user };
+ /**
+ * Emitted whenever a reaction is added to a cached message.
+ * @event Client#messageReactionAdd
+ * @param {MessageReaction} messageReaction The reaction object
+ * @param {User} user The user that applied the guild or reaction emoji
+ */
+ this.client.emit(Events.MessageReactionAdd, reaction, user);
+
+ return { message, reaction, user };
+ }
+}
+
+module.exports = MessageReactionAdd;
diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js
new file mode 100644
index 00000000..9ca3a8e
--- /dev/null
+++ b/src/client/actions/MessageReactionRemove.js
@@ -0,0 +1,45 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+/*
+{ user_id: 'id',
+ message_id: 'id',
+ emoji: { name: '�', id: null },
+ channel_id: 'id',
+ guild_id: 'id' }
+*/
+
+class MessageReactionRemove extends Action {
+ handle(data) {
+ if (!data.emoji) return false;
+
+ const user = this.getUser(data);
+ if (!user) return false;
+
+ // Verify channel
+ const channel = this.getChannel(data);
+ if (!channel?.isTextBased()) return false;
+
+ // Verify message
+ const message = this.getMessage(data, channel);
+ if (!message) return false;
+
+ // Verify reaction
+ const reaction = this.getReaction(data, message, user);
+ if (!reaction) return false;
+ reaction._remove(user);
+ /**
+ * Emitted whenever a reaction is removed from a cached message.
+ * @event Client#messageReactionRemove
+ * @param {MessageReaction} messageReaction The reaction object
+ * @param {User} user The user whose emoji or reaction emoji was removed
+ */
+ this.client.emit(Events.MessageReactionRemove, reaction, user);
+
+ return { message, reaction, user };
+ }
+}
+
+module.exports = MessageReactionRemove;
diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js
new file mode 100644
index 00000000..b1c023f
--- /dev/null
+++ b/src/client/actions/MessageReactionRemoveAll.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class MessageReactionRemoveAll extends Action {
+ handle(data) {
+ // Verify channel
+ const channel = this.getChannel(data);
+ if (!channel?.isTextBased()) return false;
+
+ // Verify message
+ const message = this.getMessage(data, channel);
+ if (!message) return false;
+
+ // Copy removed reactions to emit for the event.
+ const removed = message.reactions.cache.clone();
+
+ message.reactions.cache.clear();
+ this.client.emit(Events.MessageReactionRemoveAll, message, removed);
+
+ return { message };
+ }
+}
+
+/**
+ * Emitted whenever all reactions are removed from a cached message.
+ * @event Client#messageReactionRemoveAll
+ * @param {Message} message The message the reactions were removed from
+ * @param {Collection} reactions The cached message reactions that were removed.
+ */
+
+module.exports = MessageReactionRemoveAll;
diff --git a/src/client/actions/MessageReactionRemoveEmoji.js b/src/client/actions/MessageReactionRemoveEmoji.js
new file mode 100644
index 00000000..3290214
--- /dev/null
+++ b/src/client/actions/MessageReactionRemoveEmoji.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class MessageReactionRemoveEmoji extends Action {
+ handle(data) {
+ const channel = this.getChannel(data);
+ if (!channel?.isTextBased()) return false;
+
+ const message = this.getMessage(data, channel);
+ if (!message) return false;
+
+ const reaction = this.getReaction(data, message);
+ if (!reaction) return false;
+ if (!message.partial) message.reactions.cache.delete(reaction.emoji.id ?? reaction.emoji.name);
+
+ /**
+ * Emitted when a bot removes an emoji reaction from a cached message.
+ * @event Client#messageReactionRemoveEmoji
+ * @param {MessageReaction} reaction The reaction that was removed
+ */
+ this.client.emit(Events.MessageReactionRemoveEmoji, reaction);
+ return { reaction };
+ }
+}
+
+module.exports = MessageReactionRemoveEmoji;
diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js
new file mode 100644
index 00000000..fe757c0
--- /dev/null
+++ b/src/client/actions/MessageUpdate.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const Action = require('./Action');
+
+class MessageUpdateAction extends Action {
+ handle(data) {
+ const channel = this.getChannel(data);
+ if (channel) {
+ if (!channel.isTextBased()) return {};
+
+ const { id, channel_id, guild_id, author, timestamp, type } = data;
+ const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
+ if (message) {
+ const old = message._update(data);
+ return {
+ old,
+ updated: message,
+ };
+ }
+ }
+
+ return {};
+ }
+}
+
+module.exports = MessageUpdateAction;
diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js
new file mode 100644
index 00000000..0b4aaab
--- /dev/null
+++ b/src/client/actions/PresenceUpdate.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class PresenceUpdateAction extends Action {
+ handle(data) {
+ let user = this.client.users.cache.get(data.user.id);
+ if (!user && data.user.username) user = this.client.users._add(data.user);
+ if (!user) return;
+
+ if (data.user.username) {
+ if (!user._equals(data.user)) this.client.actions.UserUpdate.handle(data.user);
+ }
+
+ const guild = this.client.guilds.cache.get(data.guild_id);
+ if (!guild) return;
+
+ const oldPresence = guild.presences.cache.get(user.id)?._clone() ?? null;
+ let member = guild.members.cache.get(user.id);
+ if (!member && data.status !== 'offline') {
+ member = guild.members._add({
+ user,
+ deaf: false,
+ mute: false,
+ });
+ this.client.emit(Events.GuildMemberAvailable, member);
+ }
+ const newPresence = guild.presences._add(Object.assign(data, { guild }));
+ if (this.client.listenerCount(Events.PresenceUpdate) && !newPresence.equals(oldPresence)) {
+ /**
+ * Emitted whenever a guild member's presence (e.g. status, activity) is changed.
+ * @event Client#presenceUpdate
+ * @param {?Presence} oldPresence The presence before the update, if one at all
+ * @param {Presence} newPresence The presence after the update
+ */
+ this.client.emit(Events.PresenceUpdate, oldPresence, newPresence);
+ }
+ }
+}
+
+module.exports = PresenceUpdateAction;
diff --git a/src/client/actions/StageInstanceCreate.js b/src/client/actions/StageInstanceCreate.js
new file mode 100644
index 00000000..4edd530
--- /dev/null
+++ b/src/client/actions/StageInstanceCreate.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class StageInstanceCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = this.getChannel(data);
+
+ if (channel) {
+ const stageInstance = channel.guild.stageInstances._add(data);
+
+ /**
+ * Emitted whenever a stage instance is created.
+ * @event Client#stageInstanceCreate
+ * @param {StageInstance} stageInstance The created stage instance
+ */
+ client.emit(Events.StageInstanceCreate, stageInstance);
+
+ return { stageInstance };
+ }
+
+ return {};
+ }
+}
+
+module.exports = StageInstanceCreateAction;
diff --git a/src/client/actions/StageInstanceDelete.js b/src/client/actions/StageInstanceDelete.js
new file mode 100644
index 00000000..0d5da38
--- /dev/null
+++ b/src/client/actions/StageInstanceDelete.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class StageInstanceDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = this.getChannel(data);
+
+ if (channel) {
+ const stageInstance = channel.guild.stageInstances._add(data);
+ if (stageInstance) {
+ channel.guild.stageInstances.cache.delete(stageInstance.id);
+
+ /**
+ * Emitted whenever a stage instance is deleted.
+ * @event Client#stageInstanceDelete
+ * @param {StageInstance} stageInstance The deleted stage instance
+ */
+ client.emit(Events.StageInstanceDelete, stageInstance);
+
+ return { stageInstance };
+ }
+ }
+
+ return {};
+ }
+}
+
+module.exports = StageInstanceDeleteAction;
diff --git a/src/client/actions/StageInstanceUpdate.js b/src/client/actions/StageInstanceUpdate.js
new file mode 100644
index 00000000..008a53c
--- /dev/null
+++ b/src/client/actions/StageInstanceUpdate.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class StageInstanceUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = this.getChannel(data);
+
+ if (channel) {
+ const oldStageInstance = channel.guild.stageInstances.cache.get(data.id)?._clone() ?? null;
+ const newStageInstance = channel.guild.stageInstances._add(data);
+
+ /**
+ * Emitted whenever a stage instance gets updated - e.g. change in topic or privacy level
+ * @event Client#stageInstanceUpdate
+ * @param {?StageInstance} oldStageInstance The stage instance before the update
+ * @param {StageInstance} newStageInstance The stage instance after the update
+ */
+ client.emit(Events.StageInstanceUpdate, oldStageInstance, newStageInstance);
+
+ return { oldStageInstance, newStageInstance };
+ }
+
+ return {};
+ }
+}
+
+module.exports = StageInstanceUpdateAction;
diff --git a/src/client/actions/ThreadCreate.js b/src/client/actions/ThreadCreate.js
new file mode 100644
index 00000000..a8ff6c6
--- /dev/null
+++ b/src/client/actions/ThreadCreate.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ThreadCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const existing = client.channels.cache.has(data.id);
+ const thread = client.channels._add(data);
+ if (!existing && thread) {
+ /**
+ * Emitted whenever a thread is created or when the client user is added to a thread.
+ * @event Client#threadCreate
+ * @param {ThreadChannel} thread The thread that was created
+ * @param {boolean} newlyCreated Whether the thread was newly created
+ */
+ client.emit(Events.ThreadCreate, thread, data.newly_created ?? false);
+ }
+ return { thread };
+ }
+}
+
+module.exports = ThreadCreateAction;
diff --git a/src/client/actions/ThreadDelete.js b/src/client/actions/ThreadDelete.js
new file mode 100644
index 00000000..3ec81a4
--- /dev/null
+++ b/src/client/actions/ThreadDelete.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ThreadDeleteAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const thread = client.channels.cache.get(data.id);
+
+ if (thread) {
+ client.channels._remove(thread.id);
+
+ /**
+ * Emitted whenever a thread is deleted.
+ * @event Client#threadDelete
+ * @param {ThreadChannel} thread The thread that was deleted
+ */
+ client.emit(Events.ThreadDelete, thread);
+ }
+
+ return { thread };
+ }
+}
+
+module.exports = ThreadDeleteAction;
diff --git a/src/client/actions/ThreadListSync.js b/src/client/actions/ThreadListSync.js
new file mode 100644
index 00000000..bad1a47
--- /dev/null
+++ b/src/client/actions/ThreadListSync.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ThreadListSyncAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (!guild) return {};
+
+ if (data.channel_ids) {
+ for (const id of data.channel_ids) {
+ const channel = client.channels.resolve(id);
+ if (channel) this.removeStale(channel);
+ }
+ } else {
+ for (const channel of guild.channels.cache.values()) {
+ this.removeStale(channel);
+ }
+ }
+
+ const syncedThreads = data.threads.reduce((coll, rawThread) => {
+ const thread = client.channels._add(rawThread);
+ return coll.set(thread.id, thread);
+ }, new Collection());
+
+ for (const rawMember of Object.values(data.members)) {
+ // Discord sends the thread id as id in this object
+ const thread = client.channels.cache.get(rawMember.id);
+ if (thread) {
+ thread.members._add(rawMember);
+ }
+ }
+
+ /**
+ * Emitted whenever the client user gains access to a text or news channel that contains threads
+ * @event Client#threadListSync
+ * @param {Collection} threads The threads that were synced
+ */
+ client.emit(Events.ThreadListSync, syncedThreads);
+
+ return {
+ syncedThreads,
+ };
+ }
+
+ removeStale(channel) {
+ channel.threads?.cache.forEach(thread => {
+ if (!thread.archived) {
+ this.client.channels._remove(thread.id);
+ }
+ });
+ }
+}
+
+module.exports = ThreadListSyncAction;
diff --git a/src/client/actions/ThreadMemberUpdate.js b/src/client/actions/ThreadMemberUpdate.js
new file mode 100644
index 00000000..0b17f70
--- /dev/null
+++ b/src/client/actions/ThreadMemberUpdate.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ThreadMemberUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ // Discord sends the thread id as id in this object
+ const thread = client.channels.cache.get(data.id);
+ if (thread) {
+ const member = thread.members.cache.get(data.user_id);
+ if (!member) {
+ const newMember = thread.members._add(data);
+ return { newMember };
+ }
+ const old = member._update(data);
+ /**
+ * Emitted whenever the client user's thread member is updated.
+ * @event Client#threadMemberUpdate
+ * @param {ThreadMember} oldMember The member before the update
+ * @param {ThreadMember} newMember The member after the update
+ */
+ client.emit(Events.ThreadMemberUpdate, old, member);
+ }
+ return {};
+ }
+}
+
+module.exports = ThreadMemberUpdateAction;
diff --git a/src/client/actions/ThreadMembersUpdate.js b/src/client/actions/ThreadMembersUpdate.js
new file mode 100644
index 00000000..26ab70e
--- /dev/null
+++ b/src/client/actions/ThreadMembersUpdate.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class ThreadMembersUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const thread = client.channels.cache.get(data.id);
+ if (thread) {
+ const old = thread.members.cache.clone();
+ thread.memberCount = data.member_count;
+
+ data.added_members?.forEach(rawMember => {
+ thread.members._add(rawMember);
+ });
+
+ data.removed_member_ids?.forEach(memberId => {
+ thread.members.cache.delete(memberId);
+ });
+
+ /**
+ * Emitted whenever members are added or removed from a thread. Requires `GUILD_MEMBERS` privileged intent
+ * @event Client#threadMembersUpdate
+ * @param {Collection} oldMembers The members before the update
+ * @param {Collection} newMembers The members after the update
+ */
+ client.emit(Events.ThreadMembersUpdate, old, thread.members.cache);
+ }
+ return {};
+ }
+}
+
+module.exports = ThreadMembersUpdateAction;
diff --git a/src/client/actions/TypingStart.js b/src/client/actions/TypingStart.js
new file mode 100644
index 00000000..4e79920
--- /dev/null
+++ b/src/client/actions/TypingStart.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const Action = require('./Action');
+const Typing = require('../../structures/Typing');
+const Events = require('../../util/Events');
+
+class TypingStart extends Action {
+ handle(data) {
+ const channel = this.getChannel(data);
+ if (!channel) return;
+
+ if (!channel.isTextBased()) {
+ this.client.emit(Events.Warn, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`);
+ return;
+ }
+
+ const user = this.getUserFromMember(data);
+ if (user) {
+ /**
+ * Emitted whenever a user starts typing in a channel.
+ * @event Client#typingStart
+ * @param {Typing} typing The typing state
+ */
+ this.client.emit(Events.TypingStart, new Typing(channel, user, data));
+ }
+ }
+}
+
+module.exports = TypingStart;
diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js
new file mode 100644
index 00000000..1bf236a
--- /dev/null
+++ b/src/client/actions/UserUpdate.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class UserUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const newUser = data.id === client.user.id ? client.user : client.users.cache.get(data.id);
+ const oldUser = newUser._update(data);
+
+ if (!oldUser.equals(newUser)) {
+ /**
+ * Emitted whenever a user's details (e.g. username) are changed.
+ * Triggered by the Discord gateway events USER_UPDATE, GUILD_MEMBER_UPDATE, and PRESENCE_UPDATE.
+ * @event Client#userUpdate
+ * @param {User} oldUser The user before the update
+ * @param {User} newUser The user after the update
+ */
+ client.emit(Events.UserUpdate, oldUser, newUser);
+ return {
+ old: oldUser,
+ updated: newUser,
+ };
+ }
+
+ return {
+ old: null,
+ updated: null,
+ };
+ }
+}
+
+module.exports = UserUpdateAction;
diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js
new file mode 100644
index 00000000..fc7400f
--- /dev/null
+++ b/src/client/actions/VoiceStateUpdate.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const Action = require('./Action');
+const VoiceState = require('../../structures/VoiceState');
+const Events = require('../../util/Events');
+
+class VoiceStateUpdate extends Action {
+ handle(data) {
+ const client = this.client;
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ // Update the state
+ const oldState =
+ guild.voiceStates.cache.get(data.user_id)?._clone() ?? new VoiceState(guild, { user_id: data.user_id });
+
+ const newState = guild.voiceStates._add(data);
+
+ // Get the member
+ let member = guild.members.cache.get(data.user_id);
+ if (member && data.member) {
+ member._patch(data.member);
+ } else if (data.member?.user && data.member.joined_at) {
+ member = guild.members._add(data.member);
+ }
+
+ // Emit event
+ if (member?.user.id === client.user.id) {
+ client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
+ client.voice.onVoiceStateUpdate(data);
+ }
+
+ /**
+ * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
+ * @event Client#voiceStateUpdate
+ * @param {VoiceState} oldState The voice state before the update
+ * @param {VoiceState} newState The voice state after the update
+ */
+ client.emit(Events.VoiceStateUpdate, oldState, newState);
+ }
+ }
+}
+
+module.exports = VoiceStateUpdate;
diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js
new file mode 100644
index 00000000..b362b3b
--- /dev/null
+++ b/src/client/actions/WebhooksUpdate.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const Action = require('./Action');
+const Events = require('../../util/Events');
+
+class WebhooksUpdate extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.channels.cache.get(data.channel_id);
+ /**
+ * Emitted whenever a channel has its webhooks changed.
+ * @event Client#webhookUpdate
+ * @param {TextChannel|NewsChannel} channel The channel that had a webhook update
+ */
+ if (channel) client.emit(Events.WebhooksUpdate, channel);
+ }
+}
+
+module.exports = WebhooksUpdate;
diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js
new file mode 100644
index 00000000..192e700
--- /dev/null
+++ b/src/client/voice/ClientVoiceManager.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const Events = require('../../util/Events');
+
+/**
+ * Manages voice connections for the client
+ */
+class ClientVoiceManager {
+ constructor(client) {
+ /**
+ * The client that instantiated this voice manager
+ * @type {Client}
+ * @readonly
+ * @name ClientVoiceManager#client
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * Maps guild ids to voice adapters created for use with @discordjs/voice.
+ * @type {Map}
+ */
+ this.adapters = new Map();
+
+ client.on(Events.ShardDisconnect, (_, shardId) => {
+ for (const [guildId, adapter] of this.adapters.entries()) {
+ if (client.guilds.cache.get(guildId)?.shardId === shardId) {
+ adapter.destroy();
+ }
+ }
+ });
+ }
+
+ onVoiceServer(payload) {
+ this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
+ }
+
+ onVoiceStateUpdate(payload) {
+ if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
+ this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
+ }
+ }
+}
+
+module.exports = ClientVoiceManager;
diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js
new file mode 100644
index 00000000..a08b88a
--- /dev/null
+++ b/src/client/websocket/WebSocketManager.js
@@ -0,0 +1,394 @@
+'use strict';
+
+const EventEmitter = require('node:events');
+const { setImmediate } = require('node:timers');
+const { setTimeout: sleep } = require('node:timers/promises');
+const { Collection } = require('@discordjs/collection');
+const { GatewayCloseCodes, GatewayDispatchEvents, Routes } = require('discord-api-types/v9');
+const WebSocketShard = require('./WebSocketShard');
+const PacketHandlers = require('./handlers');
+const { Error } = require('../../errors');
+const Events = require('../../util/Events');
+const ShardEvents = require('../../util/ShardEvents');
+const Status = require('../../util/Status');
+
+const BeforeReadyWhitelist = [
+ GatewayDispatchEvents.Ready,
+ GatewayDispatchEvents.Resumed,
+ GatewayDispatchEvents.GuildCreate,
+ GatewayDispatchEvents.GuildDelete,
+ GatewayDispatchEvents.GuildMembersChunk,
+ GatewayDispatchEvents.GuildMemberAdd,
+ GatewayDispatchEvents.GuildMemberRemove,
+];
+
+const UNRECOVERABLE_CLOSE_CODES = [
+ GatewayCloseCodes.AuthenticationFailed,
+ GatewayCloseCodes.InvalidShard,
+ GatewayCloseCodes.ShardingRequired,
+ GatewayCloseCodes.InvalidIntents,
+ GatewayCloseCodes.DisallowedIntents,
+];
+const UNRESUMABLE_CLOSE_CODES = [1000, GatewayCloseCodes.AlreadyAuthenticated, GatewayCloseCodes.InvalidSeq];
+
+/**
+ * The WebSocket manager for this client.
+ * This class forwards raw dispatch events,
+ * read more about it here {@link https://discord.com/developers/docs/topics/gateway}
+ * @extends EventEmitter
+ */
+class WebSocketManager extends EventEmitter {
+ constructor(client) {
+ super();
+
+ /**
+ * The client that instantiated this WebSocketManager
+ * @type {Client}
+ * @readonly
+ * @name WebSocketManager#client
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The gateway this manager uses
+ * @type {?string}
+ */
+ this.gateway = null;
+
+ /**
+ * The amount of shards this manager handles
+ * @private
+ * @type {number}
+ */
+ this.totalShards = this.client.options.shards.length;
+
+ /**
+ * A collection of all shards this manager handles
+ * @type {Collection}
+ */
+ this.shards = new Collection();
+
+ /**
+ * An array of shards to be connected or that need to reconnect
+ * @type {Set}
+ * @private
+ * @name WebSocketManager#shardQueue
+ */
+ Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true });
+
+ /**
+ * An array of queued events before this WebSocketManager became ready
+ * @type {Object[]}
+ * @private
+ * @name WebSocketManager#packetQueue
+ */
+ Object.defineProperty(this, 'packetQueue', { value: [] });
+
+ /**
+ * The current status of this WebSocketManager
+ * @type {Status}
+ */
+ this.status = Status.Idle;
+
+ /**
+ * If this manager was destroyed. It will prevent shards from reconnecting
+ * @type {boolean}
+ * @private
+ */
+ this.destroyed = false;
+
+ /**
+ * If this manager is currently reconnecting one or multiple shards
+ * @type {boolean}
+ * @private
+ */
+ this.reconnecting = false;
+ }
+
+ /**
+ * The average ping of all WebSocketShards
+ * @type {number}
+ * @readonly
+ */
+ get ping() {
+ const sum = this.shards.reduce((a, b) => a + b.ping, 0);
+ return sum / this.shards.size;
+ }
+
+ /**
+ * Emits a debug message.
+ * @param {string} message The debug message
+ * @param {?WebSocketShard} [shard] The shard that emitted this message, if any
+ * @private
+ */
+ debug(message, shard) {
+ this.client.emit(Events.Debug, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
+ }
+
+ /**
+ * Connects this manager to the gateway.
+ * @private
+ */
+ async connect() {
+ const invalidToken = new Error(GatewayCloseCodes[GatewayCloseCodes.AuthenticationFailed]);
+ const {
+ url: gatewayURL,
+ shards: recommendedShards,
+ session_start_limit: sessionStartLimit,
+ } = await this.client.api.gateway.bot.get().catch(error => {
+ throw error.httpStatus === 401 ? invalidToken : error;
+ });
+
+ const { total, remaining } = sessionStartLimit;
+
+ this.debug(`Fetched Gateway Information
+ URL: ${gatewayURL}
+ Recommended Shards: ${recommendedShards}`);
+
+ this.debug(`Session Limit Information
+ Total: ${total}
+ Remaining: ${remaining}`);
+
+ this.gateway = `${gatewayURL}/`;
+
+ let { shards } = this.client.options;
+
+ if (shards === 'auto') {
+ this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
+ this.totalShards = this.client.options.shardCount = recommendedShards;
+ shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
+ }
+
+ this.totalShards = shards.length;
+ this.debug(`Spawning shards: ${shards.join(', ')}`);
+ this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id)));
+
+ return this.createShards();
+ }
+
+ /**
+ * Handles the creation of a shard.
+ * @returns {Promise}
+ * @private
+ */
+ async createShards() {
+ // If we don't have any shards to handle, return
+ if (!this.shardQueue.size) return false;
+
+ const [shard] = this.shardQueue;
+
+ this.shardQueue.delete(shard);
+
+ if (!shard.eventsAttached) {
+ shard.on(ShardEvents.AllReady, unavailableGuilds => {
+ /**
+ * Emitted when a shard turns ready.
+ * @event Client#shardReady
+ * @param {number} id The shard id that turned ready
+ * @param {?Set} unavailableGuilds Set of unavailable guild ids, if any
+ */
+ this.client.emit(Events.ShardReady, shard.id, unavailableGuilds);
+
+ if (!this.shardQueue.size) this.reconnecting = false;
+ this.checkShardsReady();
+ });
+
+ shard.on(ShardEvents.Close, event => {
+ if (event.code === 1_000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) {
+ /**
+ * Emitted when a shard's WebSocket disconnects and will no longer reconnect.
+ * @event Client#shardDisconnect
+ * @param {CloseEvent} event The WebSocket close event
+ * @param {number} id The shard id that disconnected
+ */
+ this.client.emit(Events.ShardDisconnect, event, shard.id);
+ this.debug(GatewayCloseCodes[event.code], shard);
+ return;
+ }
+
+ if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
+ // These event codes cannot be resumed
+ shard.sessionId = null;
+ }
+
+ /**
+ * Emitted when a shard is attempting to reconnect or re-identify.
+ * @event Client#shardReconnecting
+ * @param {number} id The shard id that is attempting to reconnect
+ */
+ this.client.emit(Events.ShardReconnecting, shard.id);
+
+ this.shardQueue.add(shard);
+
+ if (shard.sessionId) {
+ this.debug(`Session id is present, attempting an immediate reconnect...`, shard);
+ this.reconnect();
+ } else {
+ shard.destroy({ reset: true, emit: false, log: false });
+ this.reconnect();
+ }
+ });
+
+ shard.on(ShardEvents.InvalidSession, () => {
+ this.client.emit(Events.ShardReconnecting, shard.id);
+ });
+
+ shard.on(ShardEvents.Destroyed, () => {
+ this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard);
+
+ this.client.emit(Events.ShardReconnecting, shard.id);
+
+ this.shardQueue.add(shard);
+ this.reconnect();
+ });
+
+ shard.eventsAttached = true;
+ }
+
+ this.shards.set(shard.id, shard);
+
+ try {
+ await shard.connect();
+ } catch (error) {
+ if (error?.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) {
+ throw new Error(GatewayCloseCodes[error.code]);
+ // Undefined if session is invalid, error event for regular closes
+ } else if (!error || error.code) {
+ this.debug('Failed to connect to the gateway, requeueing...', shard);
+ this.shardQueue.add(shard);
+ } else {
+ throw error;
+ }
+ }
+ // If we have more shards, add a 5s delay
+ if (this.shardQueue.size) {
+ this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`);
+ await sleep(5_000);
+ return this.createShards();
+ }
+
+ return true;
+ }
+
+ /**
+ * Handles reconnects for this manager.
+ * @private
+ * @returns {Promise}
+ */
+ async reconnect() {
+ if (this.reconnecting || this.status !== Status.Ready) return false;
+ this.reconnecting = true;
+ try {
+ await this.createShards();
+ } catch (error) {
+ this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
+ if (error.httpStatus !== 401) {
+ this.debug(`Possible network error occurred. Retrying in 5s...`);
+ await sleep(5_000);
+ this.reconnecting = false;
+ return this.reconnect();
+ }
+ // If we get an error at this point, it means we cannot reconnect anymore
+ if (this.client.listenerCount(Events.Invalidated)) {
+ /**
+ * Emitted when the client's session becomes invalidated.
+ * You are expected to handle closing the process gracefully and preventing a boot loop
+ * if you are listening to this event.
+ * @event Client#invalidated
+ */
+ this.client.emit(Events.Invalidated);
+ // Destroy just the shards. This means you have to handle the cleanup yourself
+ this.destroy();
+ } else {
+ this.client.destroy();
+ }
+ } finally {
+ this.reconnecting = false;
+ }
+ return true;
+ }
+
+ /**
+ * Broadcasts a packet to every shard this manager handles.
+ * @param {Object} packet The packet to send
+ * @private
+ */
+ broadcast(packet) {
+ for (const shard of this.shards.values()) shard.send(packet);
+ }
+
+ /**
+ * Destroys this manager and all its shards.
+ * @private
+ */
+ destroy() {
+ if (this.destroyed) return;
+ this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`);
+ this.destroyed = true;
+ this.shardQueue.clear();
+ for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
+ }
+
+ /**
+ * Processes a packet and queues it if this WebSocketManager is not ready.
+ * @param {Object} [packet] The packet to be handled
+ * @param {WebSocketShard} [shard] The shard that will handle this packet
+ * @returns {boolean}
+ * @private
+ */
+ handlePacket(packet, shard) {
+ if (packet && this.status !== Status.Ready) {
+ if (!BeforeReadyWhitelist.includes(packet.t)) {
+ this.packetQueue.push({ packet, shard });
+ return false;
+ }
+ }
+
+ if (this.packetQueue.length) {
+ const item = this.packetQueue.shift();
+ setImmediate(() => {
+ this.handlePacket(item.packet, item.shard);
+ }).unref();
+ }
+
+ if (packet && PacketHandlers[packet.t]) {
+ PacketHandlers[packet.t](this.client, packet, shard);
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether the client is ready to be marked as ready.
+ * @private
+ */
+ checkShardsReady() {
+ if (this.status === Status.Ready) return;
+ if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.Ready)) {
+ return;
+ }
+
+ this.triggerClientReady();
+ }
+
+ /**
+ * Causes the client to be marked as ready and emits the ready event.
+ * @private
+ */
+ triggerClientReady() {
+ this.status = Status.Ready;
+
+ this.client.readyTimestamp = Date.now();
+
+ /**
+ * Emitted when the client becomes ready to start working.
+ * @event Client#ready
+ * @param {Client} client The client
+ */
+ this.client.emit(Events.ClientReady, this.client);
+
+ this.handlePacket();
+ }
+}
+
+module.exports = WebSocketManager;
diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js
new file mode 100644
index 00000000..34f34f7
--- /dev/null
+++ b/src/client/websocket/WebSocketShard.js
@@ -0,0 +1,787 @@
+'use strict';
+
+const EventEmitter = require('node:events');
+const { setTimeout, setInterval, clearTimeout, clearInterval } = require('node:timers');
+const { GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } = require('discord-api-types/v9');
+const WebSocket = require('../../WebSocket');
+const Events = require('../../util/Events');
+const IntentsBitField = require('../../util/IntentsBitField');
+const ShardEvents = require('../../util/ShardEvents');
+const Status = require('../../util/Status');
+
+const STATUS_KEYS = Object.keys(Status);
+const CONNECTION_STATE = Object.keys(WebSocket.WebSocket);
+
+let zlib;
+
+try {
+ zlib = require('zlib-sync');
+} catch {} // eslint-disable-line no-empty
+
+/**
+ * Represents a Shard's WebSocket connection
+ */
+class WebSocketShard extends EventEmitter {
+ constructor(manager, id) {
+ super();
+
+ /**
+ * The WebSocketManager of the shard
+ * @type {WebSocketManager}
+ */
+ this.manager = manager;
+
+ /**
+ * The shard's id
+ * @type {number}
+ */
+ this.id = id;
+
+ /**
+ * The current status of the shard
+ * @type {Status}
+ */
+ this.status = Status.Idle;
+
+ /**
+ * The current sequence of the shard
+ * @type {number}
+ * @private
+ */
+ this.sequence = -1;
+
+ /**
+ * The sequence of the shard after close
+ * @type {number}
+ * @private
+ */
+ this.closeSequence = 0;
+
+ /**
+ * The current session id of the shard
+ * @type {?string}
+ * @private
+ */
+ this.sessionId = null;
+
+ /**
+ * The previous heartbeat ping of the shard
+ * @type {number}
+ */
+ this.ping = -1;
+
+ /**
+ * The last time a ping was sent (a timestamp)
+ * @type {number}
+ * @private
+ */
+ this.lastPingTimestamp = -1;
+
+ /**
+ * If we received a heartbeat ack back. Used to identify zombie connections
+ * @type {boolean}
+ * @private
+ */
+ this.lastHeartbeatAcked = true;
+
+ /**
+ * Contains the rate limit queue and metadata
+ * @name WebSocketShard#ratelimit
+ * @type {Object}
+ * @private
+ */
+ Object.defineProperty(this, 'ratelimit', {
+ value: {
+ queue: [],
+ total: 120,
+ remaining: 120,
+ time: 60e3,
+ timer: null,
+ },
+ });
+
+ /**
+ * The WebSocket connection for the current shard
+ * @name WebSocketShard#connection
+ * @type {?WebSocket}
+ * @private
+ */
+ Object.defineProperty(this, 'connection', { value: null, writable: true });
+
+ /**
+ * @external Inflate
+ * @see {@link https://www.npmjs.com/package/zlib-sync}
+ */
+
+ /**
+ * The compression to use
+ * @name WebSocketShard#inflate
+ * @type {?Inflate}
+ * @private
+ */
+ Object.defineProperty(this, 'inflate', { value: null, writable: true });
+
+ /**
+ * The HELLO timeout
+ * @name WebSocketShard#helloTimeout
+ * @type {?NodeJS.Timeout}
+ * @private
+ */
+ Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
+
+ /**
+ * If the manager attached its event handlers on the shard
+ * @name WebSocketShard#eventsAttached
+ * @type {boolean}
+ * @private
+ */
+ Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
+
+ /**
+ * A set of guild ids this shard expects to receive
+ * @name WebSocketShard#expectedGuilds
+ * @type {?Set}
+ * @private
+ */
+ Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
+
+ /**
+ * The ready timeout
+ * @name WebSocketShard#readyTimeout
+ * @type {?NodeJS.Timeout}
+ * @private
+ */
+ Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
+
+ /**
+ * Time when the WebSocket connection was opened
+ * @name WebSocketShard#connectedAt
+ * @type {number}
+ * @private
+ */
+ Object.defineProperty(this, 'connectedAt', { value: 0, writable: true });
+ }
+
+ /**
+ * Emits a debug event.
+ * @param {string} message The debug message
+ * @private
+ */
+ debug(message) {
+ this.manager.debug(message, this);
+ }
+
+ /**
+ * Connects the shard to the gateway.
+ * @private
+ * @returns {Promise} A promise that will resolve if the shard turns ready successfully,
+ * or reject if we couldn't connect
+ */
+ connect() {
+ const { gateway, client } = this.manager;
+
+ if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.Ready) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ const cleanup = () => {
+ this.removeListener(ShardEvents.Close, onClose);
+ this.removeListener(ShardEvents.Ready, onReady);
+ this.removeListener(ShardEvents.Resumed, onResumed);
+ this.removeListener(ShardEvents.InvalidSession, onInvalidOrDestroyed);
+ this.removeListener(ShardEvents.Destroyed, onInvalidOrDestroyed);
+ };
+
+ const onReady = () => {
+ cleanup();
+ resolve();
+ };
+
+ const onResumed = () => {
+ cleanup();
+ resolve();
+ };
+
+ const onClose = event => {
+ cleanup();
+ reject(event);
+ };
+
+ const onInvalidOrDestroyed = () => {
+ cleanup();
+ // eslint-disable-next-line prefer-promise-reject-errors
+ reject();
+ };
+
+ this.once(ShardEvents.Ready, onReady);
+ this.once(ShardEvents.Resumed, onResumed);
+ this.once(ShardEvents.Close, onClose);
+ this.once(ShardEvents.InvalidSession, onInvalidOrDestroyed);
+ this.once(ShardEvents.Destroyed, onInvalidOrDestroyed);
+
+ if (this.connection?.readyState === WebSocket.OPEN) {
+ this.debug('An open connection was found, attempting an immediate identify.');
+ this.identify();
+ return;
+ }
+
+ if (this.connection) {
+ this.debug(`A connection object was found. Cleaning up before continuing.
+ State: ${CONNECTION_STATE[this.connection.readyState]}`);
+ this.destroy({ emit: false });
+ }
+
+ const wsQuery = { v: client.options.ws.version };
+
+ if (zlib) {
+ this.inflate = new zlib.Inflate({
+ chunkSize: 65535,
+ flush: zlib.Z_SYNC_FLUSH,
+ to: WebSocket.encoding === 'json' ? 'string' : '',
+ });
+ wsQuery.compress = 'zlib-stream';
+ }
+
+ this.debug(
+ `[CONNECT]
+ Gateway : ${gateway}
+ Version : ${client.options.ws.version}
+ Encoding : ${WebSocket.encoding}
+ Compression: ${zlib ? 'zlib-stream' : 'none'}`,
+ );
+
+ this.status = this.status === Status.Disconnected ? Status.Reconnecting : Status.Connecting;
+ this.setHelloTimeout();
+
+ this.connectedAt = Date.now();
+
+ const ws = (this.connection = WebSocket.create(gateway, wsQuery));
+ ws.onopen = this.onOpen.bind(this);
+ ws.onmessage = this.onMessage.bind(this);
+ ws.onerror = this.onError.bind(this);
+ ws.onclose = this.onClose.bind(this);
+ });
+ }
+
+ /**
+ * Called whenever a connection is opened to the gateway.
+ * @private
+ */
+ onOpen() {
+ this.debug(`[CONNECTED] Took ${Date.now() - this.connectedAt}ms`);
+ this.status = Status.Nearly;
+ }
+
+ /**
+ * Called whenever a message is received.
+ * @param {MessageEvent} event Event received
+ * @private
+ */
+ onMessage({ data }) {
+ let raw;
+ if (data instanceof ArrayBuffer) data = new Uint8Array(data);
+ if (zlib) {
+ const l = data.length;
+ const flush =
+ l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff;
+
+ this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
+ if (!flush) return;
+ raw = this.inflate.result;
+ } else {
+ raw = data;
+ }
+ let packet;
+ try {
+ packet = WebSocket.unpack(raw);
+ } catch (err) {
+ this.manager.client.emit(Events.ShardError, err, this.id);
+ return;
+ }
+ this.manager.client.emit(Events.Raw, packet, this.id);
+ if (packet.op === GatewayOpcodes.Dispatch) this.manager.emit(packet.t, packet.d, this.id);
+ this.onPacket(packet);
+ }
+
+ /**
+ * Called whenever an error occurs with the WebSocket.
+ * @param {ErrorEvent} event The error that occurred
+ * @private
+ */
+ onError(event) {
+ const error = event?.error ?? event;
+ if (!error) return;
+
+ /**
+ * Emitted whenever a shard's WebSocket encounters a connection error.
+ * @event Client#shardError
+ * @param {Error} error The encountered error
+ * @param {number} shardId The shard that encountered this error
+ */
+ this.manager.client.emit(Events.ShardError, error, this.id);
+ }
+
+ /**
+ * @external CloseEvent
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
+ */
+
+ /**
+ * @external ErrorEvent
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent}
+ */
+
+ /**
+ * @external MessageEvent
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent}
+ */
+
+ /**
+ * Called whenever a connection to the gateway is closed.
+ * @param {CloseEvent} event Close event that was received
+ * @private
+ */
+ onClose(event) {
+ if (this.sequence !== -1) this.closeSequence = this.sequence;
+ this.sequence = -1;
+
+ this.debug(`[CLOSE]
+ Event Code: ${event.code}
+ Clean : ${event.wasClean}
+ Reason : ${event.reason ?? 'No reason received'}`);
+
+ this.setHeartbeatTimer(-1);
+ this.setHelloTimeout(-1);
+ // If we still have a connection object, clean up its listeners
+ if (this.connection) this._cleanupConnection();
+
+ this.status = Status.Disconnected;
+
+ /**
+ * Emitted when a shard's WebSocket closes.
+ * @private
+ * @event WebSocketShard#close
+ * @param {CloseEvent} event The received event
+ */
+ this.emit(ShardEvents.Close, event);
+ }
+
+ /**
+ * Called whenever a packet is received.
+ * @param {Object} packet The received packet
+ * @private
+ */
+ onPacket(packet) {
+ if (!packet) {
+ this.debug(`Received broken packet: '${packet}'.`);
+ return;
+ }
+
+ switch (packet.t) {
+ case GatewayDispatchEvents.Ready:
+ /**
+ * Emitted when the shard receives the READY payload and is now waiting for guilds
+ * @event WebSocketShard#ready
+ */
+ this.emit(ShardEvents.Ready);
+
+ this.sessionId = packet.d.session_id;
+ this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
+ this.status = Status.WaitingForGuilds;
+ this.debug(`[READY] Session ${this.sessionId}.`);
+ this.lastHeartbeatAcked = true;
+ this.sendHeartbeat('ReadyHeartbeat');
+ break;
+ case GatewayDispatchEvents.Resumed: {
+ /**
+ * Emitted when the shard resumes successfully
+ * @event WebSocketShard#resumed
+ */
+ this.emit(ShardEvents.Resumed);
+
+ this.status = Status.Ready;
+ const replayed = packet.s - this.closeSequence;
+ this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`);
+ this.lastHeartbeatAcked = true;
+ this.sendHeartbeat('ResumeHeartbeat');
+ break;
+ }
+ }
+
+ if (packet.s > this.sequence) this.sequence = packet.s;
+
+ switch (packet.op) {
+ case GatewayOpcodes.Hello:
+ this.setHelloTimeout(-1);
+ this.setHeartbeatTimer(packet.d.heartbeat_interval);
+ this.identify();
+ break;
+ case GatewayOpcodes.Reconnect:
+ this.debug('[RECONNECT] Discord asked us to reconnect');
+ this.destroy({ closeCode: 4_000 });
+ break;
+ case GatewayOpcodes.InvalidSession:
+ this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`);
+ // If we can resume the session, do so immediately
+ if (packet.d) {
+ this.identifyResume();
+ return;
+ }
+ // Reset the sequence
+ this.sequence = -1;
+ // Reset the session id as it's invalid
+ this.sessionId = null;
+ // Set the status to reconnecting
+ this.status = Status.Reconnecting;
+ // Finally, emit the INVALID_SESSION event
+ this.emit(ShardEvents.InvalidSession);
+ break;
+ case GatewayOpcodes.HeartbeatAck:
+ this.ackHeartbeat();
+ break;
+ case GatewayOpcodes.Heartbeat:
+ this.sendHeartbeat('HeartbeatRequest', true);
+ break;
+ default:
+ this.manager.handlePacket(packet, this);
+ if (this.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) {
+ this.expectedGuilds.delete(packet.d.id);
+ this.checkReady();
+ }
+ }
+ }
+
+ /**
+ * Checks if the shard can be marked as ready
+ * @private
+ */
+ checkReady() {
+ // Step 0. Clear the ready timeout, if it exists
+ if (this.readyTimeout) {
+ clearTimeout(this.readyTimeout);
+ this.readyTimeout = null;
+ }
+ // Step 1. If we don't have any other guilds pending, we are ready
+ if (!this.expectedGuilds.size) {
+ this.debug('Shard received all its guilds. Marking as fully ready.');
+ this.status = Status.Ready;
+
+ /**
+ * Emitted when the shard is fully ready.
+ * This event is emitted if:
+ * * all guilds were received by this shard
+ * * the ready timeout expired, and some guilds are unavailable
+ * @event WebSocketShard#allReady
+ * @param {?Set} unavailableGuilds Set of unavailable guilds, if any
+ */
+ this.emit(ShardEvents.AllReady);
+ return;
+ }
+ const hasGuildsIntent = new IntentsBitField(this.manager.client.options.intents).has(GatewayIntentBits.Guilds);
+ // Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds
+ // * The timeout is 15 seconds by default
+ // * This can be optionally changed in the client options via the `waitGuildTimeout` option
+ // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds.
+
+ const { waitGuildTimeout } = this.manager.client.options;
+
+ this.readyTimeout = setTimeout(
+ () => {
+ this.debug(
+ `Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` +
+ `${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${
+ this.expectedGuilds.size
+ }`,
+ );
+
+ this.readyTimeout = null;
+
+ this.status = Status.Ready;
+
+ this.emit(ShardEvents.AllReady, this.expectedGuilds);
+ },
+ 0,
+ ).unref();
+ }
+
+ /**
+ * Sets the HELLO packet timeout.
+ * @param {number} [time] If set to -1, it will clear the hello timeout
+ * @private
+ */
+ setHelloTimeout(time) {
+ if (time === -1) {
+ if (this.helloTimeout) {
+ this.debug('Clearing the HELLO timeout.');
+ clearTimeout(this.helloTimeout);
+ this.helloTimeout = null;
+ }
+ return;
+ }
+ this.debug('Setting a HELLO timeout for 20s.');
+ this.helloTimeout = setTimeout(() => {
+ this.debug('Did not receive HELLO in time. Destroying and connecting again.');
+ this.destroy({ reset: true, closeCode: 4009 });
+ }, 20_000).unref();
+ }
+
+ /**
+ * Sets the heartbeat timer for this shard.
+ * @param {number} time If -1, clears the interval, any other number sets an interval
+ * @private
+ */
+ setHeartbeatTimer(time) {
+ if (time === -1) {
+ if (this.heartbeatInterval) {
+ this.debug('Clearing the heartbeat interval.');
+ clearInterval(this.heartbeatInterval);
+ this.heartbeatInterval = null;
+ }
+ return;
+ }
+ this.debug(`Setting a heartbeat interval for ${time}ms.`);
+ // Sanity checks
+ if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
+ this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref();
+ }
+
+ /**
+ * Sends a heartbeat to the WebSocket.
+ * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
+ * @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent
+ * @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully.
+ * @private
+ */
+ sendHeartbeat(
+ tag = 'HeartbeatTimer',
+ ignoreHeartbeatAck = [Status.WaitingForGuilds, Status.Identifying, Status.Resuming].includes(this.status),
+ ) {
+ if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) {
+ this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`);
+ } else if (!this.lastHeartbeatAcked) {
+ this.debug(
+ `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting.
+ Status : ${STATUS_KEYS[this.status]}
+ Sequence : ${this.sequence}
+ Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
+ );
+
+ this.destroy({ closeCode: 4009, reset: true });
+ return;
+ }
+
+ this.debug(`[${tag}] Sending a heartbeat.`);
+ this.lastHeartbeatAcked = false;
+ this.lastPingTimestamp = Date.now();
+ this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence }, true);
+ }
+
+ /**
+ * Acknowledges a heartbeat.
+ * @private
+ */
+ ackHeartbeat() {
+ this.lastHeartbeatAcked = true;
+ const latency = Date.now() - this.lastPingTimestamp;
+ this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`);
+ this.ping = latency;
+ }
+
+ /**
+ * Identifies the client on the connection.
+ * @private
+ * @returns {void}
+ */
+ identify() {
+ return this.sessionId ? this.identifyResume() : this.identifyNew();
+ }
+
+ /**
+ * Identifies as a new connection on the gateway.
+ * @private
+ */
+ identifyNew() {
+ const { client } = this.manager;
+ if (!client.token) {
+ this.debug('[IDENTIFY] No token available to identify a new session.');
+ return;
+ }
+
+ this.status = Status.Identifying;
+
+ // Clone the identify payload and assign the token and shard info
+ const d = {
+ ...client.options.ws,
+ token: client.token,
+ };
+
+ this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: 32767`);
+ this.send({ op: GatewayOpcodes.Identify, d }, true);
+ }
+
+ /**
+ * Resumes a session on the gateway.
+ * @private
+ */
+ identifyResume() {
+ if (!this.sessionId) {
+ this.debug('[RESUME] No session id was present; identifying as a new session.');
+ this.identifyNew();
+ return;
+ }
+
+ this.status = Status.Resuming;
+
+ this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`);
+
+ const d = {
+ token: this.manager.client.token,
+ session_id: this.sessionId,
+ seq: this.closeSequence,
+ };
+
+ this.send({ op: GatewayOpcodes.Resume, d }, true);
+ }
+
+ /**
+ * Adds a packet to the queue to be sent to the gateway.
+ * If you use this method, make sure you understand that you need to provide
+ * a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
+ * Do not use this method if you don't know what you're doing.
+ * @param {Object} data The full packet to send
+ * @param {boolean} [important=false] If this packet should be added first in queue
+ */
+ send(data, important = false) {
+ this.ratelimit.queue[important ? 'unshift' : 'push'](data);
+ this.processQueue();
+ }
+
+ /**
+ * Sends data, bypassing the queue.
+ * @param {Object} data Packet to send
+ * @returns {void}
+ * @private
+ */
+ _send(data) {
+ if (this.connection?.readyState !== WebSocket.OPEN) {
+ this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`);
+ this.destroy({ closeCode: 4_000 });
+ return;
+ }
+
+ this.connection.send(WebSocket.pack(data), err => {
+ if (err) this.manager.client.emit(Events.ShardError, err, this.id);
+ });
+ }
+
+ /**
+ * Processes the current WebSocket queue.
+ * @returns {void}
+ * @private
+ */
+ processQueue() {
+ if (this.ratelimit.remaining === 0) return;
+ if (this.ratelimit.queue.length === 0) return;
+ if (this.ratelimit.remaining === this.ratelimit.total) {
+ this.ratelimit.timer = setTimeout(() => {
+ this.ratelimit.remaining = this.ratelimit.total;
+ this.processQueue();
+ }, this.ratelimit.time).unref();
+ }
+ while (this.ratelimit.remaining > 0) {
+ const item = this.ratelimit.queue.shift();
+ if (!item) return;
+ this._send(item);
+ this.ratelimit.remaining--;
+ }
+ }
+
+ /**
+ * Destroys this shard and closes its WebSocket connection.
+ * @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard
+ * @private
+ */
+ destroy({ closeCode = 1_000, reset = false, emit = true, log = true } = {}) {
+ if (log) {
+ this.debug(`[DESTROY]
+ Close Code : ${closeCode}
+ Reset : ${reset}
+ Emit DESTROYED: ${emit}`);
+ }
+
+ // Step 0: Remove all timers
+ this.setHeartbeatTimer(-1);
+ this.setHelloTimeout(-1);
+
+ // Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED
+ if (this.connection) {
+ // If the connection is currently opened, we will (hopefully) receive close
+ if (this.connection.readyState === WebSocket.OPEN) {
+ this.connection.close(closeCode);
+ } else {
+ // Connection is not OPEN
+ this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
+ // Remove listeners from the connection
+ this._cleanupConnection();
+ // Attempt to close the connection just in case
+ try {
+ this.connection.close(closeCode);
+ } catch {
+ // No-op
+ }
+ // Emit the destroyed event if needed
+ if (emit) this._emitDestroyed();
+ }
+ } else if (emit) {
+ // We requested a destroy, but we had no connection. Emit destroyed
+ this._emitDestroyed();
+ }
+
+ // Step 2: Null the connection object
+ this.connection = null;
+
+ // Step 3: Set the shard status to Disconnected
+ this.status = Status.Disconnected;
+
+ // Step 4: Cache the old sequence (use to attempt a resume)
+ if (this.sequence !== -1) this.closeSequence = this.sequence;
+
+ // Step 5: Reset the sequence and session id if requested
+ if (reset) {
+ this.sequence = -1;
+ this.sessionId = null;
+ }
+
+ // Step 6: reset the rate limit data
+ this.ratelimit.remaining = this.ratelimit.total;
+ this.ratelimit.queue.length = 0;
+ if (this.ratelimit.timer) {
+ clearTimeout(this.ratelimit.timer);
+ this.ratelimit.timer = null;
+ }
+ }
+
+ /**
+ * Cleans up the WebSocket connection listeners.
+ * @private
+ */
+ _cleanupConnection() {
+ this.connection.onopen = this.connection.onclose = this.connection.onerror = this.connection.onmessage = null;
+ }
+
+ /**
+ * Emits the DESTROYED event on the shard
+ * @private
+ */
+ _emitDestroyed() {
+ /**
+ * Emitted when a shard is destroyed, but no WebSocket connection was present.
+ * @private
+ * @event WebSocketShard#destroyed
+ */
+ this.emit(ShardEvents.Destroyed);
+ }
+}
+
+module.exports = WebSocketShard;
diff --git a/src/client/websocket/handlers/CHANNEL_CREATE.js b/src/client/websocket/handlers/CHANNEL_CREATE.js
new file mode 100644
index 00000000..d6d560d
--- /dev/null
+++ b/src/client/websocket/handlers/CHANNEL_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ChannelCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/CHANNEL_DELETE.js b/src/client/websocket/handlers/CHANNEL_DELETE.js
new file mode 100644
index 00000000..cb9f3d8
--- /dev/null
+++ b/src/client/websocket/handlers/CHANNEL_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ChannelDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
new file mode 100644
index 00000000..c46e527
--- /dev/null
+++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+
+module.exports = (client, { d: data }) => {
+ const channel = client.channels.cache.get(data.channel_id);
+ const time = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null;
+
+ if (channel) {
+ // Discord sends null for last_pin_timestamp if the last pinned message was removed
+ channel.lastPinTimestamp = time;
+
+ /**
+ * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event,
+ * not much information can be provided easily here - you need to manually check the pins yourself.
+ * @event Client#channelPinsUpdate
+ * @param {TextBasedChannels} channel The channel that the pins update occurred in
+ * @param {Date} time The time of the pins update
+ */
+ client.emit(Events.ChannelPinsUpdate, channel, time);
+ }
+};
diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js
new file mode 100644
index 00000000..8f35121
--- /dev/null
+++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+
+module.exports = (client, packet) => {
+ const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
+ if (old && updated) {
+ /**
+ * Emitted whenever a channel is updated - e.g. name change, topic change, channel type change.
+ * @event Client#channelUpdate
+ * @param {DMChannel|GuildChannel} oldChannel The channel before the update
+ * @param {DMChannel|GuildChannel} newChannel The channel after the update
+ */
+ client.emit(Events.ChannelUpdate, old, updated);
+ }
+};
diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js
new file mode 100644
index 00000000..d8dc0f9
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildBanAdd.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js
new file mode 100644
index 00000000..8389e46
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildBanRemove.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js
new file mode 100644
index 00000000..7202dc8
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_CREATE.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+const Status = require('../../../util/Status');
+
+module.exports = (client, { d: data }, shard) => {
+ let guild = client.guilds.cache.get(data.id);
+ if (guild) {
+ if (!guild.available && !data.unavailable) {
+ // A newly available guild
+ guild._patch(data);
+ }
+ } else {
+ // A new guild
+ data.shardId = shard.id;
+ guild = client.guilds._add(data);
+ if (client.ws.status === Status.Ready) {
+ /**
+ * Emitted whenever the client joins a guild.
+ * @event Client#guildCreate
+ * @param {Guild} guild The created guild
+ */
+ client.emit(Events.GuildCreate, guild);
+ }
+ }
+};
diff --git a/src/client/websocket/handlers/GUILD_DELETE.js b/src/client/websocket/handlers/GUILD_DELETE.js
new file mode 100644
index 00000000..27a3256
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js
new file mode 100644
index 00000000..e23b671
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildEmojisUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js
new file mode 100644
index 00000000..e90a72c
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildIntegrationsUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js
new file mode 100644
index 00000000..6f7ca7e
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const Events = require('../../../util/Events');
+
+module.exports = (client, { d: data }) => {
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (!guild) return;
+ const members = new Collection();
+
+ for (const member of data.members) members.set(member.user.id, guild.members._add(member));
+ if (data.presences) {
+ for (const presence of data.presences) guild.presences._add(Object.assign(presence, { guild }));
+ }
+
+ /**
+ * Represents the properties of a guild members chunk
+ * @typedef {Object} GuildMembersChunk
+ * @property {number} index Index of the received chunk
+ * @property {number} count Number of chunks the client should receive
+ * @property {?string} nonce Nonce for this chunk
+ */
+
+ /**
+ * Emitted whenever a chunk of guild members is received (all members come from the same guild).
+ * @event Client#guildMembersChunk
+ * @param {Collection} members The members in the chunk
+ * @param {Guild} guild The guild related to the member chunk
+ * @param {GuildMembersChunk} chunk Properties of the received chunk
+ */
+ client.emit(Events.GuildMembersChunk, members, guild, {
+ count: data.chunk_count,
+ index: data.chunk_index,
+ nonce: data.nonce,
+ });
+};
diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js
new file mode 100644
index 00000000..fece5d7
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+const Status = require('../../../util/Status');
+
+module.exports = (client, { d: data }, shard) => {
+ const guild = client.guilds.cache.get(data.guild_id);
+ if (guild) {
+ guild.memberCount++;
+ const member = guild.members._add(data);
+ if (shard.status === Status.Ready) {
+ /**
+ * Emitted whenever a user joins a guild.
+ * @event Client#guildMemberAdd
+ * @param {GuildMember} member The member that has joined a guild
+ */
+ client.emit(Events.GuildMemberAdd, member);
+ }
+ }
+};
diff --git a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js
new file mode 100644
index 00000000..72432af
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet, shard) => {
+ client.actions.GuildMemberRemove.handle(packet.d, shard);
+};
diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js
new file mode 100644
index 00000000..cafc6bd
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet, shard) => {
+ client.actions.GuildMemberUpdate.handle(packet.d, shard);
+};
diff --git a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js
new file mode 100644
index 00000000..da9e7bc
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildRoleCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js
new file mode 100644
index 00000000..cdc6353
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildRoleDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js
new file mode 100644
index 00000000..3a9b62e
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildRoleUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_CREATE.js b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_CREATE.js
new file mode 100644
index 00000000..04ff2df
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildScheduledEventCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_DELETE.js b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_DELETE.js
new file mode 100644
index 00000000..b660c09
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildScheduledEventDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_UPDATE.js b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_UPDATE.js
new file mode 100644
index 00000000..0064708
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildScheduledEventUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_USER_ADD.js b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_USER_ADD.js
new file mode 100644
index 00000000..d5adca2
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_USER_ADD.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildScheduledEventUserAdd.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_USER_REMOVE.js b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_USER_REMOVE.js
new file mode 100644
index 00000000..114df68
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_SCHEDULED_EVENT_USER_REMOVE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildScheduledEventUserRemove.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_STICKERS_UPDATE.js b/src/client/websocket/handlers/GUILD_STICKERS_UPDATE.js
new file mode 100644
index 00000000..e3aba61
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_STICKERS_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildStickersUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/GUILD_UPDATE.js b/src/client/websocket/handlers/GUILD_UPDATE.js
new file mode 100644
index 00000000..fd0012a
--- /dev/null
+++ b/src/client/websocket/handlers/GUILD_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.GuildUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js
new file mode 100644
index 00000000..5bf30fc
--- /dev/null
+++ b/src/client/websocket/handlers/INTERACTION_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.InteractionCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/INVITE_CREATE.js b/src/client/websocket/handlers/INVITE_CREATE.js
new file mode 100644
index 00000000..50a2e72
--- /dev/null
+++ b/src/client/websocket/handlers/INVITE_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.InviteCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/INVITE_DELETE.js b/src/client/websocket/handlers/INVITE_DELETE.js
new file mode 100644
index 00000000..5971852
--- /dev/null
+++ b/src/client/websocket/handlers/INVITE_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.InviteDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_CREATE.js b/src/client/websocket/handlers/MESSAGE_CREATE.js
new file mode 100644
index 00000000..c9b79a8
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_DELETE.js b/src/client/websocket/handlers/MESSAGE_DELETE.js
new file mode 100644
index 00000000..85ae2bc
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js
new file mode 100644
index 00000000..fbcf80f
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageDeleteBulk.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js
new file mode 100644
index 00000000..e219b4a
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageReactionAdd.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js
new file mode 100644
index 00000000..2980e69
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageReactionRemove.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js
new file mode 100644
index 00000000..ead80f7
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageReactionRemoveAll.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js
new file mode 100644
index 00000000..579444c
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.MessageReactionRemoveEmoji.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js
new file mode 100644
index 00000000..c2a470b
--- /dev/null
+++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+
+module.exports = (client, packet) => {
+ const { old, updated } = client.actions.MessageUpdate.handle(packet.d);
+ if (old && updated) {
+ /**
+ * Emitted whenever a message is updated - e.g. embed or content change.
+ * @event Client#messageUpdate
+ * @param {Message} oldMessage The message before the update
+ * @param {Message} newMessage The message after the update
+ */
+ client.emit(Events.MessageUpdate, old, updated);
+ }
+};
diff --git a/src/client/websocket/handlers/PRESENCE_UPDATE.js b/src/client/websocket/handlers/PRESENCE_UPDATE.js
new file mode 100644
index 00000000..bde3629
--- /dev/null
+++ b/src/client/websocket/handlers/PRESENCE_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.PresenceUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js
new file mode 100644
index 00000000..97b99ef
--- /dev/null
+++ b/src/client/websocket/handlers/READY.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const ClientApplication = require('../../../structures/ClientApplication');
+const User = require('../../../structures/User');
+let ClientUser;
+
+module.exports = (client, { d: data }, shard) => {
+ //console.log(data);
+
+ client.session_id = data.session_id;
+ if (client.user) {
+ client.user._patch(data.user);
+ } else {
+ ClientUser ??= require('../../../structures/ClientUser');
+ client.user = new ClientUser(client, data.user);
+ client.users.cache.set(client.user.id, client.user);
+ }
+
+ client.user.setAFK(true);
+
+ for (const guild of data.guilds) {
+ guild.shardId = shard.id;
+ client.guilds._add(guild);
+ }
+
+ for (const r of data.relationships) {
+ if(r.type == 1) {
+ client.friends.cache.set(r.id, new User(client, r.user));
+ } else if(r.type == 2) {
+ client.blocked.cache.set(r.id, new User(client, r.user));
+ }
+ }
+
+ if (client.application) {
+ client.application._patch(data.application);
+ } else {
+ client.application = new ClientApplication(client, data.application);
+ }
+
+ shard.checkReady();
+};
diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js
new file mode 100644
index 00000000..39824bc
--- /dev/null
+++ b/src/client/websocket/handlers/RESUMED.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+
+module.exports = (client, packet, shard) => {
+ const replayed = shard.sequence - shard.closeSequence;
+ /**
+ * Emitted when a shard resumes successfully.
+ * @event Client#shardResume
+ * @param {number} id The shard id that resumed
+ * @param {number} replayedEvents The amount of replayed events
+ */
+ client.emit(Events.ShardResume, shard.id, replayed);
+};
diff --git a/src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js b/src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js
new file mode 100644
index 00000000..77ae2ff
--- /dev/null
+++ b/src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.StageInstanceCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js b/src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js
new file mode 100644
index 00000000..e2bb627
--- /dev/null
+++ b/src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.StageInstanceDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js b/src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js
new file mode 100644
index 00000000..fabc84a
--- /dev/null
+++ b/src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.StageInstanceUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/THREAD_CREATE.js b/src/client/websocket/handlers/THREAD_CREATE.js
new file mode 100644
index 00000000..d92cab0
--- /dev/null
+++ b/src/client/websocket/handlers/THREAD_CREATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ThreadCreate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/THREAD_DELETE.js b/src/client/websocket/handlers/THREAD_DELETE.js
new file mode 100644
index 00000000..1140a08
--- /dev/null
+++ b/src/client/websocket/handlers/THREAD_DELETE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ThreadDelete.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/THREAD_LIST_SYNC.js b/src/client/websocket/handlers/THREAD_LIST_SYNC.js
new file mode 100644
index 00000000..17b173a
--- /dev/null
+++ b/src/client/websocket/handlers/THREAD_LIST_SYNC.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ThreadListSync.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/THREAD_MEMBERS_UPDATE.js b/src/client/websocket/handlers/THREAD_MEMBERS_UPDATE.js
new file mode 100644
index 00000000..f3c7a73
--- /dev/null
+++ b/src/client/websocket/handlers/THREAD_MEMBERS_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ThreadMembersUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/THREAD_MEMBER_UPDATE.js b/src/client/websocket/handlers/THREAD_MEMBER_UPDATE.js
new file mode 100644
index 00000000..a111b0a
--- /dev/null
+++ b/src/client/websocket/handlers/THREAD_MEMBER_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.ThreadMemberUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/THREAD_UPDATE.js b/src/client/websocket/handlers/THREAD_UPDATE.js
new file mode 100644
index 00000000..481dcd4
--- /dev/null
+++ b/src/client/websocket/handlers/THREAD_UPDATE.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const Events = require('../../../util/Events');
+
+module.exports = (client, packet) => {
+ const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
+ if (old && updated) {
+ /**
+ * Emitted whenever a thread is updated - e.g. name change, archive state change, locked state change.
+ * @event Client#threadUpdate
+ * @param {ThreadChannel} oldThread The thread before the update
+ * @param {ThreadChannel} newThread The thread after the update
+ */
+ client.emit(Events.ThreadUpdate, old, updated);
+ }
+};
diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js
new file mode 100644
index 00000000..9a56a54
--- /dev/null
+++ b/src/client/websocket/handlers/TYPING_START.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.TypingStart.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/USER_UPDATE.js b/src/client/websocket/handlers/USER_UPDATE.js
new file mode 100644
index 00000000..a02bf58
--- /dev/null
+++ b/src/client/websocket/handlers/USER_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.UserUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js
new file mode 100644
index 00000000..f9cf534
--- /dev/null
+++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js
@@ -0,0 +1,6 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`);
+ client.voice.onVoiceServer(packet.d);
+};
diff --git a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js
new file mode 100644
index 00000000..dbff6ea
--- /dev/null
+++ b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.VoiceStateUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js
new file mode 100644
index 00000000..46cacee
--- /dev/null
+++ b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (client, packet) => {
+ client.actions.WebhooksUpdate.handle(packet.d);
+};
diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js
new file mode 100644
index 00000000..d7739c1
--- /dev/null
+++ b/src/client/websocket/handlers/index.js
@@ -0,0 +1,58 @@
+'use strict';
+
+const handlers = Object.fromEntries([
+ ['READY', require('./READY')],
+ ['RESUMED', require('./RESUMED')],
+ ['GUILD_CREATE', require('./GUILD_CREATE')],
+ ['GUILD_DELETE', require('./GUILD_DELETE')],
+ ['GUILD_UPDATE', require('./GUILD_UPDATE')],
+ ['INVITE_CREATE', require('./INVITE_CREATE')],
+ ['INVITE_DELETE', require('./INVITE_DELETE')],
+ ['GUILD_MEMBER_ADD', require('./GUILD_MEMBER_ADD')],
+ ['GUILD_MEMBER_REMOVE', require('./GUILD_MEMBER_REMOVE')],
+ ['GUILD_MEMBER_UPDATE', require('./GUILD_MEMBER_UPDATE')],
+ ['GUILD_MEMBERS_CHUNK', require('./GUILD_MEMBERS_CHUNK')],
+ ['GUILD_INTEGRATIONS_UPDATE', require('./GUILD_INTEGRATIONS_UPDATE')],
+ ['GUILD_ROLE_CREATE', require('./GUILD_ROLE_CREATE')],
+ ['GUILD_ROLE_DELETE', require('./GUILD_ROLE_DELETE')],
+ ['GUILD_ROLE_UPDATE', require('./GUILD_ROLE_UPDATE')],
+ ['GUILD_BAN_ADD', require('./GUILD_BAN_ADD')],
+ ['GUILD_BAN_REMOVE', require('./GUILD_BAN_REMOVE')],
+ ['GUILD_EMOJIS_UPDATE', require('./GUILD_EMOJIS_UPDATE')],
+ ['CHANNEL_CREATE', require('./CHANNEL_CREATE')],
+ ['CHANNEL_DELETE', require('./CHANNEL_DELETE')],
+ ['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')],
+ ['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')],
+ ['MESSAGE_CREATE', require('./MESSAGE_CREATE')],
+ ['MESSAGE_DELETE', require('./MESSAGE_DELETE')],
+ ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')],
+ ['MESSAGE_DELETE_BULK', require('./MESSAGE_DELETE_BULK')],
+ ['MESSAGE_REACTION_ADD', require('./MESSAGE_REACTION_ADD')],
+ ['MESSAGE_REACTION_REMOVE', require('./MESSAGE_REACTION_REMOVE')],
+ ['MESSAGE_REACTION_REMOVE_ALL', require('./MESSAGE_REACTION_REMOVE_ALL')],
+ ['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI')],
+ ['THREAD_CREATE', require('./THREAD_CREATE')],
+ ['THREAD_UPDATE', require('./THREAD_UPDATE')],
+ ['THREAD_DELETE', require('./THREAD_DELETE')],
+ ['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],
+ ['THREAD_MEMBER_UPDATE', require('./THREAD_MEMBER_UPDATE')],
+ ['THREAD_MEMBERS_UPDATE', require('./THREAD_MEMBERS_UPDATE')],
+ ['USER_UPDATE', require('./USER_UPDATE')],
+ ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
+ ['TYPING_START', require('./TYPING_START')],
+ ['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')],
+ ['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')],
+ ['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')],
+ ['INTERACTION_CREATE', require('./INTERACTION_CREATE')],
+ ['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
+ ['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
+ ['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
+ ['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE')],
+ ['GUILD_SCHEDULED_EVENT_CREATE', require('./GUILD_SCHEDULED_EVENT_CREATE')],
+ ['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')],
+ ['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')],
+ ['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')],
+ ['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')],
+]);
+
+module.exports = handlers;
diff --git a/src/errors/DJSError.js b/src/errors/DJSError.js
new file mode 100644
index 00000000..af2ff09
--- /dev/null
+++ b/src/errors/DJSError.js
@@ -0,0 +1,61 @@
+'use strict';
+
+// Heavily inspired by node's `internal/errors` module
+
+const kCode = Symbol('code');
+const messages = new Map();
+
+/**
+ * Extend an error of some sort into a DiscordjsError.
+ * @param {Error} Base Base error to extend
+ * @returns {DiscordjsError}
+ */
+function makeDiscordjsError(Base) {
+ return class DiscordjsError extends Base {
+ constructor(key, ...args) {
+ super(message(key, args));
+ this[kCode] = key;
+ if (Error.captureStackTrace) Error.captureStackTrace(this, DiscordjsError);
+ }
+
+ get name() {
+ return `${super.name} [${this[kCode]}]`;
+ }
+
+ get code() {
+ return this[kCode];
+ }
+ };
+}
+
+/**
+ * Format the message for an error.
+ * @param {string} key Error key
+ * @param {Array<*>} args Arguments to pass for util format or as function args
+ * @returns {string} Formatted string
+ */
+function message(key, args) {
+ if (typeof key !== 'string') throw new Error('Error message key must be a string');
+ const msg = messages.get(key);
+ if (!msg) throw new Error(`An invalid error message key was used: ${key}.`);
+ if (typeof msg === 'function') return msg(...args);
+ if (!args?.length) return msg;
+ args.unshift(msg);
+ return String(...args);
+}
+
+/**
+ * Register an error code and message.
+ * @param {string} sym Unique name for the error
+ * @param {*} val Value of the error
+ */
+function register(sym, val) {
+ messages.set(sym, typeof val === 'function' ? val : String(val));
+}
+
+module.exports = {
+ register,
+ Error: makeDiscordjsError(Error),
+ TypeError: makeDiscordjsError(TypeError),
+ RangeError: makeDiscordjsError(RangeError),
+};
diff --git a/src/errors/Messages.js b/src/errors/Messages.js
new file mode 100644
index 00000000..ae526a6
--- /dev/null
+++ b/src/errors/Messages.js
@@ -0,0 +1,197 @@
+'use strict';
+
+const { register } = require('./DJSError');
+
+const Messages = {
+ CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`,
+ CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.',
+ CLIENT_MISSING_INTENTS: 'Valid intents must be provided for the Client.',
+ CLIENT_NOT_READY: (action) =>
+ `The client needs to be logged in to ${action}.`,
+
+ TOKEN_INVALID: 'An invalid token was provided.',
+ TOKEN_MISSING:
+ 'Request to use token, but token was unavailable to the client.',
+
+ WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.',
+ WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',
+ WS_NOT_OPEN: (data = 'data') => `WebSocket not open to send ${data}`,
+ MANAGER_DESTROYED: 'Manager was destroyed.',
+
+ BITFIELD_INVALID: (bit) => `Invalid bitfield flag or number: ${bit}.`,
+
+ SHARDING_INVALID: 'Invalid shard settings were provided.',
+ SHARDING_REQUIRED:
+ 'This session would have handled too many guilds - Sharding is required.',
+ INVALID_INTENTS: 'Invalid intent provided for WebSocket intents.',
+ DISALLOWED_INTENTS:
+ 'Privileged intent provided is not enabled or whitelisted.',
+ SHARDING_NO_SHARDS: 'No shards have been spawned.',
+ SHARDING_IN_PROCESS: 'Shards are still being spawned.',
+ SHARDING_INVALID_EVAL_BROADCAST: 'Script to evaluate must be a function',
+ SHARDING_SHARD_NOT_FOUND: (id) => `Shard ${id} could not be found.`,
+ SHARDING_ALREADY_SPAWNED: (count) => `Already spawned ${count} shards.`,
+ SHARDING_PROCESS_EXISTS: (id) => `Shard ${id} already has an active process.`,
+ SHARDING_WORKER_EXISTS: (id) => `Shard ${id} already has an active worker.`,
+ SHARDING_READY_TIMEOUT: (id) =>
+ `Shard ${id}'s Client took too long to become ready.`,
+ SHARDING_READY_DISCONNECTED: (id) =>
+ `Shard ${id}'s Client disconnected before becoming ready.`,
+ SHARDING_READY_DIED: (id) =>
+ `Shard ${id}'s process exited before its Client became ready.`,
+ SHARDING_NO_CHILD_EXISTS: (id) =>
+ `Shard ${id} has no active process or worker.`,
+ SHARDING_SHARD_MISCALCULATION: (shard, guild, count) =>
+ `Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`,
+
+ COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
+ COLOR_CONVERT: 'Unable to convert color to a number.',
+
+ INVITE_OPTIONS_MISSING_CHANNEL:
+ 'A valid guild channel must be provided when GuildScheduledEvent is EXTERNAL.',
+
+ BUTTON_LABEL: 'MessageButton label must be a string',
+ BUTTON_URL: 'MessageButton URL must be a string',
+ BUTTON_CUSTOM_ID: 'MessageButton customId must be a string',
+
+ SELECT_MENU_CUSTOM_ID: 'MessageSelectMenu customId must be a string',
+ SELECT_MENU_PLACEHOLDER: 'MessageSelectMenu placeholder must be a string',
+ SELECT_OPTION_LABEL: 'MessageSelectOption label must be a string',
+ SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
+ SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',
+
+ INTERACTION_COLLECTOR_ERROR: (reason) =>
+ `Collector received no interactions before ending with reason: ${reason}`,
+
+ FILE_NOT_FOUND: (file) => `File could not be found: ${file}`,
+
+ USER_BANNER_NOT_FETCHED:
+ "You must fetch this user's banner before trying to generate its URL!",
+ USER_NO_DM_CHANNEL: 'No DM Channel exists!',
+
+ VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.',
+
+ VOICE_STATE_NOT_OWN:
+ 'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.',
+ VOICE_STATE_INVALID_TYPE: (name) => `${name} must be a boolean.`,
+
+ REQ_RESOURCE_TYPE:
+ 'The resource must be a string, Buffer or a valid file stream.',
+
+ IMAGE_FORMAT: (format) => `Invalid image format: ${format}`,
+ IMAGE_SIZE: (size) => `Invalid image size: ${size}`,
+
+ MESSAGE_BULK_DELETE_TYPE:
+ 'The messages must be an Array, Collection, or number.',
+ MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.',
+ MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.',
+
+ SPLIT_MAX_LEN:
+ 'Chunk exceeds the max length and contains no split characters.',
+
+ BAN_RESOLVE_ID: (ban = false) =>
+ `Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`,
+ FETCH_BAN_RESOLVE_ID: "Couldn't resolve the user id to fetch the ban.",
+
+ PRUNE_DAYS_TYPE: 'Days must be a number',
+
+ GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.',
+ GUILD_VOICE_CHANNEL_RESOLVE:
+ 'Could not resolve channel to a guild voice channel.',
+ GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.',
+ GUILD_CHANNEL_UNOWNED:
+ "The fetched channel does not belong to this manager's guild.",
+ GUILD_OWNED: 'Guild is owned by the client.',
+ GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.",
+ GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.',
+ CHANNEL_NOT_CACHED:
+ 'Could not find the channel where this message came from in the cache!',
+ STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.',
+ GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.',
+
+ INVALID_TYPE: (name, expected, an = false) =>
+ `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`,
+ INVALID_ELEMENT: (type, name, elem) =>
+ `Supplied ${type} ${name} includes an invalid element: ${elem}`,
+
+ MESSAGE_THREAD_PARENT:
+ 'The message was not sent in a guild text or news channel',
+ MESSAGE_EXISTING_THREAD: 'The message already has a thread',
+ THREAD_INVITABLE_TYPE: (type) => `Invitable cannot be edited on ${type}`,
+
+ WEBHOOK_MESSAGE: 'The message was not sent by a webhook.',
+ WEBHOOK_TOKEN_UNAVAILABLE:
+ 'This action requires a webhook token, but none is available.',
+ WEBHOOK_URL_INVALID: 'The provided webhook URL is not valid.',
+ WEBHOOK_APPLICATION:
+ 'This message webhook belongs to an application and cannot be fetched.',
+ MESSAGE_REFERENCE_MISSING: 'The message does not reference another message',
+
+ EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji',
+ EMOJI_MANAGED: 'Emoji is managed and has no Author.',
+ MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION: (guild) =>
+ `Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`,
+ NOT_GUILD_STICKER:
+ 'Sticker is a standard (non-guild) sticker and has no author.',
+
+ REACTION_RESOLVE_USER:
+ "Couldn't resolve the user id to remove from the reaction.",
+
+ VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.',
+
+ INVITE_RESOLVE_CODE: 'Could not resolve the code to fetch the invite.',
+
+ INVITE_NOT_FOUND: 'Could not find the requested invite.',
+
+ DELETE_GROUP_DM_CHANNEL:
+ "Bots don't have access to Group DM Channels and cannot delete them",
+ FETCH_GROUP_DM_CHANNEL:
+ "Bots don't have access to Group DM Channels and cannot fetch them",
+
+ MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.',
+
+ GLOBAL_COMMAND_PERMISSIONS:
+ 'Permissions for global commands may only be fetched or modified by providing a GuildResolvable ' +
+ "or from a guild's application command manager.",
+ GUILD_UNCACHED_ROLE_RESOLVE:
+ 'Cannot resolve roles from an arbitrary guild, provide an id instead',
+
+ INTERACTION_ALREADY_REPLIED:
+ 'The reply to this interaction has already been sent or deferred.',
+ INTERACTION_NOT_REPLIED:
+ 'The reply to this interaction has not been sent or deferred.',
+ INTERACTION_EPHEMERAL_REPLIED: 'Ephemeral responses cannot be deleted.',
+
+ COMMAND_INTERACTION_OPTION_NOT_FOUND: (name) =>
+ `Required option "${name}" not found.`,
+ COMMAND_INTERACTION_OPTION_TYPE: (name, type, expected) =>
+ `Option "${name}" is of type: ${type}; expected ${expected}.`,
+ COMMAND_INTERACTION_OPTION_EMPTY: (name, type) =>
+ `Required option "${name}" is of type: ${type}; expected a non-empty value.`,
+ COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND:
+ 'No subcommand specified for interaction.',
+ COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP:
+ 'No subcommand group specified for interaction.',
+ AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION:
+ 'No focused option for autocomplete interaction.',
+
+ INVITE_MISSING_SCOPES:
+ 'At least one valid scope must be provided for the invite',
+
+ NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,
+
+ SWEEP_FILTER_RETURN:
+ 'The return value of the sweepFilter function was not false or a Function',
+
+ INVALID_BOT_METHOD: `Bot accounts cannot use this method`,
+ INVALID_USER_METHOD: `User accounts cannot use this method`,
+ INVALID_LOCALE: 'Unable to select this location',
+};
+
+Messages.AuthenticationFailed = Messages.TOKEN_INVALID;
+Messages.InvalidShard = Messages.SHARDING_INVALID;
+Messages.ShardingRequired = Messages.SHARDING_REQUIRED;
+Messages.InvalidIntents = Messages.INVALID_INTENTS;
+Messages.DisallowedIntents = Messages.DISALLOWED_INTENTS;
+
+for (const [name, message] of Object.entries(Messages)) register(name, message);
diff --git a/src/errors/index.js b/src/errors/index.js
new file mode 100644
index 00000000..c94ddc7
--- /dev/null
+++ b/src/errors/index.js
@@ -0,0 +1,4 @@
+'use strict';
+
+module.exports = require('./DJSError');
+module.exports.Messages = require('./Messages');
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 00000000..2539b92
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,204 @@
+'use strict';
+
+// "Root" classes (starting points)
+exports.BaseClient = require('./client/BaseClient');
+exports.Client = require('./client/Client');
+exports.Shard = require('./sharding/Shard');
+exports.ShardClientUtil = require('./sharding/ShardClientUtil');
+exports.ShardingManager = require('./sharding/ShardingManager');
+exports.WebhookClient = require('./client/WebhookClient');
+
+// Utilities
+exports.ActivityFlagsBitField = require('./util/ActivityFlagsBitField');
+exports.ApplicationFlagsBitField = require('./util/ApplicationFlagsBitField');
+exports.BaseManager = require('./managers/BaseManager');
+exports.BitField = require('./util/BitField');
+exports.Collection = require('@discordjs/collection').Collection;
+exports.Constants = require('./util/Constants');
+exports.Colors = require('./util/Colors');
+exports.DataResolver = require('./util/DataResolver');
+exports.EnumResolvers = require('./util/EnumResolvers');
+exports.Events = require('./util/Events');
+exports.Formatters = require('./util/Formatters');
+exports.IntentsBitField = require('./util/IntentsBitField');
+exports.LimitedCollection = require('./util/LimitedCollection');
+exports.MessageFlagsBitField = require('./util/MessageFlagsBitField');
+exports.Options = require('./util/Options');
+exports.Partials = require('./util/Partials');
+exports.PermissionsBitField = require('./util/PermissionsBitField');
+exports.ShardEvents = require('./util/ShardEvents');
+exports.Status = require('./util/Status');
+exports.SnowflakeUtil = require('@sapphire/snowflake').DiscordSnowflake;
+exports.Sweepers = require('./util/Sweepers');
+exports.SystemChannelFlagsBitField = require('./util/SystemChannelFlagsBitField');
+exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField');
+exports.UserFlagsBitField = require('./util/UserFlagsBitField');
+exports.Util = require('./util/Util');
+exports.version = require('../package.json').version;
+
+// Managers
+exports.ApplicationCommandManager = require('./managers/ApplicationCommandManager');
+exports.ApplicationCommandPermissionsManager = require('./managers/ApplicationCommandPermissionsManager');
+exports.BaseGuildEmojiManager = require('./managers/BaseGuildEmojiManager');
+exports.CachedManager = require('./managers/CachedManager');
+exports.ChannelManager = require('./managers/ChannelManager');
+exports.ClientVoiceManager = require('./client/voice/ClientVoiceManager');
+exports.DataManager = require('./managers/DataManager');
+exports.GuildApplicationCommandManager = require('./managers/GuildApplicationCommandManager');
+exports.GuildBanManager = require('./managers/GuildBanManager');
+exports.GuildChannelManager = require('./managers/GuildChannelManager');
+exports.GuildEmojiManager = require('./managers/GuildEmojiManager');
+exports.GuildEmojiRoleManager = require('./managers/GuildEmojiRoleManager');
+exports.GuildInviteManager = require('./managers/GuildInviteManager');
+exports.GuildManager = require('./managers/GuildManager');
+exports.GuildMemberManager = require('./managers/GuildMemberManager');
+exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager');
+exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager');
+exports.GuildStickerManager = require('./managers/GuildStickerManager');
+exports.MessageManager = require('./managers/MessageManager');
+exports.PermissionOverwriteManager = require('./managers/PermissionOverwriteManager');
+exports.PresenceManager = require('./managers/PresenceManager');
+exports.ReactionManager = require('./managers/ReactionManager');
+exports.ReactionUserManager = require('./managers/ReactionUserManager');
+exports.RoleManager = require('./managers/RoleManager');
+exports.StageInstanceManager = require('./managers/StageInstanceManager');
+exports.ThreadManager = require('./managers/ThreadManager');
+exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
+exports.UserManager = require('./managers/UserManager');
+exports.VoiceStateManager = require('./managers/VoiceStateManager');
+exports.WebSocketManager = require('./client/websocket/WebSocketManager');
+exports.WebSocketShard = require('./client/websocket/WebSocketShard');
+
+// Structures
+exports.ActionRow = require('./structures/ActionRow');
+exports.Activity = require('./structures/Presence').Activity;
+exports.AnonymousGuild = require('./structures/AnonymousGuild');
+exports.Application = require('./structures/interfaces/Application');
+exports.ApplicationCommand = require('./structures/ApplicationCommand');
+exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction');
+exports.Base = require('./structures/Base');
+exports.BaseGuild = require('./structures/BaseGuild');
+exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji');
+exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel');
+exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel');
+exports.ButtonComponent = require('./structures/ButtonComponent');
+exports.ButtonInteraction = require('./structures/ButtonInteraction');
+exports.CategoryChannel = require('./structures/CategoryChannel');
+exports.Channel = require('./structures/Channel').Channel;
+exports.ChatInputCommandInteraction = require('./structures/ChatInputCommandInteraction');
+exports.ClientApplication = require('./structures/ClientApplication');
+exports.ClientPresence = require('./structures/ClientPresence');
+exports.ClientUser = require('./structures/ClientUser');
+exports.CommandInteraction = require('./structures/CommandInteraction');
+exports.Collector = require('./structures/interfaces/Collector');
+exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver');
+exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction');
+exports.DMChannel = require('./structures/DMChannel');
+exports.Embed = require('./structures/Embed');
+exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed;
+exports.Emoji = require('./structures/Emoji').Emoji;
+exports.Guild = require('./structures/Guild').Guild;
+exports.GuildAuditLogs = require('./structures/GuildAuditLogs');
+exports.GuildAuditLogsEntry = require('./structures/GuildAuditLogs').Entry;
+exports.GuildBan = require('./structures/GuildBan');
+exports.GuildChannel = require('./structures/GuildChannel');
+exports.GuildEmoji = require('./structures/GuildEmoji');
+exports.GuildMember = require('./structures/GuildMember').GuildMember;
+exports.GuildPreview = require('./structures/GuildPreview');
+exports.GuildPreviewEmoji = require('./structures/GuildPreviewEmoji');
+exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildScheduledEvent;
+exports.GuildTemplate = require('./structures/GuildTemplate');
+exports.Integration = require('./structures/Integration');
+exports.IntegrationApplication = require('./structures/IntegrationApplication');
+exports.Interaction = require('./structures/Interaction');
+exports.InteractionCollector = require('./structures/InteractionCollector');
+exports.InteractionWebhook = require('./structures/InteractionWebhook');
+exports.Invite = require('./structures/Invite');
+exports.InviteStageInstance = require('./structures/InviteStageInstance');
+exports.InviteGuild = require('./structures/InviteGuild');
+exports.Message = require('./structures/Message').Message;
+exports.MessageAttachment = require('./structures/MessageAttachment');
+exports.MessageCollector = require('./structures/MessageCollector');
+exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction');
+exports.MessageContextMenuCommandInteraction = require('./structures/MessageContextMenuCommandInteraction');
+exports.MessageMentions = require('./structures/MessageMentions');
+exports.MessagePayload = require('./structures/MessagePayload');
+exports.MessageReaction = require('./structures/MessageReaction');
+exports.NewsChannel = require('./structures/NewsChannel');
+exports.OAuth2Guild = require('./structures/OAuth2Guild');
+exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
+exports.PermissionOverwrites = require('./structures/PermissionOverwrites');
+exports.Presence = require('./structures/Presence').Presence;
+exports.ReactionCollector = require('./structures/ReactionCollector');
+exports.ReactionEmoji = require('./structures/ReactionEmoji');
+exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
+exports.Role = require('./structures/Role').Role;
+exports.SelectMenuComponent = require('./structures/SelectMenuComponent');
+exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction');
+exports.StageChannel = require('./structures/StageChannel');
+exports.StageInstance = require('./structures/StageInstance').StageInstance;
+exports.Sticker = require('./structures/Sticker').Sticker;
+exports.StickerPack = require('./structures/StickerPack');
+exports.StoreChannel = require('./structures/StoreChannel');
+exports.Team = require('./structures/Team');
+exports.TeamMember = require('./structures/TeamMember');
+exports.TextChannel = require('./structures/TextChannel');
+exports.ThreadChannel = require('./structures/ThreadChannel');
+exports.ThreadMember = require('./structures/ThreadMember');
+exports.Typing = require('./structures/Typing');
+exports.User = require('./structures/User');
+exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction');
+exports.VoiceChannel = require('./structures/VoiceChannel');
+exports.VoiceRegion = require('./structures/VoiceRegion');
+exports.VoiceState = require('./structures/VoiceState');
+exports.Webhook = require('./structures/Webhook');
+exports.Widget = require('./structures/Widget');
+exports.WidgetMember = require('./structures/WidgetMember');
+exports.WelcomeChannel = require('./structures/WelcomeChannel');
+exports.WelcomeScreen = require('./structures/WelcomeScreen');
+
+exports.WebSocket = require('./WebSocket');
+
+// External
+exports.ActivityType = require('discord-api-types/v9').ActivityType;
+exports.ApplicationCommandType = require('discord-api-types/v9').ApplicationCommandType;
+exports.ApplicationCommandOptionType = require('discord-api-types/v9').ApplicationCommandOptionType;
+exports.ApplicationCommandPermissionType = require('discord-api-types/v9').ApplicationCommandPermissionType;
+exports.AuditLogEvent = require('discord-api-types/v9').AuditLogEvent;
+exports.ButtonStyle = require('discord-api-types/v9').ButtonStyle;
+exports.ChannelType = require('discord-api-types/v9').ChannelType;
+exports.ComponentType = require('discord-api-types/v9').ComponentType;
+exports.GatewayCloseCodes = require('discord-api-types/v9').GatewayCloseCodes;
+exports.GatewayDispatchEvents = require('discord-api-types/v9').GatewayDispatchEvents;
+exports.GatewayIntentBits = require('discord-api-types/v9').GatewayIntentBits;
+exports.GatewayOpcodes = require('discord-api-types/v9').GatewayOpcodes;
+exports.GuildFeature = require('discord-api-types/v9').GuildFeature;
+exports.GuildMFALevel = require('discord-api-types/v9').GuildMFALevel;
+exports.GuildNSFWLevel = require('discord-api-types/v9').GuildNSFWLevel;
+exports.GuildPremiumTier = require('discord-api-types/v9').GuildPremiumTier;
+exports.GuildScheduledEventEntityType = require('discord-api-types/v9').GuildScheduledEventEntityType;
+exports.GuildScheduledEventPrivacyLevel = require('discord-api-types/v9').GuildScheduledEventPrivacyLevel;
+exports.GuildScheduledEventStatus = require('discord-api-types/v9').GuildScheduledEventStatus;
+exports.GuildSystemChannelFlags = require('discord-api-types/v9').GuildSystemChannelFlags;
+exports.GuildVerificationLevel = require('discord-api-types/v9').GuildVerificationLevel;
+exports.InteractionType = require('discord-api-types/v9').InteractionType;
+exports.InteractionResponseType = require('discord-api-types/v9').InteractionResponseType;
+exports.InviteTargetType = require('discord-api-types/v9').InviteTargetType;
+exports.Locale = require('discord-api-types/v9').Locale;
+exports.MessageType = require('discord-api-types/v9').MessageType;
+exports.MessageFlags = require('discord-api-types/v9').MessageFlags;
+exports.OAuth2Scopes = require('discord-api-types/v9').OAuth2Scopes;
+exports.PermissionFlagsBits = require('discord-api-types/v9').PermissionFlagsBits;
+exports.RESTJSONErrorCodes = require('discord-api-types/v9').RESTJSONErrorCodes;
+exports.StageInstancePrivacyLevel = require('discord-api-types/v9').StageInstancePrivacyLevel;
+exports.StickerType = require('discord-api-types/v9').StickerType;
+exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType;
+exports.UserFlags = require('discord-api-types/v9').UserFlags;
+exports.WebhookType = require('discord-api-types/v9').WebhookType;
+exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent;
+exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeSelectMenuComponent;
+exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption;
+exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption;
+exports.DiscordAPIError = require('./rest/DiscordAPIError');
+exports.HTTPError = require('./rest/HTTPError');
+exports.RateLimitError = require('./rest/RateLimitError');
diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js
new file mode 100644
index 00000000..b04ecb2
--- /dev/null
+++ b/src/managers/ApplicationCommandManager.js
@@ -0,0 +1,217 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const ApplicationCommand = require('../structures/ApplicationCommand');
+
+/**
+ * Manages API methods for application commands and stores their cache.
+ * @extends {CachedManager}
+ */
+class ApplicationCommandManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, ApplicationCommand, iterable);
+
+ /**
+ * The manager for permissions of arbitrary commands on arbitrary guilds
+ * @type {ApplicationCommandPermissionsManager}
+ */
+ this.permissions = new ApplicationCommandPermissionsManager(this);
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name ApplicationCommandManager#cache
+ */
+
+ _add(data, cache, guildId) {
+ return super._add(data, cache, { extras: [this.guild, guildId] });
+ }
+
+ /**
+ * The APIRouter path to the commands
+ * @param {Snowflake} [options.id] The application command's id
+ * @param {Snowflake} [options.guildId] The guild's id to use in the path,
+ * ignored when using a {@link GuildApplicationCommandManager}
+ * @returns {string}
+ * @private
+ */
+ commandPath({ id, guildId } = {}) {
+ let path = this.client.api.applications(this.client.application.id);
+ if(this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId);
+ return id ? path.commands(id) : path.commands;
+ }
+
+ /**
+ * Data that resolves to give an ApplicationCommand object. This can be:
+ * * An ApplicationCommand object
+ * * A Snowflake
+ * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable
+ */
+
+ /**
+ * Options used to fetch data from Discord
+ * @typedef {Object} BaseFetchOptions
+ * @property {boolean} [cache=true] Whether to cache the fetched data if it wasn't already
+ * @property {boolean} [force=false] Whether to skip the cache check and request the API
+ */
+
+ /**
+ * Options used to fetch Application Commands from Discord
+ * @typedef {BaseFetchOptions} FetchApplicationCommandOptions
+ * @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached
+ */
+
+ /**
+ * Obtains one or multiple application commands from Discord, or the cache if it's already available.
+ * @param {Snowflake} [id] The application command's id
+ * @param {FetchApplicationCommandOptions} [options] Additional options for this fetch
+ * @returns {Promise>}
+ * @example
+ * // Fetch a single command
+ * client.application.commands.fetch('123456789012345678')
+ * .then(command => console.log(`Fetched command ${command.name}`))
+ * .catch(console.error);
+ * @example
+ * // Fetch all commands
+ * guild.commands.fetch()
+ * .then(commands => console.log(`Fetched ${commands.size} commands`))
+ * .catch(console.error);
+ */
+ async fetch(id, { guildId, cache = true, force = false } = {}) {
+ if (typeof id === 'object') {
+ ({ guildId, cache = true } = id);
+ } else if (id) {
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+ const command = await this.commandPath({ id, guildId }).get();
+ return this._add(command, cache);
+ }
+
+ const data = await this.commandPath({ guildId }).get();
+ return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection());
+ }
+
+ /**
+ * Creates an application command.
+ * @param {ApplicationCommandData|APIApplicationCommand} command The command
+ * @param {Snowflake} [guildId] The guild's id to create this command in,
+ * ignored when using a {@link GuildApplicationCommandManager}
+ * @returns {Promise}
+ * @example
+ * // Create a new command
+ * client.application.commands.create({
+ * name: 'test',
+ * description: 'A test command',
+ * })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async create(command, guildId) {
+ const data = await this.commandPath({ guildId }).post({
+ data: this.constructor.transformCommand(command),
+ });
+ return this._add(data, true, guildId);
+ }
+
+ /**
+ * Sets all the commands for this application or guild.
+ * @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands
+ * @param {Snowflake} [guildId] The guild's id to create the commands in,
+ * ignored when using a {@link GuildApplicationCommandManager}
+ * @returns {Promise>}
+ * @example
+ * // Set all commands to just this one
+ * client.application.commands.set([
+ * {
+ * name: 'test',
+ * description: 'A test command',
+ * },
+ * ])
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Remove all commands
+ * guild.commands.set([])
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async set(commands, guildId) {
+ const data = await this.commandPath({ guildId }).put({
+ data: commands.map(c => this.constructor.transformCommand(c)),
+ });
+ return data.reduce((coll, command) => coll.set(command.id, this._add(command, true, guildId)), new Collection());
+ }
+
+ /**
+ * Edits an application command.
+ * @param {ApplicationCommandResolvable} command The command to edit
+ * @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with
+ * @param {Snowflake} [guildId] The guild's id where the command registered,
+ * ignored when using a {@link GuildApplicationCommandManager}
+ * @returns {Promise}
+ * @example
+ * // Edit an existing command
+ * client.application.commands.edit('123456789012345678', {
+ * description: 'New description',
+ * })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async edit(command, data, guildId) {
+ const id = this.resolveId(command);
+ if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
+
+ const patched = await this.commandPath({ id, guildId }).patch({
+ body: this.constructor.transformCommand(data),
+ });
+ return this._add(patched, true, guildId);
+ }
+
+ /**
+ * Deletes an application command.
+ * @param {ApplicationCommandResolvable} command The command to delete
+ * @param {Snowflake} [guildId] The guild's id where the command is registered,
+ * ignored when using a {@link GuildApplicationCommandManager}
+ * @returns {Promise}
+ * @example
+ * // Delete a command
+ * guild.commands.delete('123456789012345678')
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async delete(command, guildId) {
+ const id = this.resolveId(command);
+ if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
+
+ await this.commandPath({ id, guildId }).delete();
+
+ const cached = this.cache.get(id);
+ this.cache.delete(id);
+ return cached ?? null;
+ }
+
+ /**
+ * Transforms an {@link ApplicationCommandData} object into something that can be used with the API.
+ * @param {ApplicationCommandData|APIApplicationCommand} command The command to transform
+ * @returns {APIApplicationCommand}
+ * @private
+ */
+ static transformCommand(command) {
+ return {
+ name: command.name,
+ description: command.description,
+ type: command.type,
+ options: command.options?.map(o => ApplicationCommand.transformOption(o)),
+ default_permission: command.defaultPermission ?? command.default_permission,
+ };
+ }
+}
+
+module.exports = ApplicationCommandManager;
diff --git a/src/managers/ApplicationCommandPermissionsManager.js b/src/managers/ApplicationCommandPermissionsManager.js
new file mode 100644
index 00000000..b944e1d
--- /dev/null
+++ b/src/managers/ApplicationCommandPermissionsManager.js
@@ -0,0 +1,399 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { RESTJSONErrorCodes, Routes } = require('discord-api-types/v9');
+const BaseManager = require('./BaseManager');
+const { Error, TypeError } = require('../errors');
+
+/**
+ * Manages API methods for permissions of Application Commands.
+ * @extends {BaseManager}
+ */
+class ApplicationCommandPermissionsManager extends BaseManager {
+ constructor(manager) {
+ super(manager.client);
+
+ /**
+ * The manager or command that this manager belongs to
+ * @type {ApplicationCommandManager|ApplicationCommand}
+ * @private
+ */
+ this.manager = manager;
+
+ /**
+ * The guild that this manager acts on
+ * @type {?Guild}
+ */
+ this.guild = manager.guild ?? null;
+
+ /**
+ * The id of the guild that this manager acts on
+ * @type {?Snowflake}
+ */
+ this.guildId = manager.guildId ?? manager.guild?.id ?? null;
+
+ /**
+ * The id of the command this manager acts on
+ * @type {?Snowflake}
+ */
+ this.commandId = manager.id ?? null;
+ }
+
+ /**
+ * The APIRouter path to the commands
+ * @param {Snowflake} guildId The guild's id to use in the path,
+ * @param {Snowflake} [commandId] The application command's id
+ * @returns {string}
+ * @private
+ */
+ permissionsPath(guildId, commandId) {
+ return this.client.api.applications(this.client.application.id).guilds(guildId).commands(commandId).permissions;
+ }
+
+ /**
+ * Data for setting the permissions of an application command.
+ * @typedef {Object} ApplicationCommandPermissionData
+ * @property {Snowflake} id The role or user's id
+ * @property {ApplicationCommandPermissionType|number} type Whether this permission is for a role or a user
+ * @property {boolean} permission Whether the role or user has the permission to use this command
+ */
+
+ /**
+ * The object returned when fetching permissions for an application command.
+ * @typedef {Object} ApplicationCommandPermissions
+ * @property {Snowflake} id The role or user's id
+ * @property {ApplicationCommandPermissionType} type Whether this permission is for a role or a user
+ * @property {boolean} permission Whether the role or user has the permission to use this command
+ */
+
+ /**
+ * Options for managing permissions for one or more Application Commands
+ * When passing these options to a manager where `guildId` is `null`,
+ * `guild` is a required parameter
+ * @typedef {Object} BaseApplicationCommandPermissionsOptions
+ * @property {GuildResolvable} [guild] The guild to modify / check permissions for
+ * Ignored when the manager has a non-null `guildId` property
+ * @property {ApplicationCommandResolvable} [command] The command to modify / check permissions for
+ * Ignored when the manager has a non-null `commandId` property
+ */
+
+ /**
+ * Fetches the permissions for one or multiple commands.
+ * @param {BaseApplicationCommandPermissionsOptions} [options] Options used to fetch permissions
+ * @returns {Promise>}
+ * @example
+ * // Fetch permissions for one command
+ * guild.commands.permissions.fetch({ command: '123456789012345678' })
+ * .then(perms => console.log(`Fetched permissions for ${perms.length} users`))
+ * .catch(console.error);
+ * @example
+ * // Fetch permissions for all commands in a guild
+ * client.application.commands.permissions.fetch({ guild: '123456789012345678' })
+ * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`))
+ * .catch(console.error);
+ */
+ async fetch({ guild, command } = {}) {
+ const { guildId, commandId } = this._validateOptions(guild, command);
+ if (commandId) {
+ const data = await this.permissionsPath(guildId, commandId).get();
+ return data.permissions.map(perm => this.constructor.transformPermissions(perm, true));
+ }
+
+ const data = await this.permissionsPath(guildId).get();
+ return data.reduce(
+ (coll, perm) =>
+ coll.set(
+ perm.id,
+ perm.permissions.map(p => this.constructor.transformPermissions(p, true)),
+ ),
+ new Collection(),
+ );
+ }
+
+ /**
+ * Data used for overwriting the permissions for all application commands in a guild.
+ * @typedef {Object} GuildApplicationCommandPermissionData
+ * @property {Snowflake} id The command's id
+ * @property {ApplicationCommandPermissionData[]} permissions The permissions for this command
+ */
+
+ /**
+ * Options used to set permissions for one or more Application Commands in a guild
+ * One of `command` AND `permissions`, OR `fullPermissions` is required.
+ * `fullPermissions` is not a valid option when passing to a manager where `commandId` is non-null
+ * @typedef {BaseApplicationCommandPermissionsOptions} SetApplicationCommandPermissionsOptions
+ * @property {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command
+ * @property {GuildApplicationCommandPermissionData[]} [fullPermissions] The new permissions for all commands
+ * in a guild When this parameter is set, `permissions` and `command` are ignored
+ */
+
+ /**
+ * Sets the permissions for one or more commands.
+ * @param {SetApplicationCommandPermissionsOptions} options Options used to set permissions
+ * @returns {Promise>}
+ * @example
+ * // Set the permissions for one command
+ * client.application.commands.permissions.set({ guild: '892455839386304532', command: '123456789012345678',
+ * permissions: [
+ * {
+ * id: '876543210987654321',
+ * type: ApplicationCommandOptionType.User,
+ * permission: false,
+ * },
+ * ]})
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Set the permissions for all commands
+ * guild.commands.permissions.set({ fullPermissions: [
+ * {
+ * id: '123456789012345678',
+ * permissions: [{
+ * id: '876543210987654321',
+ * type: ApplicationCommandOptionType.User,
+ * permission: false,
+ * }],
+ * },
+ * ]})
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async set({ guild, command, permissions, fullPermissions } = {}) {
+ const { guildId, commandId } = this._validateOptions(guild, command);
+
+ if (commandId) {
+ if (!Array.isArray(permissions)) {
+ throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true);
+ }
+ const data = await this.permissionsPath(guildId, commandId).put({
+ data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) },
+ });
+ return data.permissions.map(perm => this.constructor.transformPermissions(perm, true));
+ }
+
+ if (!Array.isArray(fullPermissions)) {
+ throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true);
+ }
+
+ const APIPermissions = [];
+ for (const perm of fullPermissions) {
+ if (!Array.isArray(perm.permissions)) throw new TypeError('INVALID_ELEMENT', 'Array', 'fullPermissions', perm);
+ APIPermissions.push({
+ id: perm.id,
+ permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)),
+ });
+ }
+ const data = await this.permissionsPath(guildId).put({
+ data: APIPermissions,
+ });
+ return data.reduce(
+ (coll, perm) =>
+ coll.set(
+ perm.id,
+ perm.permissions.map(p => this.constructor.transformPermissions(p, true)),
+ ),
+ new Collection(),
+ );
+ }
+
+ /**
+ * Options used to add permissions to a command
+ * The `command` parameter is not optional when the managers `commandId` is `null`
+ * @typedef {BaseApplicationCommandPermissionsOptions} AddApplicationCommandPermissionsOptions
+ * @property {ApplicationCommandPermissionData[]} permissions The permissions to add to the command
+ */
+
+ /**
+ * Add permissions to a command.
+ * @param {AddApplicationCommandPermissionsOptions} options Options used to add permissions
+ * @returns {Promise}
+ * @example
+ * // Block a role from the command permissions
+ * guild.commands.permissions.add({ command: '123456789012345678', permissions: [
+ * {
+ * id: '876543211234567890',
+ * type: ApplicationCommandPermissionType.Role,
+ * permission: false
+ * },
+ * ]})
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async add({ guild, command, permissions }) {
+ const { guildId, commandId } = this._validateOptions(guild, command);
+ if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
+ if (!Array.isArray(permissions)) {
+ throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true);
+ }
+
+ let existing = [];
+ try {
+ existing = await this.fetch({ guild: guildId, command: commandId });
+ } catch (error) {
+ if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
+ }
+
+ const newPermissions = permissions.slice();
+ for (const perm of existing) {
+ if (!newPermissions.some(x => x.id === perm.id)) {
+ newPermissions.push(perm);
+ }
+ }
+
+ return this.set({ guild: guildId, command: commandId, permissions: newPermissions });
+ }
+
+ /**
+ * Options used to remove permissions from a command
+ * The `command` parameter is not optional when the managers `commandId` is `null`
+ * @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions
+ * @property {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions
+ * One of `users` or `roles` is required
+ * @property {RoleResolvable|RoleResolvable[]} [roles] The role(s) to remove from the command permissions
+ * One of `users` or `roles` is required
+ */
+
+ /**
+ * Remove permissions from a command.
+ * @param {RemoveApplicationCommandPermissionsOptions} options Options used to remove permissions
+ * @returns {Promise}
+ * @example
+ * // Remove a user permission from this command
+ * guild.commands.permissions.remove({ command: '123456789012345678', users: '876543210123456789' })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Remove multiple roles from this command
+ * guild.commands.permissions.remove({
+ * command: '123456789012345678', roles: ['876543210123456789', '765432101234567890']
+ * })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async remove({ guild, command, users, roles }) {
+ const { guildId, commandId } = this._validateOptions(guild, command);
+ if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
+
+ if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true);
+
+ let resolvedIds = [];
+ if (Array.isArray(users)) {
+ users.forEach(user => {
+ const userId = this.client.users.resolveId(user);
+ if (!userId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user);
+ resolvedIds.push(userId);
+ });
+ } else if (users) {
+ const userId = this.client.users.resolveId(users);
+ if (!userId) {
+ throw new TypeError('INVALID_TYPE', 'users', 'Array or UserResolvable');
+ }
+ resolvedIds.push(userId);
+ }
+
+ if (Array.isArray(roles)) {
+ roles.forEach(role => {
+ if (typeof role === 'string') {
+ resolvedIds.push(role);
+ return;
+ }
+ if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
+ const roleId = this.guild.roles.resolveId(role);
+ if (!roleId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role);
+ resolvedIds.push(roleId);
+ });
+ } else if (roles) {
+ if (typeof roles === 'string') {
+ resolvedIds.push(roles);
+ } else {
+ if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
+ const roleId = this.guild.roles.resolveId(roles);
+ if (!roleId) {
+ throw new TypeError('INVALID_TYPE', 'users', 'Array or RoleResolvable');
+ }
+ resolvedIds.push(roleId);
+ }
+ }
+
+ let existing = [];
+ try {
+ existing = await this.fetch({ guild: guildId, command: commandId });
+ } catch (error) {
+ if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
+ }
+
+ const permissions = existing.filter(perm => !resolvedIds.includes(perm.id));
+
+ return this.set({ guild: guildId, command: commandId, permissions });
+ }
+
+ /**
+ * Options used to check the existence of permissions on a command
+ * The `command` parameter is not optional when the managers `commandId` is `null`
+ * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions
+ * @property {UserResolvable|RoleResolvable} permissionId The user or role to check if a permission exists for
+ * on this command.
+ */
+
+ /**
+ * Check whether a permission exists for a user or role
+ * @param {AddApplicationCommandPermissionsOptions} options Options used to check permissions
+ * @returns {Promise}
+ * @example
+ * // Check whether a user has permission to use a command
+ * guild.commands.permissions.has({ command: '123456789012345678', permissionId: '876543210123456789' })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async has({ guild, command, permissionId }) {
+ const { guildId, commandId } = this._validateOptions(guild, command);
+ if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
+
+ if (!permissionId) throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable');
+ let resolvedId = permissionId;
+ if (typeof permissionId !== 'string') {
+ resolvedId = this.client.users.resolveId(permissionId);
+ if (!resolvedId) {
+ if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
+ resolvedId = this.guild.roles.resolveId(permissionId);
+ }
+ if (!resolvedId) {
+ throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable');
+ }
+ }
+
+ let existing = [];
+ try {
+ existing = await this.fetch({ guild: guildId, command: commandId });
+ } catch (error) {
+ if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
+ }
+
+ return existing.some(perm => perm.id === resolvedId);
+ }
+
+ _validateOptions(guild, command) {
+ const guildId = this.guildId ?? this.client.guilds.resolveId(guild);
+ if (!guildId) throw new Error('GLOBAL_COMMAND_PERMISSIONS');
+ let commandId = this.commandId;
+ if (command && !commandId) {
+ commandId = this.manager.resolveId?.(command);
+ if (!commandId && this.guild) {
+ commandId = this.guild.commands.resolveId(command);
+ }
+ commandId ??= this.client.application?.commands.resolveId(command);
+ if (!commandId) {
+ throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable', true);
+ }
+ }
+ return { guildId, commandId };
+ }
+}
+
+module.exports = ApplicationCommandPermissionsManager;
+
+/* eslint-disable max-len */
+/**
+ * @external APIApplicationCommandPermissions
+ * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure}
+ */
diff --git a/src/managers/BaseGuildEmojiManager.js b/src/managers/BaseGuildEmojiManager.js
new file mode 100644
index 00000000..a9bbbff
--- /dev/null
+++ b/src/managers/BaseGuildEmojiManager.js
@@ -0,0 +1,80 @@
+'use strict';
+
+const CachedManager = require('./CachedManager');
+const GuildEmoji = require('../structures/GuildEmoji');
+const ReactionEmoji = require('../structures/ReactionEmoji');
+const { parseEmoji } = require('../util/Util');
+
+/**
+ * Holds methods to resolve GuildEmojis and stores their cache.
+ * @extends {CachedManager}
+ */
+class BaseGuildEmojiManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, GuildEmoji, iterable);
+ }
+
+ /**
+ * The cache of GuildEmojis
+ * @type {Collection}
+ * @name BaseGuildEmojiManager#cache
+ */
+
+ /**
+ * Data that can be resolved into a GuildEmoji object. This can be:
+ * * A Snowflake
+ * * A GuildEmoji object
+ * * A ReactionEmoji object
+ * @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
+ */
+
+ /**
+ * Resolves an EmojiResolvable to an Emoji object.
+ * @param {EmojiResolvable} emoji The Emoji resolvable to identify
+ * @returns {?GuildEmoji}
+ */
+ resolve(emoji) {
+ if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
+ return super.resolve(emoji);
+ }
+
+ /**
+ * Resolves an EmojiResolvable to an Emoji id string.
+ * @param {EmojiResolvable} emoji The Emoji resolvable to identify
+ * @returns {?Snowflake}
+ */
+ resolveId(emoji) {
+ if (emoji instanceof ReactionEmoji) return emoji.id;
+ return super.resolveId(emoji);
+ }
+
+ /**
+ * Data that can be resolved to give an emoji identifier. This can be:
+ * * The unicode representation of an emoji
+ * * The ``, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
+ * * An EmojiResolvable
+ * @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
+ */
+
+ /**
+ * Resolves an EmojiResolvable to an emoji identifier.
+ * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
+ * @returns {?string}
+ */
+ resolveIdentifier(emoji) {
+ const emojiResolvable = this.resolve(emoji);
+ if (emojiResolvable) return emojiResolvable.identifier;
+ if (emoji instanceof ReactionEmoji) return emoji.identifier;
+ if (typeof emoji === 'string') {
+ const res = parseEmoji(emoji);
+ if (res?.name.length) {
+ emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`;
+ }
+ if (!emoji.includes('%')) return encodeURIComponent(emoji);
+ return emoji;
+ }
+ return null;
+ }
+}
+
+module.exports = BaseGuildEmojiManager;
diff --git a/src/managers/BaseManager.js b/src/managers/BaseManager.js
new file mode 100644
index 00000000..0651401
--- /dev/null
+++ b/src/managers/BaseManager.js
@@ -0,0 +1,19 @@
+'use strict';
+
+/**
+ * Manages the API methods of a data model.
+ * @abstract
+ */
+class BaseManager {
+ constructor(client) {
+ /**
+ * The client that instantiated this Manager
+ * @name BaseManager#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+ }
+}
+
+module.exports = BaseManager;
diff --git a/src/managers/BlockedManager.js b/src/managers/BlockedManager.js
new file mode 100644
index 00000000..fac1b7a
--- /dev/null
+++ b/src/managers/BlockedManager.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const CachedManager = require('./CachedManager');
+const GuildMember = require('../structures/GuildMember');
+const Message = require('../structures/Message');
+const ThreadMember = require('../structures/ThreadMember');
+const User = require('../structures/User');
+
+/**
+ * Manages API methods for users and stores their cache.
+ * @extends {CachedManager}
+ */
+class BlockedManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, User, iterable);
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name BlockedManager#cache
+ */
+
+ /**
+ * Data that resolves to give a User object. This can be:
+ * * A User object
+ * * A Snowflake
+ * * A Message object (resolves to the message author)
+ * * A GuildMember object
+ * * A ThreadMember object
+ * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
+ */
+
+ /**
+ * Resolves a {@link UserResolvable} to a {@link User} object.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?User}
+ */
+ resolve(user) {
+ if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
+ if (user instanceof Message) return user.author;
+ return super.resolve(user);
+ }
+
+ /**
+ * Resolves a {@link UserResolvable} to a {@link User} id.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?Snowflake}
+ */
+ resolveId(user) {
+ if (user instanceof ThreadMember) return user.id;
+ if (user instanceof GuildMember) return user.user.id;
+ if (user instanceof Message) return user.author.id;
+ return super.resolveId(user);
+ }
+
+ /**
+ * Obtains a user from Discord, or the user cache if it's already available.
+ * @param {UserResolvable} user The user to fetch
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ */
+ async fetch(user, { cache = true, force = false } = {}) {
+ const id = this.resolveId(user);
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.users(id).get();
+ return this._add(data, cache);
+ }
+}
+
+module.exports = BlockedManager;
diff --git a/src/managers/CachedManager.js b/src/managers/CachedManager.js
new file mode 100644
index 00000000..1058285
--- /dev/null
+++ b/src/managers/CachedManager.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const DataManager = require('./DataManager');
+
+/**
+ * Manages the API methods of a data model with a mutable cache of instances.
+ * @extends {DataManager}
+ * @abstract
+ */
+class CachedManager extends DataManager {
+ constructor(client, holds, iterable) {
+ super(client, holds);
+
+ Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) });
+
+ if (iterable) {
+ for (const item of iterable) {
+ this._add(item);
+ }
+ }
+ }
+
+ /**
+ * The cache of items for this manager.
+ * @type {Collection}
+ * @abstract
+ */
+ get cache() {
+ return this._cache;
+ }
+
+ _add(data, cache = true, { id, extras = [] } = {}) {
+ const existing = this.cache.get(id ?? data.id);
+ if (existing) {
+ if (cache) {
+ existing._patch(data);
+ return existing;
+ }
+ const clone = existing._clone();
+ clone._patch(data);
+ return clone;
+ }
+
+ const entry = this.holds ? new this.holds(this.client, data, ...extras) : data;
+ if (cache) this.cache.set(id ?? entry.id, entry);
+ return entry;
+ }
+}
+
+module.exports = CachedManager;
diff --git a/src/managers/CategoryChannelChildManager.js b/src/managers/CategoryChannelChildManager.js
new file mode 100644
index 00000000..0280251
--- /dev/null
+++ b/src/managers/CategoryChannelChildManager.js
@@ -0,0 +1,69 @@
+'use strict';
+
+const DataManager = require('./DataManager');
+const GuildChannel = require('../structures/GuildChannel');
+
+/**
+ * Manages API methods for CategoryChannels' children.
+ * @extends {DataManager}
+ */
+class CategoryChannelChildManager extends DataManager {
+ constructor(channel) {
+ super(channel.client, GuildChannel);
+ /**
+ * The category channel this manager belongs to
+ * @type {CategoryChannel}
+ */
+ this.channel = channel;
+ }
+
+ /**
+ * The channels that are a part of this category
+ * @type {Collection}
+ * @readonly
+ */
+ get cache() {
+ return this.guild.channels.cache.filter(c => c.parentId === this.channel.id);
+ }
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ * @readonly
+ */
+ get guild() {
+ return this.channel.guild;
+ }
+
+ /**
+ * Options for creating a channel using {@link CategoryChannel#createChannel}.
+ * @typedef {Object} CategoryCreateChannelOptions
+ * @property {ChannelType} [type=ChannelType.GuildText] The type of the new channel.
+ * @property {string} [topic] The topic for the new channel
+ * @property {boolean} [nsfw] Whether the new channel is NSFW
+ * @property {number} [bitrate] Bitrate of the new channel in bits (only voice)
+ * @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice)
+ * @property {OverwriteResolvable[]|Collection} [permissionOverwrites]
+ * Permission overwrites of the new channel
+ * @property {number} [position] Position of the new channel
+ * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds
+ * @property {string} [rtcRegion] The specific region of the new channel.
+ * @property {string} [reason] Reason for creating the new channel
+ */
+
+ /**
+ * Creates a new channel within this category.
+ * You cannot create a channel of type {@link ChannelType.GuildCategory} inside a CategoryChannel.
+ * @param {string} name The name of the new channel
+ * @param {CategoryCreateChannelOptions} options Options for creating the new channel
+ * @returns {Promise}
+ */
+ create(name, options) {
+ return this.guild.channels.create(name, {
+ ...options,
+ parent: this.channel.id,
+ });
+ }
+}
+
+module.exports = CategoryChannelChildManager;
diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js
new file mode 100644
index 00000000..9142702
--- /dev/null
+++ b/src/managers/ChannelManager.js
@@ -0,0 +1,122 @@
+'use strict';
+
+const process = require('node:process');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { Channel } = require('../structures/Channel');
+const { ThreadChannelTypes } = require('../util/Constants');
+const Events = require('../util/Events');
+
+let cacheWarningEmitted = false;
+
+/**
+ * A manager of channels belonging to a client
+ * @extends {CachedManager}
+ */
+class ChannelManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, Channel, iterable);
+ const defaultCaching =
+ this._cache.constructor.name === 'Collection' ||
+ this._cache.maxSize === undefined ||
+ this._cache.maxSize === Infinity;
+ if (!cacheWarningEmitted && !defaultCaching) {
+ cacheWarningEmitted = true;
+ process.emitWarning(
+ `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
+ 'UnsupportedCacheOverwriteWarning',
+ );
+ }
+ }
+
+ /**
+ * The cache of Channels
+ * @type {Collection}
+ * @name ChannelManager#cache
+ */
+
+ _add(data, guild, { cache = true, allowUnknownGuild = false, fromInteraction = false } = {}) {
+ const existing = this.cache.get(data.id);
+ if (existing) {
+ if (cache) existing._patch(data, fromInteraction);
+ guild?.channels?._add(existing);
+ if (ThreadChannelTypes.includes(existing.type)) {
+ existing.parent?.threads?._add(existing);
+ }
+ return existing;
+ }
+
+ const channel = Channel.create(this.client, data, guild, { allowUnknownGuild, fromInteraction });
+
+ if (!channel) {
+ this.client.emit(Events.Debug, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`);
+ return null;
+ }
+
+ if (cache && !allowUnknownGuild) this.cache.set(channel.id, channel);
+
+ return channel;
+ }
+
+ _remove(id) {
+ const channel = this.cache.get(id);
+ channel?.guild?.channels.cache.delete(id);
+ channel?.parent?.threads?.cache.delete(id);
+ this.cache.delete(id);
+ }
+
+ /**
+ * Data that can be resolved to give a Channel object. This can be:
+ * * A Channel object
+ * * A Snowflake
+ * @typedef {Channel|Snowflake} ChannelResolvable
+ */
+
+ /**
+ * Resolves a ChannelResolvable to a Channel object.
+ * @method resolve
+ * @memberof ChannelManager
+ * @instance
+ * @param {ChannelResolvable} channel The channel resolvable to resolve
+ * @returns {?Channel}
+ */
+
+ /**
+ * Resolves a ChannelResolvable to a channel id string.
+ * @method resolveId
+ * @memberof ChannelManager
+ * @instance
+ * @param {ChannelResolvable} channel The channel resolvable to resolve
+ * @returns {?Snowflake}
+ */
+
+ /**
+ * Options for fetching a channel from Discord
+ * @typedef {BaseFetchOptions} FetchChannelOptions
+ * @property {boolean} [allowUnknownGuild=false] Allows the channel to be returned even if the guild is not in cache,
+ * it will not be cached. Many of the properties and methods on the returned channel will throw errors
+ */
+
+ /**
+ * Obtains a channel from Discord, or the channel cache if it's already available.
+ * @param {Snowflake} id The channel's id
+ * @param {FetchChannelOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ * @example
+ * // Fetch a channel by its id
+ * client.channels.fetch('222109930545610754')
+ * .then(channel => console.log(channel.name))
+ * .catch(console.error);
+ */
+ async fetch(id, { allowUnknownGuild = false, cache = true, force = false } = {}) {
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.channels(id).get();
+ return this._add(data, null, { cache, allowUnknownGuild });
+ }
+}
+
+module.exports = ChannelManager;
diff --git a/src/managers/ClientUserSettingManager.js b/src/managers/ClientUserSettingManager.js
new file mode 100644
index 00000000..5ef0214
--- /dev/null
+++ b/src/managers/ClientUserSettingManager.js
@@ -0,0 +1,228 @@
+'use strict';
+
+const CachedManager = require('./CachedManager');
+const { default: Collection } = require('@discordjs/collection');
+const { Error } = require('../errors/DJSError');
+/**
+ * Manages API methods for users and stores their cache.
+ * @extends {CachedManager}
+ */
+const localeObject = {
+ DANISH: 'da',
+ GERMAN: 'de',
+ ENGLISH_UK: 'en-GB',
+ ENGLISH_US: 'en-US',
+ SPANISH: 'es-ES',
+ FRENCH: 'fr',
+ CROATIAN: 'hr',
+ ITALIAN: 'it',
+ LITHUANIAN: 'lt',
+ HUNGARIAN: 'hu',
+ DUTCH: 'nl',
+ NORWEGIAN: 'no',
+ POLISH: 'pl',
+ BRAZILIAN_PORTUGUESE: 'pt-BR',
+ ROMANIA_ROMANIAN: 'ro',
+ FINNISH: 'fi',
+ SWEDISH: 'sv-SE',
+ VIETNAMESE: 'vi',
+ TURKISH: 'tr',
+ CZECH: 'cs',
+ GREEK: 'el',
+ BULGARIAN: 'bg',
+ RUSSIAN: 'ru',
+ UKRAINIAN: 'uk',
+ HINDI: 'hi',
+ THAI: 'th',
+ CHINA_CHINESE: 'zh-CN',
+ JAPANESE: 'ja',
+ TAIWAN_CHINESE: 'zh-TW',
+ KOREAN: 'ko',
+};
+class ClientUserSettingManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client);
+ // Raw data
+ this.rawSetting = {};
+ // Language
+ this.locale = null;
+ // Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message
+ this.showCurrentGame = null;
+ // Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused.
+ this.autoplayGIF = null;
+ // Setting => APP SETTINGS => Appearance => Message Display => Compact Mode [OK]
+ this.compactMode = null;
+ // Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons
+ this.convertEmoticons = null;
+ // Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback
+ this.allowTTS = null;
+ // Setting => APP SETTINGS => Appearance => Theme [OK]
+ this.theme = '';
+ // Setting => APP SETTINGS => Accessibility => Play Animated Emojis
+ this.animatedEmojis = null;
+ // Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions
+ this.showEmojiReactions = null;
+ // Custom Stauts [It's not working now]
+ this.customStatus = null;
+ // Guild folder and position
+ this.guildMetadata = new Collection();
+ }
+ _patch(data) {
+ this.rawSetting = data;
+ if ('locale' in data) {
+ this.locale = data.locale;
+ }
+ if ('show_current_game' in data) {
+ this.showCurrentGame = data.show_current_game;
+ }
+ if ('gif_auto_play' in data) {
+ this.autoplayGIF = data.gif_auto_play;
+ }
+ if ('message_display_compact' in data) {
+ this.compactMode = data.message_display_compact;
+ }
+ if ('convert_emoticons' in data) {
+ this.convertEmoticons = data.convert_emoticons;
+ }
+ if ('enable_tts_command' in data) {
+ this.allowTTS = data.enable_tts_command;
+ }
+ if ('theme' in data) {
+ this.theme = data.theme;
+ }
+ if ('animate_emoji' in data) {
+ this.animatedEmojis = data.animate_emoji;
+ }
+ if ('render_reactions' in data) {
+ this.showEmojiReactions = data.render_reactions;
+ }
+ if ('custom_status' in data) {
+ this.customStatus = data.custom_status;
+ this.customStatus.status = data.status;
+ }
+ if ('guild_folders' in data) {
+ const data_ = data.guild_positions.map((guildId, i) => {
+ // Find folder
+ const folderIndex = data.guild_folders.findIndex((obj) =>
+ obj.guild_ids.includes(guildId),
+ );
+ const metadata = {
+ guildId: guildId,
+ guildIndex: i,
+ folderId: data.guild_folders[folderIndex]?.id,
+ folderIndex,
+ folderName: data.guild_folders[folderIndex]?.name,
+ folderColor: data.guild_folders[folderIndex]?.color,
+ folderGuilds: data.guild_folders[folderIndex]?.guild_ids,
+ };
+ return [guildId, metadata];
+ });
+ this.guildMetadata = new Collection(data_);
+ }
+ }
+ async fetch() {
+ if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
+ try {
+ const data = await this.client.api.users('@me').settings.get();
+ this._patch(data);
+ return this;
+ } catch (e) {
+ throw e;
+ }
+ }
+ /**
+ * Edit data
+ * @param {Object} data Data to edit
+ * @private
+ */
+ async edit(data) {
+ if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
+ try {
+ const res = await this.client.api.users('@me').settings.patch({ data });
+ this._patch(res);
+ return this;
+ } catch (e) {
+ throw e;
+ }
+ }
+ /**
+ * Set compact mode
+ * @param {Boolean | null} value Compact mode enable or disable
+ * @returns {Boolean}
+ */
+ async setDisplayCompactMode(value) {
+ if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
+ if (!value) value = !this.compactMode;
+ if (value !== this.compactMode) {
+ await this.edit({ message_display_compact: value });
+ }
+ return this.compactMode;
+ }
+ /**
+ * Discord Theme
+ * @param {null |dark |light} value Theme to set
+ * @returns {theme}
+ */
+ async setTheme(value) {
+ if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
+ const validValues = ['dark', 'light'];
+ if (!validValues.includes(value)) {
+ value == validValues[0]
+ ? (value = validValues[1])
+ : (value = validValues[0]);
+ }
+ if (value !== this.theme) {
+ await this.edit({ theme: value });
+ }
+ return this.theme;
+ }
+ /**
+ * Locale Setting, must be one of:
+ * * `DANISH`
+ * * `GERMAN`
+ * * `ENGLISH_UK`
+ * * `ENGLISH_US`
+ * * `SPANISH`
+ * * `FRENCH`
+ * * `CROATIAN`
+ * * `ITALIAN`
+ * * `LITHUANIAN`
+ * * `HUNGARIAN`
+ * * `DUTCH`
+ * * `NORWEGIAN`
+ * * `POLISH`
+ * * `BRAZILIAN_PORTUGUESE`
+ * * `ROMANIA_ROMANIAN`
+ * * `FINNISH`
+ * * `SWEDISH`
+ * * `VIETNAMESE`
+ * * `TURKISH`
+ * * `CZECH`
+ * * `GREEK`
+ * * `BULGARIAN`
+ * * `RUSSIAN`
+ * * `UKRAINIAN`
+ * * `HINDI`
+ * * `THAI`
+ * * `CHINA_CHINESE`
+ * * `JAPANESE`
+ * * `TAIWAN_CHINESE`
+ * * `KOREAN`
+ * @typedef {string} LocaleStrings
+ */
+ /**
+ *
+ * @param {LocaleStrings} value
+ * @returns {locale}
+ */
+ async setLocale(value) {
+ if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
+ if (!localeObject[value]) throw new Error('INVALID_LOCALE');
+ if (localeObject[value] !== this.locale) {
+ await this.edit({ locale: localeObject[value] });
+ }
+ return this.locale;
+ }
+}
+
+module.exports = ClientUserSettingManager;
diff --git a/src/managers/DataManager.js b/src/managers/DataManager.js
new file mode 100644
index 00000000..6a5c14c
--- /dev/null
+++ b/src/managers/DataManager.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const BaseManager = require('./BaseManager');
+const { Error } = require('../errors');
+
+/**
+ * Manages the API methods of a data model along with a collection of instances.
+ * @extends {BaseManager}
+ * @abstract
+ */
+class DataManager extends BaseManager {
+ constructor(client, holds) {
+ super(client);
+
+ /**
+ * The data structure belonging to this manager.
+ * @name DataManager#holds
+ * @type {Function}
+ * @private
+ * @readonly
+ */
+ Object.defineProperty(this, 'holds', { value: holds });
+ }
+
+ /**
+ * The cache of items for this manager.
+ * @type {Collection}
+ * @abstract
+ */
+ get cache() {
+ throw new Error('NOT_IMPLEMENTED', 'get cache', this.constructor.name);
+ }
+
+ /**
+ * Resolves a data entry to a data Object.
+ * @param {string|Object} idOrInstance The id or instance of something in this Manager
+ * @returns {?Object} An instance from this Manager
+ */
+ resolve(idOrInstance) {
+ if (idOrInstance instanceof this.holds) return idOrInstance;
+ if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) ?? null;
+ return null;
+ }
+
+ /**
+ * Resolves a data entry to an instance id.
+ * @param {string|Object} idOrInstance The id or instance of something in this Manager
+ * @returns {?Snowflake}
+ */
+ resolveId(idOrInstance) {
+ if (idOrInstance instanceof this.holds) return idOrInstance.id;
+ if (typeof idOrInstance === 'string') return idOrInstance;
+ return null;
+ }
+
+ valueOf() {
+ return this.cache;
+ }
+}
+
+module.exports = DataManager;
diff --git a/src/managers/FriendsManager.js b/src/managers/FriendsManager.js
new file mode 100644
index 00000000..1d45dcb
--- /dev/null
+++ b/src/managers/FriendsManager.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const CachedManager = require('./CachedManager');
+const GuildMember = require('../structures/GuildMember');
+const Message = require('../structures/Message');
+const ThreadMember = require('../structures/ThreadMember');
+const User = require('../structures/User');
+
+/**
+ * Manages API methods for users and stores their cache.
+ * @extends {CachedManager}
+ */
+class FriendsManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, User, iterable);
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name FriendsManager#cache
+ */
+
+ /**
+ * Data that resolves to give a User object. This can be:
+ * * A User object
+ * * A Snowflake
+ * * A Message object (resolves to the message author)
+ * * A GuildMember object
+ * * A ThreadMember object
+ * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
+ */
+
+ /**
+ * Resolves a {@link UserResolvable} to a {@link User} object.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?User}
+ */
+ resolve(user) {
+ if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
+ if (user instanceof Message) return user.author;
+ return super.resolve(user);
+ }
+
+ /**
+ * Resolves a {@link UserResolvable} to a {@link User} id.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?Snowflake}
+ */
+ resolveId(user) {
+ if (user instanceof ThreadMember) return user.id;
+ if (user instanceof GuildMember) return user.user.id;
+ if (user instanceof Message) return user.author.id;
+ return super.resolveId(user);
+ }
+
+ /**
+ * Obtains a user from Discord, or the user cache if it's already available.
+ * @param {UserResolvable} user The user to fetch
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ */
+ async fetch(user, { cache = true, force = false } = {}) {
+ const id = this.resolveId(user);
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.users(id).get();
+ return this._add(data, cache);
+ }
+}
+
+module.exports = FriendsManager;
diff --git a/src/managers/GuildApplicationCommandManager.js b/src/managers/GuildApplicationCommandManager.js
new file mode 100644
index 00000000..97fea5e
--- /dev/null
+++ b/src/managers/GuildApplicationCommandManager.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const ApplicationCommandManager = require('./ApplicationCommandManager');
+const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
+
+/**
+ * An extension for guild-specific application commands.
+ * @extends {ApplicationCommandManager}
+ */
+class GuildApplicationCommandManager extends ApplicationCommandManager {
+ constructor(guild, iterable) {
+ super(guild.client, iterable);
+
+ /**
+ * The guild that this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+
+ /**
+ * The manager for permissions of arbitrary commands on this guild
+ * @type {ApplicationCommandPermissionsManager}
+ */
+ this.permissions = new ApplicationCommandPermissionsManager(this);
+ }
+}
+
+module.exports = GuildApplicationCommandManager;
diff --git a/src/managers/GuildBanManager.js b/src/managers/GuildBanManager.js
new file mode 100644
index 00000000..e882a56
--- /dev/null
+++ b/src/managers/GuildBanManager.js
@@ -0,0 +1,175 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError, Error } = require('../errors');
+const GuildBan = require('../structures/GuildBan');
+const { GuildMember } = require('../structures/GuildMember');
+
+/**
+ * Manages API methods for GuildBans and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildBanManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, GuildBan, iterable);
+
+ /**
+ * The guild this Manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name GuildBanManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache, { id: data.user.id, extras: [this.guild] });
+ }
+
+ /**
+ * Data that resolves to give a GuildBan object. This can be:
+ * * A GuildBan object
+ * * A User resolvable
+ * @typedef {GuildBan|UserResolvable} GuildBanResolvable
+ */
+
+ /**
+ * Resolves a GuildBanResolvable to a GuildBan object.
+ * @param {GuildBanResolvable} ban The ban that is in the guild
+ * @returns {?GuildBan}
+ */
+ resolve(ban) {
+ return super.resolve(ban) ?? super.resolve(this.client.users.resolveId(ban));
+ }
+
+ /**
+ * Options used to fetch a single ban from a guild.
+ * @typedef {BaseFetchOptions} FetchBanOptions
+ * @property {UserResolvable} user The ban to fetch
+ */
+
+ /**
+ * Options used to fetch all bans from a guild.
+ * @typedef {Object} FetchBansOptions
+ * @property {boolean} cache Whether or not to cache the fetched bans
+ */
+
+ /**
+ * Fetches ban(s) from Discord.
+ * @param {UserResolvable|FetchBanOptions|FetchBansOptions} [options] Options for fetching guild ban(s)
+ * @returns {Promise>}
+ * @example
+ * // Fetch all bans from a guild
+ * guild.bans.fetch()
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch all bans from a guild without caching
+ * guild.bans.fetch({ cache: false })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch a single ban
+ * guild.bans.fetch('351871113346809860')
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch a single ban without checking cache
+ * guild.bans.fetch({ user, force: true })
+ * .then(console.log)
+ * .catch(console.error)
+ * @example
+ * // Fetch a single ban without caching
+ * guild.bans.fetch({ user, cache: false })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ fetch(options) {
+ if (!options) return this._fetchMany();
+ const user = this.client.users.resolveId(options);
+ if (user) return this._fetchSingle({ user, cache: true });
+ options.user &&= this.client.users.resolveId(options.user);
+ if (!options.user) {
+ if ('cache' in options) return this._fetchMany(options.cache);
+ return Promise.reject(new Error('FETCH_BAN_RESOLVE_ID'));
+ }
+ return this._fetchSingle(options);
+ }
+
+ async _fetchSingle({ user, cache, force = false }) {
+ if (!force) {
+ const existing = this.cache.get(user);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).bans(user).get();
+ return this._add(data, cache);
+ }
+
+ async _fetchMany(cache) {
+ const data = await this.client.api.guilds(this.guild.id).bans.get();
+ return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, cache)), new Collection());
+ }
+
+ /**
+ * Options used to ban a user from a guild.
+ * @typedef {Object} BanOptions
+ * @property {number} [deleteMessageDays] Number of days of messages to delete, must be between 0 and 7, inclusive
+ * @property {string} [reason] The reason for the ban
+ */
+
+ /**
+ * Bans a user from the guild.
+ * @param {UserResolvable} user The user to ban
+ * @param {BanOptions} [options] Options for the ban
+ * @returns {Promise} Result object will be resolved as specifically as possible.
+ * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
+ * be resolved, the user id will be the result.
+ * @example
+ * // Ban a user by id (or with a user/guild member object)
+ * guild.bans.create('84484653687267328')
+ * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
+ * .catch(console.error);
+ */
+ async create(user, options = {}) {
+ if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ const id = this.client.users.resolveId(user);
+ if (!id) throw new Error('BAN_RESOLVE_ID', true);
+ await this.client.api.guilds(this.guild.id).bans(id).put({
+ body: { delete_message_days: options.deleteMessageDays },
+ reason: options.reason,
+ });
+ if (user instanceof GuildMember) return user;
+ const _user = this.client.users.resolve(id);
+ if (_user) {
+ return this.guild.members.resolve(_user) ?? _user;
+ }
+ return id;
+ }
+
+ /**
+ * Unbans a user from the guild.
+ * @param {UserResolvable} user The user to unban
+ * @param {string} [reason] Reason for unbanning user
+ * @returns {Promise}
+ * @example
+ * // Unban a user by id (or with a user/guild member object)
+ * guild.bans.remove('84484653687267328')
+ * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
+ * .catch(console.error);
+ */
+ async remove(user, reason) {
+ const id = this.client.users.resolveId(user);
+ if (!id) throw new Error('BAN_RESOLVE_ID');
+ await this.client.api.guilds(this.guild.id).bans(id).delete({ reason });
+ return this.client.users.resolve(user);
+ }
+}
+
+module.exports = GuildBanManager;
diff --git a/src/managers/GuildChannelManager.js b/src/managers/GuildChannelManager.js
new file mode 100644
index 00000000..09a4f39
--- /dev/null
+++ b/src/managers/GuildChannelManager.js
@@ -0,0 +1,443 @@
+'use strict';
+
+const process = require('node:process');
+const { Collection } = require('@discordjs/collection');
+const { ChannelType, Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const ThreadManager = require('./ThreadManager');
+const { Error, TypeError } = require('../errors');
+const GuildChannel = require('../structures/GuildChannel');
+const PermissionOverwrites = require('../structures/PermissionOverwrites');
+const ThreadChannel = require('../structures/ThreadChannel');
+const Webhook = require('../structures/Webhook');
+const { ThreadChannelTypes } = require('../util/Constants');
+const DataResolver = require('../util/DataResolver');
+const Util = require('../util/Util');
+
+let cacheWarningEmitted = false;
+let storeChannelDeprecationEmitted = false;
+
+/**
+ * Manages API methods for GuildChannels and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildChannelManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, GuildChannel, iterable);
+ const defaultCaching =
+ this._cache.constructor.name === 'Collection' ||
+ this._cache.maxSize === undefined ||
+ this._cache.maxSize === Infinity;
+ if (!cacheWarningEmitted && !defaultCaching) {
+ cacheWarningEmitted = true;
+ process.emitWarning(
+ `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
+ 'UnsupportedCacheOverwriteWarning',
+ );
+ }
+
+ /**
+ * The guild this Manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The number of channels in this managers cache excluding thread channels
+ * that do not count towards a guild's maximum channels restriction.
+ * @type {number}
+ * @readonly
+ */
+ get channelCountWithoutThreads() {
+ return this.cache.reduce((acc, channel) => {
+ if (ThreadChannelTypes.includes(channel.type)) return acc;
+ return ++acc;
+ }, 0);
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name GuildChannelManager#cache
+ */
+
+ _add(channel) {
+ const existing = this.cache.get(channel.id);
+ if (existing) return existing;
+ this.cache.set(channel.id, channel);
+ return channel;
+ }
+
+ /**
+ * Data that can be resolved to give a Guild Channel object. This can be:
+ * * A GuildChannel object
+ * * A ThreadChannel object
+ * * A Snowflake
+ * @typedef {GuildChannel|ThreadChannel|Snowflake} GuildChannelResolvable
+ */
+
+ /**
+ * Resolves a GuildChannelResolvable to a Channel object.
+ * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
+ * @returns {?(GuildChannel|ThreadChannel)}
+ */
+ resolve(channel) {
+ if (channel instanceof ThreadChannel) return super.resolve(channel.id);
+ return super.resolve(channel);
+ }
+
+ /**
+ * Resolves a GuildChannelResolvable to a channel id.
+ * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
+ * @returns {?Snowflake}
+ */
+ resolveId(channel) {
+ if (channel instanceof ThreadChannel) return super.resolveId(channel.id);
+ return super.resolveId(channel);
+ }
+
+ /**
+ * Options used to create a new channel in a guild.
+ * @typedef {CategoryCreateChannelOptions} GuildChannelCreateOptions
+ * @property {CategoryChannelResolvable} [parent] Parent of the new channel
+ */
+
+ /**
+ * Creates a new channel in the guild.
+ * @param {string} name The name of the new channel
+ * @param {GuildChannelCreateOptions} [options={}] Options for creating the new channel
+ * @returns {Promise}
+ * @example
+ * // Create a new text channel
+ * guild.channels.create('new-general', { reason: 'Needed a cool new channel' })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Create a new channel with permission overwrites
+ * guild.channels.create('new-voice', {
+ * type: ChannelType.GuildVoice,
+ * permissionOverwrites: [
+ * {
+ * id: message.author.id,
+ * deny: [PermissionFlagsBits.ViewChannel],
+ * },
+ * ],
+ * })
+ */
+ async create(
+ name,
+ {
+ type,
+ topic,
+ nsfw,
+ bitrate,
+ userLimit,
+ parent,
+ permissionOverwrites,
+ position,
+ rateLimitPerUser,
+ rtcRegion,
+ reason,
+ } = {},
+ ) {
+ parent &&= this.client.channels.resolveId(parent);
+ permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
+
+ if (type === ChannelType.GuildStore && !storeChannelDeprecationEmitted) {
+ storeChannelDeprecationEmitted = true;
+ process.emitWarning(
+ // eslint-disable-next-line max-len
+ 'Creating store channels is deprecated by Discord and will stop working in March 2022. Check the docs for more info.',
+ 'DeprecationWarning',
+ );
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).channels.post({
+ body: {
+ name,
+ topic,
+ type,
+ nsfw,
+ bitrate,
+ user_limit: userLimit,
+ parent_id: parent,
+ position,
+ permission_overwrites: permissionOverwrites,
+ rate_limit_per_user: rateLimitPerUser,
+ rtc_region: rtcRegion,
+ },
+ reason,
+ });
+ return this.client.actions.ChannelCreate.handle(data).channel;
+ }
+
+ /**
+ * Creates a webhook for the channel.
+ * @param {GuildChannelResolvable} channel The channel to create the webhook for
+ * @param {string} name The name of the webhook
+ * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
+ * @returns {Promise} Returns the created Webhook
+ * @example
+ * // Create a webhook for the current channel
+ * guild.channels.createWebhook('222197033908436994', 'Snek', {
+ * avatar: 'https://i.imgur.com/mI8XcpG.jpg',
+ * reason: 'Needed a cool new Webhook'
+ * })
+ * .then(console.log)
+ * .catch(console.error)
+ */
+ async createWebhook(channel, name, { avatar, reason } = {}) {
+ const id = this.resolveId(channel);
+ if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
+ if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
+ avatar = await DataResolver.resolveImage(avatar);
+ }
+ const data = await this.client.api.channels(id).webhooks.post({
+ body: {
+ name,
+ avatar,
+ },
+ reason,
+ });
+ return new Webhook(this.client, data);
+ }
+
+ /**
+ * The data for a guild channel.
+ * @typedef {Object} ChannelData
+ * @property {string} [name] The name of the channel
+ * @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
+ * @property {number} [position] The position of the channel
+ * @property {string} [topic] The topic of the text channel
+ * @property {boolean} [nsfw] Whether the channel is NSFW
+ * @property {number} [bitrate] The bitrate of the voice channel
+ * @property {number} [userLimit] The user limit of the voice channel
+ * @property {?CategoryChannelResolvable} [parent] The parent of the channel
+ * @property {boolean} [lockPermissions]
+ * Lock the permissions of the channel to what the parent's permissions are
+ * @property {OverwriteResolvable[]|Collection} [permissionOverwrites]
+ * Permission overwrites for the channel
+ * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
+ * @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
+ * The default auto archive duration for all new threads in this channel
+ * @property {?string} [rtcRegion] The RTC region of the channel
+ */
+
+ /**
+ * Edits the channel.
+ * @param {GuildChannelResolvable} channel The channel to edit
+ * @param {ChannelData} data The new data for the channel
+ * @param {string} [reason] Reason for editing this channel
+ * @returns {Promise}
+ * @example
+ * // Edit a channel
+ * guild.channels.edit('222197033908436994', { name: 'new-channel' })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async edit(channel, data, reason) {
+ channel = this.resolve(channel);
+ if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
+
+ const parent = data.parent && this.client.channels.resolveId(data.parent);
+
+ if (typeof data.position !== 'undefined') await this.setPosition(channel, data.position, { reason });
+
+ let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild));
+
+ if (data.lockPermissions) {
+ if (parent) {
+ const newParent = this.guild.channels.resolve(parent);
+ if (newParent?.type === ChannelType.GuildCategory) {
+ permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
+ PermissionOverwrites.resolve(o, this.guild),
+ );
+ }
+ } else if (channel.parent) {
+ permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
+ PermissionOverwrites.resolve(o, this.guild),
+ );
+ }
+ }
+
+ const newData = await this.client.api.channels(channel.id).patch({
+ body: {
+ name: (data.name ?? channel.name).trim(),
+ type: data.type,
+ topic: data.topic,
+ nsfw: data.nsfw,
+ bitrate: data.bitrate ?? channel.bitrate,
+ user_limit: data.userLimit ?? channel.userLimit,
+ rtc_region: data.rtcRegion ?? channel.rtcRegion,
+ parent_id: parent,
+ lock_permissions: data.lockPermissions,
+ rate_limit_per_user: data.rateLimitPerUser,
+ default_auto_archive_duration: data.defaultAutoArchiveDuration,
+ permission_overwrites,
+ },
+ reason,
+ })
+
+ return this.client.actions.ChannelUpdate.handle(newData).updated;
+ }
+
+ /**
+ * Sets a new position for the guild channel.
+ * @param {GuildChannelResolvable} channel The channel to set the position for
+ * @param {number} position The new position for the guild channel
+ * @param {SetChannelPositionOptions} [options] Options for setting position
+ * @returns {Promise}
+ * @example
+ * // Set a new channel position
+ * guild.channels.setPosition('222078374472843266', 2)
+ * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
+ * .catch(console.error);
+ */
+ async setPosition(channel, position, { relative, reason } = {}) {
+ channel = this.resolve(channel);
+ if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
+ const updatedChannels = await Util.setPosition(
+ channel,
+ position,
+ relative,
+ this.guild._sortedChannels(channel),
+ this.client,
+ Routes.guildChannels(this.guild.id),
+ reason,
+ );
+
+ this.client.actions.GuildChannelsPositionUpdate.handle({
+ guild_id: this.guild.id,
+ channels: updatedChannels,
+ });
+ return channel;
+ }
+
+ /**
+ * Obtains one or more guild channels from Discord, or the channel cache if they're already available.
+ * @param {Snowflake} [id] The channel's id
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise>}
+ * @example
+ * // Fetch all channels from the guild (excluding threads)
+ * message.guild.channels.fetch()
+ * .then(channels => console.log(`There are ${channels.size} channels.`))
+ * .catch(console.error);
+ * @example
+ * // Fetch a single channel
+ * message.guild.channels.fetch('222197033908436994')
+ * .then(channel => console.log(`The channel name is: ${channel.name}`))
+ * .catch(console.error);
+ */
+ async fetch(id, { cache = true, force = false } = {}) {
+ if (id && !force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+
+ if (id) {
+ const data = await this.client.api.channels(id).get();
+ // Since this is the guild manager, throw if on a different guild
+ if (this.guild.id !== data.guild_id) throw new Error('GUILD_CHANNEL_UNOWNED');
+ return this.client.channels._add(data, this.guild, { cache });
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).channels.get();
+ const channels = new Collection();
+ for (const channel of data) channels.set(channel.id, this.client.channels._add(channel, this.guild, { cache }));
+ return channels;
+ }
+
+ /**
+ * Fetches all webhooks for the channel.
+ * @param {GuildChannelResolvable} channel The channel to fetch webhooks for
+ * @returns {Promise>}
+ * @example
+ * // Fetch webhooks
+ * guild.channels.fetchWebhooks('769862166131245066')
+ * .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
+ * .catch(console.error);
+ */
+ async fetchWebhooks(channel) {
+ const id = this.resolveId(channel);
+ if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
+ const data = await this.client.api.channels(id).webhooks.get();
+ return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection());
+ }
+
+ /**
+ * Data that can be resolved to give a Category Channel object. This can be:
+ * * A CategoryChannel object
+ * * A Snowflake
+ * @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable
+ */
+
+ /**
+ * The data needed for updating a channel's position.
+ * @typedef {Object} ChannelPosition
+ * @property {GuildChannel|Snowflake} channel Channel to update
+ * @property {number} [position] New position for the channel
+ * @property {CategoryChannelResolvable} [parent] Parent channel for this channel
+ * @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites
+ */
+
+ /**
+ * Batch-updates the guild's channels' positions.
+ * Only one channel's parent can be changed at a time
+ * @param {ChannelPosition[]} channelPositions Channel positions to update
+ * @returns {Promise}
+ * @example
+ * guild.channels.setPositions([{ channel: channelId, position: newChannelIndex }])
+ * .then(guild => console.log(`Updated channel positions for ${guild}`))
+ * .catch(console.error);
+ */
+ async setPositions(channelPositions) {
+ channelPositions = channelPositions.map(r => ({
+ id: this.client.channels.resolveId(r.channel),
+ position: r.position,
+ lock_permissions: r.lockPermissions,
+ parent_id: typeof r.parent !== 'undefined' ? this.channels.resolveId(r.parent) : undefined,
+ }));
+
+ await this.client.api.guilds(this.guild.id).channels.post({ body: channelPositions });
+ return this.client.actions.GuildChannelsPositionUpdate.handle({
+ guild_id: this.guild.id,
+ channels: channelPositions,
+ }).guild;
+ }
+
+ /**
+ * Obtains all active thread channels in the guild from Discord
+ * @param {boolean} [cache=true] Whether to cache the fetched data
+ * @returns {Promise}
+ * @example
+ * // Fetch all threads from the guild
+ * message.guild.channels.fetchActiveThreads()
+ * .then(fetched => console.log(`There are ${fetched.threads.size} threads.`))
+ * .catch(console.error);
+ */
+ async fetchActiveThreads(cache = true) {
+ const raw = await this.client.api.guilds(this.guild.id).threads.active.get();
+ return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
+ }
+
+ /**
+ * Deletes the channel.
+ * @param {GuildChannelResolvable} channel The channel to delete
+ * @param {string} [reason] Reason for deleting this channel
+ * @returns {Promise}
+ * @example
+ * // Delete the channel
+ * guild.channels.delete('858850993013260338', 'making room for new channels')
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async delete(channel, reason) {
+ const id = this.resolveId(channel);
+ if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
+ await this.client.api.channels(id).delete({ reason });
+ }
+}
+
+module.exports = GuildChannelManager;
diff --git a/src/managers/GuildEmojiManager.js b/src/managers/GuildEmojiManager.js
new file mode 100644
index 00000000..1935d03
--- /dev/null
+++ b/src/managers/GuildEmojiManager.js
@@ -0,0 +1,168 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes, PermissionFlagsBits } = require('discord-api-types/v9');
+const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
+const { Error, TypeError } = require('../errors');
+const DataResolver = require('../util/DataResolver');
+
+/**
+ * Manages API methods for GuildEmojis and stores their cache.
+ * @extends {BaseGuildEmojiManager}
+ */
+class GuildEmojiManager extends BaseGuildEmojiManager {
+ constructor(guild, iterable) {
+ super(guild.client, iterable);
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ _add(data, cache) {
+ return super._add(data, cache, { extras: [this.guild] });
+ }
+
+ /**
+ * Options used for creating an emoji in a guild.
+ * @typedef {Object} GuildEmojiCreateOptions
+ * @property {Collection|RoleResolvable[]} [roles] The roles to limit the emoji to
+ * @property {string} [reason] The reason for creating the emoji
+ */
+
+ /**
+ * Creates a new custom emoji in the guild.
+ * @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji
+ * @param {string} name The name for the emoji
+ * @param {GuildEmojiCreateOptions} [options] Options for creating the emoji
+ * @returns {Promise} The created emoji
+ * @example
+ * // Create a new emoji from a URL
+ * guild.emojis.create('https://i.imgur.com/w3duR07.png', 'rip')
+ * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
+ * .catch(console.error);
+ * @example
+ * // Create a new emoji from a file on your computer
+ * guild.emojis.create('./memes/banana.png', 'banana')
+ * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
+ * .catch(console.error);
+ */
+ async create(attachment, name, { roles, reason } = {}) {
+ attachment = await DataResolver.resolveImage(attachment);
+ if (!attachment) throw new TypeError('REQ_RESOURCE_TYPE');
+
+ const body = { image: attachment, name };
+ if (roles) {
+ if (!Array.isArray(roles) && !(roles instanceof Collection)) {
+ throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
+ }
+ body.roles = [];
+ for (const role of roles.values()) {
+ const resolvedRole = this.guild.roles.resolveId(role);
+ if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role);
+ body.roles.push(resolvedRole);
+ }
+ }
+
+ const emoji = await this.client.api.guilds(this.guild.id).emojis.post({ body, reason });
+ return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji;
+ }
+
+ /**
+ * Obtains one or more emojis from Discord, or the emoji cache if they're already available.
+ * @param {Snowflake} [id] The emoji's id
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise>}
+ * @example
+ * // Fetch all emojis from the guild
+ * message.guild.emojis.fetch()
+ * .then(emojis => console.log(`There are ${emojis.size} emojis.`))
+ * .catch(console.error);
+ * @example
+ * // Fetch a single emoji
+ * message.guild.emojis.fetch('222078108977594368')
+ * .then(emoji => console.log(`The emoji name is: ${emoji.name}`))
+ * .catch(console.error);
+ */
+ async fetch(id, { cache = true, force = false } = {}) {
+ if (id) {
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+ const emoji = await this.client.api.guilds(this.guild.id).emojis(id).get();
+ return this._add(emoji, cache);
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).emojis.get();
+ const emojis = new Collection();
+ for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache));
+ return emojis;
+ }
+
+ /**
+ * Deletes an emoji.
+ * @param {EmojiResolvable} emoji The Emoji resolvable to delete
+ * @param {string} [reason] Reason for deleting the emoji
+ * @returns {Promise}
+ */
+ async delete(emoji, reason) {
+ const id = this.resolveId(emoji);
+ if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
+ await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason });
+ }
+
+ /**
+ * Edits an emoji.
+ * @param {EmojiResolvable} emoji The Emoji resolvable to edit
+ * @param {GuildEmojiEditData} data The new data for the emoji
+ * @param {string} [reason] Reason for editing this emoji
+ * @returns {Promise}
+ */
+ async edit(emoji, data, reason) {
+ const id = this.resolveId(emoji);
+ if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
+ const roles = data.roles?.map(r => this.guild.roles.resolveId(r));
+ const newData = await this.client.api.guilds(this.guild.id).emojis(id).patch({
+ body: {
+ name: data.name,
+ roles,
+ },
+ reason,
+ })
+ const existing = this.cache.get(id);
+ if (existing) {
+ const clone = existing._clone();
+ clone._patch(newData);
+ return clone;
+ }
+ return this._add(newData);
+ }
+
+ /**
+ * Fetches the author for this emoji
+ * @param {EmojiResolvable} emoji The emoji to fetch the author of
+ * @returns {Promise}
+ */
+ async fetchAuthor(emoji) {
+ emoji = this.resolve(emoji);
+ if (!emoji) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
+ if (emoji.managed) {
+ throw new Error('EMOJI_MANAGED');
+ }
+
+ const { me } = this.guild;
+ if (!me) throw new Error('GUILD_UNCACHED_ME');
+ if (!me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers)) {
+ throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).emojis(emoji.id).get();
+ emoji._patch(data);
+ return emoji.author;
+ }
+}
+
+module.exports = GuildEmojiManager;
diff --git a/src/managers/GuildEmojiRoleManager.js b/src/managers/GuildEmojiRoleManager.js
new file mode 100644
index 00000000..f8da58d
--- /dev/null
+++ b/src/managers/GuildEmojiRoleManager.js
@@ -0,0 +1,118 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const DataManager = require('./DataManager');
+const { TypeError } = require('../errors');
+const { Role } = require('../structures/Role');
+
+/**
+ * Manages API methods for roles belonging to emojis and stores their cache.
+ * @extends {DataManager}
+ */
+class GuildEmojiRoleManager extends DataManager {
+ constructor(emoji) {
+ super(emoji.client, Role);
+
+ /**
+ * The emoji belonging to this manager
+ * @type {GuildEmoji}
+ */
+ this.emoji = emoji;
+ /**
+ * The guild belonging to this manager
+ * @type {Guild}
+ */
+ this.guild = emoji.guild;
+ }
+
+ /**
+ * The cache of roles belonging to this emoji
+ * @type {Collection}
+ * @readonly
+ */
+ get cache() {
+ return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id));
+ }
+
+ /**
+ * Adds a role (or multiple roles) to the list of roles that can use this emoji.
+ * @param {RoleResolvable|RoleResolvable[]|Collection} roleOrRoles The role or roles to add
+ * @returns {Promise}
+ */
+ add(roleOrRoles) {
+ if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles];
+
+ const resolvedRoles = [];
+ for (const role of roleOrRoles.values()) {
+ const resolvedRole = this.guild.roles.resolveId(role);
+ if (!resolvedRole) {
+ return Promise.reject(new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role));
+ }
+ resolvedRoles.push(resolvedRole);
+ }
+
+ const newRoles = [...new Set(resolvedRoles.concat(...this.cache.keys()))];
+ return this.set(newRoles);
+ }
+
+ /**
+ * Removes a role (or multiple roles) from the list of roles that can use this emoji.
+ * @param {RoleResolvable|RoleResolvable[]|Collection} roleOrRoles The role or roles to remove
+ * @returns {Promise}
+ */
+ remove(roleOrRoles) {
+ if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles];
+
+ const resolvedRoleIds = [];
+ for (const role of roleOrRoles.values()) {
+ const roleId = this.guild.roles.resolveId(role);
+ if (!roleId) {
+ return Promise.reject(new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role));
+ }
+ resolvedRoleIds.push(roleId);
+ }
+
+ const newRoles = [...this.cache.keys()].filter(id => !resolvedRoleIds.includes(id));
+ return this.set(newRoles);
+ }
+
+ /**
+ * Sets the role(s) that can use this emoji.
+ * @param {Collection|RoleResolvable[]} roles The roles or role ids to apply
+ * @returns {Promise}
+ * @example
+ * // Set the emoji's roles to a single role
+ * guildEmoji.roles.set(['391156570408615936'])
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Remove all roles from an emoji
+ * guildEmoji.roles.set([])
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ set(roles) {
+ return this.emoji.edit({ roles });
+ }
+
+ clone() {
+ const clone = new this.constructor(this.emoji);
+ clone._patch([...this.cache.keys()]);
+ return clone;
+ }
+
+ /**
+ * Patches the roles for this manager's cache
+ * @param {Snowflake[]} roles The new roles
+ * @private
+ */
+ _patch(roles) {
+ this.emoji._roles = roles;
+ }
+
+ valueOf() {
+ return this.cache;
+ }
+}
+
+module.exports = GuildEmojiRoleManager;
diff --git a/src/managers/GuildInviteManager.js b/src/managers/GuildInviteManager.js
new file mode 100644
index 00000000..435d08e
--- /dev/null
+++ b/src/managers/GuildInviteManager.js
@@ -0,0 +1,214 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { Error } = require('../errors');
+const Invite = require('../structures/Invite');
+const DataResolver = require('../util/DataResolver');
+
+/**
+ * Manages API methods for GuildInvites and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildInviteManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, Invite, iterable);
+
+ /**
+ * The guild this Manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name GuildInviteManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache, { id: data.code, extras: [this.guild] });
+ }
+
+ /**
+ * Data that resolves to give an Invite object. This can be:
+ * * An invite code
+ * * An invite URL
+ * @typedef {string} InviteResolvable
+ */
+
+ /**
+ * Data that can be resolved to a channel that an invite can be created on. This can be:
+ * * TextChannel
+ * * VoiceChannel
+ * * NewsChannel
+ * * StoreChannel
+ * * StageChannel
+ * * Snowflake
+ * @typedef {TextChannel|VoiceChannel|NewsChannel|StoreChannel|StageChannel|Snowflake}
+ * GuildInvitableChannelResolvable
+ */
+
+ /**
+ * Resolves an InviteResolvable to an Invite object.
+ * @method resolve
+ * @memberof GuildInviteManager
+ * @instance
+ * @param {InviteResolvable} invite The invite resolvable to resolve
+ * @returns {?Invite}
+ */
+
+ /**
+ * Resolves an InviteResolvable to an invite code string.
+ * @method resolveId
+ * @memberof GuildInviteManager
+ * @instance
+ * @param {InviteResolvable} invite The invite resolvable to resolve
+ * @returns {?string}
+ */
+
+ /**
+ * Options used to fetch a single invite from a guild.
+ * @typedef {Object} FetchInviteOptions
+ * @property {InviteResolvable} code The invite to fetch
+ * @property {boolean} [cache=true] Whether or not to cache the fetched invite
+ * @property {boolean} [force=false] Whether to skip the cache check and request the API
+ */
+
+ /**
+ * Options used to fetch all invites from a guild.
+ * @typedef {Object} FetchInvitesOptions
+ * @property {GuildInvitableChannelResolvable} [channelId]
+ * The channel to fetch all invites from
+ * @property {boolean} [cache=true] Whether or not to cache the fetched invites
+ */
+
+ /**
+ * Fetches invite(s) from Discord.
+ * @param {InviteResolvable|FetchInviteOptions|FetchInvitesOptions} [options] Options for fetching guild invite(s)
+ * @returns {Promise>}
+ * @example
+ * // Fetch all invites from a guild
+ * guild.invites.fetch()
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch all invites from a guild without caching
+ * guild.invites.fetch({ cache: false })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch all invites from a channel
+ * guild.invites.fetch({ channelId: '222197033908436994' })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch a single invite
+ * guild.invites.fetch('bRCvFy9')
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch a single invite without checking cache
+ * guild.invites.fetch({ code: 'bRCvFy9', force: true })
+ * .then(console.log)
+ * .catch(console.error)
+ * @example
+ * // Fetch a single invite without caching
+ * guild.invites.fetch({ code: 'bRCvFy9', cache: false })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ fetch(options) {
+ if (!options) return this._fetchMany();
+ if (typeof options === 'string') {
+ const code = DataResolver.resolveInviteCode(options);
+ if (!code) return Promise.reject(new Error('INVITE_RESOLVE_CODE'));
+ return this._fetchSingle({ code, cache: true });
+ }
+ if (!options.code) {
+ if (options.channelId) {
+ const id = this.guild.channels.resolveId(options.channelId);
+ if (!id) return Promise.reject(new Error('GUILD_CHANNEL_RESOLVE'));
+ return this._fetchChannelMany(id, options.cache);
+ }
+
+ if ('cache' in options) return this._fetchMany(options.cache);
+ return Promise.reject(new Error('INVITE_RESOLVE_CODE'));
+ }
+ return this._fetchSingle({
+ ...options,
+ code: DataResolver.resolveInviteCode(options.code),
+ });
+ }
+
+ async _fetchSingle({ code, cache, force = false }) {
+ if (!force) {
+ const existing = this.cache.get(code);
+ if (existing) return existing;
+ }
+
+ const invites = await this._fetchMany(cache);
+ const invite = invites.get(code);
+ if (!invite) throw new Error('INVITE_NOT_FOUND');
+ return invite;
+ }
+
+ async _fetchMany(cache) {
+ const data = await this.client.api.guilds(this.guild.id).invites.get();
+ return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection());
+ }
+
+ async _fetchChannelMany(channelId, cache) {
+ const data = await this.client.api.channels(channelId).invites.get();
+ return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection());
+ }
+
+ /**
+ * Create an invite to the guild from the provided channel.
+ * @param {GuildInvitableChannelResolvable} channel The options for creating the invite from a channel.
+ * @param {CreateInviteOptions} [options={}] The options for creating the invite from a channel.
+ * @returns {Promise}
+ * @example
+ * // Create an invite to a selected channel
+ * guild.invites.create('599942732013764608')
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async create(
+ channel,
+ { temporary, maxAge, maxUses, unique, targetUser, targetApplication, targetType, reason } = {},
+ ) {
+ const id = this.guild.channels.resolveId(channel);
+ if (!id) throw new Error('GUILD_CHANNEL_RESOLVE');
+
+ const invite = await this.client.api.channels(id).invites.post({
+ body: {
+ temporary,
+ max_age: maxAge,
+ max_uses: maxUses,
+ unique,
+ target_user_id: this.client.users.resolveId(targetUser),
+ target_application_id: targetApplication?.id ?? targetApplication?.applicationId ?? targetApplication,
+ target_type: targetType,
+ },
+ reason,
+ });
+ return new Invite(this.client, invite);
+ }
+
+ /**
+ * Deletes an invite.
+ * @param {InviteResolvable} invite The invite to delete
+ * @param {string} [reason] Reason for deleting the invite
+ * @returns {Promise}
+ */
+ async delete(invite, reason) {
+ const code = DataResolver.resolveInviteCode(invite);
+
+ await this.client.api.invites(code).delete({ reason });
+ }
+}
+
+module.exports = GuildInviteManager;
diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js
new file mode 100644
index 00000000..d76f9f7
--- /dev/null
+++ b/src/managers/GuildManager.js
@@ -0,0 +1,281 @@
+'use strict';
+
+const process = require('node:process');
+const { setTimeout, clearTimeout } = require('node:timers');
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { Guild } = require('../structures/Guild');
+const GuildChannel = require('../structures/GuildChannel');
+const GuildEmoji = require('../structures/GuildEmoji');
+const { GuildMember } = require('../structures/GuildMember');
+const Invite = require('../structures/Invite');
+const OAuth2Guild = require('../structures/OAuth2Guild');
+const { Role } = require('../structures/Role');
+const DataResolver = require('../util/DataResolver');
+const Events = require('../util/Events');
+const PermissionsBitField = require('../util/PermissionsBitField');
+const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField');
+const { resolveColor } = require('../util/Util');
+
+let cacheWarningEmitted = false;
+
+/**
+ * Manages API methods for Guilds and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, Guild, iterable);
+ if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') {
+ cacheWarningEmitted = true;
+ process.emitWarning(
+ `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
+ 'UnsupportedCacheOverwriteWarning',
+ );
+ }
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name GuildManager#cache
+ */
+
+ /**
+ * Data that resolves to give a Guild object. This can be:
+ * * A Guild object
+ * * A GuildChannel object
+ * * A GuildEmoji object
+ * * A Role object
+ * * A Snowflake
+ * * An Invite object
+ * @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable
+ */
+
+ /**
+ * Partial data for a Role.
+ * @typedef {Object} PartialRoleData
+ * @property {Snowflake|number} [id] The role's id, used to set channel overrides,
+ * this is a placeholder and will be replaced by the API after consumption
+ * @property {string} [name] The name of the role
+ * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number
+ * @property {boolean} [hoist] Whether or not the role should be hoisted
+ * @property {number} [position] The position of the role
+ * @property {PermissionResolvable} [permissions] The permissions of the role
+ * @property {boolean} [mentionable] Whether or not the role should be mentionable
+ */
+
+ /**
+ * Partial overwrite data.
+ * @typedef {Object} PartialOverwriteData
+ * @property {Snowflake|number} id The id of the {@link Role} or {@link User} this overwrite belongs to
+ * @property {OverwriteType} [type] The type of this overwrite
+ * @property {PermissionResolvable} [allow] The permissions to allow
+ * @property {PermissionResolvable} [deny] The permissions to deny
+ */
+
+ /**
+ * Partial data for a Channel.
+ * @typedef {Object} PartialChannelData
+ * @property {Snowflake|number} [id] The channel's id, used to set its parent,
+ * this is a placeholder and will be replaced by the API after consumption
+ * @property {Snowflake|number} [parentId] The parent id for this channel
+ * @property {ChannelType|number} [type] The type of the channel
+ * @property {string} name The name of the channel
+ * @property {string} [topic] The topic of the text channel
+ * @property {boolean} [nsfw] Whether the channel is NSFW
+ * @property {number} [bitrate] The bitrate of the voice channel
+ * @property {number} [userLimit] The user limit of the channel
+ * @property {?string} [rtcRegion] The RTC region of the channel
+ * @property {PartialOverwriteData[]} [permissionOverwrites]
+ * Overwrites of the channel
+ * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) of the channel in seconds
+ */
+
+ /**
+ * Resolves a GuildResolvable to a Guild object.
+ * @method resolve
+ * @memberof GuildManager
+ * @instance
+ * @param {GuildResolvable} guild The guild resolvable to identify
+ * @returns {?Guild}
+ */
+ resolve(guild) {
+ if (
+ guild instanceof GuildChannel ||
+ guild instanceof GuildMember ||
+ guild instanceof GuildEmoji ||
+ guild instanceof Role ||
+ (guild instanceof Invite && guild.guild)
+ ) {
+ return super.resolve(guild.guild);
+ }
+ return super.resolve(guild);
+ }
+
+ /**
+ * Resolves a {@link GuildResolvable} to a {@link Guild} id string.
+ * @method resolveId
+ * @memberof GuildManager
+ * @instance
+ * @param {GuildResolvable} guild The guild resolvable to identify
+ * @returns {?Snowflake}
+ */
+ resolveId(guild) {
+ if (
+ guild instanceof GuildChannel ||
+ guild instanceof GuildMember ||
+ guild instanceof GuildEmoji ||
+ guild instanceof Role ||
+ (guild instanceof Invite && guild.guild)
+ ) {
+ return super.resolveId(guild.guild.id);
+ }
+ return super.resolveId(guild);
+ }
+
+ /**
+ * Options used to create a guild.
+ * @typedef {Object} GuildCreateOptions
+ * @property {Snowflake|number} [afkChannelId] The AFK channel's id
+ * @property {number} [afkTimeout] The AFK timeout in seconds
+ * @property {PartialChannelData[]} [channels=[]] The channels for this guild
+ * @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notifications
+ * for the guild
+ * @property {ExplicitContentFilterLevel} [explicitContentFilter] The explicit content filter level for the guild
+ * @property {?(BufferResolvable|Base64Resolvable)} [icon=null] The icon for the guild
+ * @property {PartialRoleData[]} [roles=[]] The roles for this guild,
+ * the first element of this array is used to change properties of the guild's everyone role.
+ * @property {Snowflake|number} [systemChannelId] The system channel's id
+ * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The flags of the system channel
+ * @property {VerificationLevel} [verificationLevel] The verification level for the guild
+ */
+
+ /**
+ * Creates a guild.
+ * This is only available to bots in fewer than 10 guilds.
+ * @param {string} name The name of the guild
+ * @param {GuildCreateOptions} [options] Options for creating the guild
+ * @returns {Promise} The guild that was created
+ */
+ async create(
+ name,
+ {
+ afkChannelId,
+ afkTimeout,
+ channels = [],
+ defaultMessageNotifications,
+ explicitContentFilter,
+ icon = null,
+ roles = [],
+ systemChannelId,
+ systemChannelFlags,
+ verificationLevel,
+ } = {},
+ ) {
+ icon = await DataResolver.resolveImage(icon);
+
+ for (const channel of channels) {
+ channel.parent_id = channel.parentId;
+ delete channel.parentId;
+ channel.user_limit = channel.userLimit;
+ delete channel.userLimit;
+ channel.rate_limit_per_user = channel.rateLimitPerUser;
+ delete channel.rateLimitPerUser;
+ channel.rtc_region = channel.rtcRegion;
+ delete channel.rtcRegion;
+
+ if (!channel.permissionOverwrites) continue;
+ for (const overwrite of channel.permissionOverwrites) {
+ overwrite.allow &&= PermissionsBitField.resolve(overwrite.allow).toString();
+ overwrite.deny &&= PermissionsBitField.resolve(overwrite.deny).toString();
+ }
+ channel.permission_overwrites = channel.permissionOverwrites;
+ delete channel.permissionOverwrites;
+ }
+ for (const role of roles) {
+ role.color &&= resolveColor(role.color);
+ role.permissions &&= PermissionsBitField.resolve(role.permissions).toString();
+ }
+ systemChannelFlags &&= SystemChannelFlagsBitField.resolve(systemChannelFlags);
+
+ const data = await this.client.api.guilds.post({
+ body: {
+ name,
+ icon,
+ verification_level: verificationLevel,
+ default_message_notifications: defaultMessageNotifications,
+ explicit_content_filter: explicitContentFilter,
+ roles,
+ channels,
+ afk_channel_id: afkChannelId,
+ afk_timeout: afkTimeout,
+ system_channel_id: systemChannelId,
+ system_channel_flags: systemChannelFlags,
+ },
+ });
+
+ if (this.client.guilds.cache.has(data.id)) return this.client.guilds.cache.get(data.id);
+
+ return new Promise(resolve => {
+ const handleGuild = guild => {
+ if (guild.id === data.id) {
+ clearTimeout(timeout);
+ this.client.removeListener(Events.GuildCreate, handleGuild);
+ this.client.decrementMaxListeners();
+ resolve(guild);
+ }
+ };
+ this.client.incrementMaxListeners();
+ this.client.on(Events.GuildCreate, handleGuild);
+
+ const timeout = setTimeout(() => {
+ this.client.removeListener(Events.GuildCreate, handleGuild);
+ this.client.decrementMaxListeners();
+ resolve(this.client.guilds._add(data));
+ }, 10_000).unref();
+ });
+ }
+
+ /**
+ * Options used to fetch a single guild.
+ * @typedef {BaseFetchOptions} FetchGuildOptions
+ * @property {GuildResolvable} guild The guild to fetch
+ * @property {boolean} [withCounts=true] Whether the approximate member and presence counts should be returned
+ */
+
+ /**
+ * Options used to fetch multiple guilds.
+ * @typedef {Object} FetchGuildsOptions
+ * @property {Snowflake} [before] Get guilds before this guild id
+ * @property {Snowflake} [after] Get guilds after this guild id
+ * @property {number} [limit] Maximum number of guilds to request (1-200)
+ */
+
+ /**
+ * Obtains one or multiple guilds from Discord, or the guild cache if it's already available.
+ * @param {GuildResolvable|FetchGuildOptions|FetchGuildsOptions} [options] The guild's id or options
+ * @returns {Promise>}
+ */
+ async fetch(options = {}) {
+ const id = this.resolveId(options) ?? this.resolveId(options.guild);
+
+ if (id) {
+ if (!options.force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+
+ const data = await this.client.api.guilds(id).get({
+ query: new URLSearchParams({ with_counts: options.withCounts ?? true }),
+ })
+ return this._add(data, options.cache);
+ }
+
+ const data = await this.client.api.users('@me').guilds.get({ query: new URLSearchParams(options) });
+ return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
+ }
+}
+
+module.exports = GuildManager;
diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js
new file mode 100644
index 00000000..857b7b4
--- /dev/null
+++ b/src/managers/GuildMemberManager.js
@@ -0,0 +1,472 @@
+'use strict';
+
+const { Buffer } = require('node:buffer');
+const { setTimeout, clearTimeout } = require('node:timers');
+const { Collection } = require('@discordjs/collection');
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const { Routes, GatewayOpcodes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { Error, TypeError, RangeError } = require('../errors');
+const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
+const { GuildMember } = require('../structures/GuildMember');
+const { Role } = require('../structures/Role');
+const Events = require('../util/Events');
+
+/**
+ * Manages API methods for GuildMembers and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildMemberManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, GuildMember, iterable);
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name GuildMemberManager#cache
+ */
+
+ _add(data, cache = true) {
+ return super._add(data, cache, { id: data.user.id, extras: [this.guild] });
+ }
+
+ /**
+ * Data that resolves to give a GuildMember object. This can be:
+ * * A GuildMember object
+ * * A User resolvable
+ * @typedef {GuildMember|UserResolvable} GuildMemberResolvable
+ */
+
+ /**
+ * Resolves a {@link GuildMemberResolvable} to a {@link GuildMember} object.
+ * @param {GuildMemberResolvable} member The user that is part of the guild
+ * @returns {?GuildMember}
+ */
+ resolve(member) {
+ const memberResolvable = super.resolve(member);
+ if (memberResolvable) return memberResolvable;
+ const userResolvable = this.client.users.resolveId(member);
+ if (userResolvable) return super.resolve(userResolvable);
+ return null;
+ }
+
+ /**
+ * Resolves a {@link GuildMemberResolvable} to a member id.
+ * @param {GuildMemberResolvable} member The user that is part of the guild
+ * @returns {?Snowflake}
+ */
+ resolveId(member) {
+ const memberResolvable = super.resolveId(member);
+ if (memberResolvable) return memberResolvable;
+ const userResolvable = this.client.users.resolveId(member);
+ return this.cache.has(userResolvable) ? userResolvable : null;
+ }
+
+ /**
+ * Options used to add a user to a guild using OAuth2.
+ * @typedef {Object} AddGuildMemberOptions
+ * @property {string} accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the
+ * bot's application
+ * @property {string} [nick] The nickname to give to the member (requires `MANAGE_NICKNAMES`)
+ * @property {Collection|RoleResolvable[]} [roles] The roles to add to the member
+ * (requires `MANAGE_ROLES`)
+ * @property {boolean} [mute] Whether the member should be muted (requires `MUTE_MEMBERS`)
+ * @property {boolean} [deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`)
+ * @property {boolean} [force] Whether to skip the cache check and call the API directly
+ * @property {boolean} [fetchWhenExisting=true] Whether to fetch the user if not cached and already a member
+ */
+
+ /**
+ * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission.
+ * @param {UserResolvable} user The user to add to the guild
+ * @param {AddGuildMemberOptions} options Options for adding the user to the guild
+ * @returns {Promise}
+ */
+ async add(user, options) {
+ const userId = this.client.users.resolveId(user);
+ if (!userId) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
+ if (!options.force) {
+ const cachedUser = this.cache.get(userId);
+ if (cachedUser) return cachedUser;
+ }
+ const resolvedOptions = {
+ access_token: options.accessToken,
+ nick: options.nick,
+ mute: options.mute,
+ deaf: options.deaf,
+ };
+ if (options.roles) {
+ if (!Array.isArray(options.roles) && !(options.roles instanceof Collection)) {
+ throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
+ }
+ const resolvedRoles = [];
+ for (const role of options.roles.values()) {
+ const resolvedRole = this.guild.roles.resolveId(role);
+ if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role);
+ resolvedRoles.push(resolvedRole);
+ }
+ resolvedOptions.roles = resolvedRoles;
+ }
+ const data = await this.client.rest.put(Routes.guildMember(this.guild.id, userId), { body: resolvedOptions });
+ // Data is an empty buffer if the member is already part of the guild.
+ return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data);
+ }
+
+ /**
+ * Options used to fetch a single member from a guild.
+ * @typedef {BaseFetchOptions} FetchMemberOptions
+ * @property {UserResolvable} user The user to fetch
+ */
+
+ /**
+ * Options used to fetch multiple members from a guild.
+ * @typedef {Object} FetchMembersOptions
+ * @property {UserResolvable|UserResolvable[]} user The user(s) to fetch
+ * @property {?string} query Limit fetch to members with similar usernames
+ * @property {number} [limit=0] Maximum number of members to request
+ * @property {boolean} [withPresences=false] Whether or not to include the presences
+ * @property {number} [time=120e3] Timeout for receipt of members
+ * @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
+ * @property {boolean} [force=false] Whether to skip the cache check and request the API
+ */
+
+ /**
+ * Fetches member(s) from Discord, even if they're offline.
+ * @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] If a UserResolvable, the user to fetch.
+ * If undefined, fetches all members.
+ * If a query, it limits the results to users with similar usernames.
+ * @returns {Promise>}
+ * @example
+ * // Fetch all members from a guild
+ * guild.members.fetch()
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch a single member
+ * guild.members.fetch('66564597481480192')
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch a single member without checking cache
+ * guild.members.fetch({ user, force: true })
+ * .then(console.log)
+ * .catch(console.error)
+ * @example
+ * // Fetch a single member without caching
+ * guild.members.fetch({ user, cache: false })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch by an array of users including their presences
+ * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true })
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Fetch by query
+ * guild.members.fetch({ query: 'hydra', limit: 1 })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ fetch(options) {
+ if (!options) return this._fetchMany();
+ const user = this.client.users.resolveId(options);
+ if (user) return this._fetchSingle({ user, cache: true });
+ if (options.user) {
+ if (Array.isArray(options.user)) {
+ options.user = options.user.map(u => this.client.users.resolveId(u));
+ return this._fetchMany(options);
+ } else {
+ options.user = this.client.users.resolveId(options.user);
+ }
+ if (!options.limit && !options.withPresences) return this._fetchSingle(options);
+ }
+ return this._fetchMany(options);
+ }
+
+ /**
+ * Options used for searching guild members.
+ * @typedef {Object} GuildSearchMembersOptions
+ * @property {string} query Filter members whose username or nickname start with this query
+ * @property {number} [limit] Maximum number of members to search
+ * @property {boolean} [cache=true] Whether or not to cache the fetched member(s)
+ */
+
+ /**
+ * Searches for members in the guild based on a query.
+ * @param {GuildSearchMembersOptions} options Options for searching members
+ * @returns {Promise>}
+ */
+ async search({ query, limit, cache = true } = {}) {
+ const data = await this.client.api.guilds(this.guild.id).members.search.get({
+ query: new URLSearchParams({ query, limit }),
+ })
+ return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
+ }
+
+ /**
+ * Options used for listing guild members.
+ * @typedef {Object} GuildListMembersOptions
+ * @property {Snowflake} [after] Limit fetching members to those with an id greater than the supplied id
+ * @property {number} [limit] Maximum number of members to list
+ * @property {boolean} [cache=true] Whether or not to cache the fetched member(s)
+ */
+
+ /**
+ * Lists up to 1000 members of the guild.
+ * @param {GuildListMembersOptions} [options] Options for listing members
+ * @returns {Promise>}
+ */
+ async list({ after, limit, cache = true } = {}) {
+ const query = new URLSearchParams({ limit });
+ if (after) {
+ query.set('after', after);
+ }
+ const data = await this.client.api.guilds(this.guild.id).members.get({ query });
+ return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
+ }
+
+ /**
+ * The data for editing a guild member.
+ * @typedef {Object} GuildMemberEditData
+ * @property {?string} [nick] The nickname to set for the member
+ * @property {Collection|RoleResolvable[]} [roles] The roles or role ids to apply
+ * @property {boolean} [mute] Whether or not the member should be muted
+ * @property {boolean} [deaf] Whether or not the member should be deafened
+ * @property {GuildVoiceChannelResolvable|null} [channel] Channel to move the member to
+ * (if they are connected to voice), or `null` if you want to disconnect them from voice
+ * @property {DateResolvable|null} [communicationDisabledUntil] The date or timestamp
+ * for the member's communication to be disabled until. Provide `null` to enable communication again.
+ */
+
+ /**
+ * Edits a member of the guild.
+ * The user must be a member of the guild
+ * @param {UserResolvable} user The member to edit
+ * @param {GuildMemberEditData} data The data to edit the member with
+ * @param {string} [reason] Reason for editing this user
+ * @returns {Promise}
+ */
+ async edit(user, data, reason) {
+ const id = this.client.users.resolveId(user);
+ if (!id) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
+
+ // Clone the data object for immutability
+ const _data = { ...data };
+ if (_data.channel) {
+ _data.channel = this.guild.channels.resolve(_data.channel);
+ if (!(_data.channel instanceof BaseGuildVoiceChannel)) {
+ throw new Error('GUILD_VOICE_CHANNEL_RESOLVE');
+ }
+ _data.channel_id = _data.channel.id;
+ _data.channel = undefined;
+ } else if (_data.channel === null) {
+ _data.channel_id = null;
+ _data.channel = undefined;
+ }
+ _data.roles &&= _data.roles.map(role => (role instanceof Role ? role.id : role));
+
+ _data.communication_disabled_until =
+ // eslint-disable-next-line eqeqeq
+ _data.communicationDisabledUntil != null
+ ? new Date(_data.communicationDisabledUntil).toISOString()
+ : _data.communicationDisabledUntil;
+
+ let endpoint = this.client.api.guilds(this.guild.id);
+ if (id === this.client.user.id) {
+ const keys = Object.keys(data);
+ if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me');
+ else endpoint = endpoint.members(id);
+ } else {
+ endpoint = endpoint.members(id);
+ }
+ const d = await endpoint.patch({ data: _data, reason });
+
+ const clone = this.cache.get(id)?._clone();
+ clone?._patch(d);
+ return clone ?? this._add(d, false);
+ }
+
+ /**
+ * Options used for pruning guild members.
+ * It's recommended to set {@link GuildPruneMembersOptions#count options.count}
+ * to `false` for large guilds.
+ * @typedef {Object} GuildPruneMembersOptions
+ * @property {number} [days] Number of days of inactivity required to kick
+ * @property {boolean} [dry=false] Get the number of users that will be kicked, without actually kicking them
+ * @property {boolean} [count] Whether or not to return the number of users that have been kicked.
+ * @property {RoleResolvable[]} [roles] Array of roles to bypass the "...and no roles" constraint when pruning
+ * @property {string} [reason] Reason for this prune
+ */
+
+ /**
+ * Prunes members from the guild based on how long they have been inactive.
+ * @param {GuildPruneMembersOptions} [options] Options for pruning
+ * @returns {Promise} The number of members that were/will be kicked
+ * @example
+ * // See how many members will be pruned
+ * guild.members.prune({ dry: true })
+ * .then(pruned => console.log(`This will prune ${pruned} people!`))
+ * .catch(console.error);
+ * @example
+ * // Actually prune the members
+ * guild.members.prune({ days: 1, reason: 'too many people!' })
+ * .then(pruned => console.log(`I just pruned ${pruned} people!`))
+ * .catch(console.error);
+ * @example
+ * // Include members with a specified role
+ * guild.members.prune({ days: 7, roles: ['657259391652855808'] })
+ * .then(pruned => console.log(`I just pruned ${pruned} people!`))
+ * .catch(console.error);
+ */
+ async prune({ days, dry = false, count: compute_prune_count, roles = [], reason } = {}) {
+ if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
+
+ const query = { days };
+ const resolvedRoles = [];
+
+ for (const role of roles) {
+ const resolvedRole = this.guild.roles.resolveId(role);
+ if (!resolvedRole) {
+ throw new TypeError('INVALID_ELEMENT', 'Array', 'options.roles', role);
+ }
+ resolvedRoles.push(resolvedRole);
+ }
+
+ if (resolvedRoles.length) {
+ query.include_roles = dry ? resolvedRoles.join(',') : resolvedRoles;
+ }
+
+ const endpoint = this.client.api.guilds(this.guild.id).prune;
+
+ const { pruned } = await (dry
+ ? endpoint.get({ query: new URLSearchParams(query), reason })
+ : endpoint.post({ body: { ...query, compute_prune_count }, reason }));
+
+ return pruned;
+ }
+
+ /**
+ * Kicks a user from the guild.
+ * The user must be a member of the guild
+ * @param {UserResolvable} user The member to kick
+ * @param {string} [reason] Reason for kicking
+ * @returns {Promise} Result object will be resolved as specifically as possible.
+ * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
+ * be resolved, the user's id will be the result.
+ * @example
+ * // Kick a user by id (or with a user/guild member object)
+ * guild.members.kick('84484653687267328')
+ * .then(kickInfo => console.log(`Kicked ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`))
+ * .catch(console.error);
+ */
+ async kick(user, reason) {
+ const id = this.client.users.resolveId(user);
+ if (!id) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
+
+ await this.clinet.api.guilds(this.guild.id).members(id).delete({ reason });
+
+ return this.resolve(user) ?? this.client.users.resolve(user) ?? id;
+ }
+
+ /**
+ * Bans a user from the guild.
+ * @param {UserResolvable} user The user to ban
+ * @param {BanOptions} [options] Options for the ban
+ * @returns {Promise} Result object will be resolved as specifically as possible.
+ * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
+ * be resolved, the user id will be the result.
+ * Internally calls the GuildBanManager#create method.
+ * @example
+ * // Ban a user by id (or with a user/guild member object)
+ * guild.members.ban('84484653687267328')
+ * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
+ * .catch(console.error);
+ */
+ ban(user, options) {
+ return this.guild.bans.create(user, options);
+ }
+
+ /**
+ * Unbans a user from the guild. Internally calls the {@link GuildBanManager#remove} method.
+ * @param {UserResolvable} user The user to unban
+ * @param {string} [reason] Reason for unbanning user
+ * @returns {Promise} The user that was unbanned
+ * @example
+ * // Unban a user by id (or with a user/guild member object)
+ * guild.members.unban('84484653687267328')
+ * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
+ * .catch(console.error);
+ */
+ unban(user, reason) {
+ return this.guild.bans.remove(user, reason);
+ }
+
+ async _fetchSingle({ user, cache, force = false }) {
+ if (!force) {
+ const existing = this.cache.get(user);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).members(user).get();
+ return this._add(data, cache);
+ }
+
+ _fetchMany({
+ limit = 0,
+ withPresences: presences = false,
+ user: user_ids,
+ query,
+ time = 120e3,
+ nonce = DiscordSnowflake.generate().toString(),
+ } = {}) {
+ return new Promise((resolve, reject) => {
+ if (!query && !user_ids) query = '';
+ if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
+ this.guild.shard.send({
+ op: GatewayOpcodes.RequestGuildMembers,
+ d: {
+ guild_id: this.guild.id,
+ presences,
+ user_ids,
+ query,
+ nonce,
+ limit,
+ },
+ });
+ const fetchedMembers = new Collection();
+ let i = 0;
+ const handler = (members, _, chunk) => {
+ timeout.refresh();
+ if (chunk.nonce !== nonce) return;
+ i++;
+ for (const member of members.values()) {
+ fetchedMembers.set(member.id, member);
+ }
+ if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) {
+ clearTimeout(timeout);
+ this.client.removeListener(Events.GuildMembersChunk, handler);
+ this.client.decrementMaxListeners();
+ let fetched = fetchedMembers;
+ if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
+ resolve(fetched);
+ }
+ };
+ const timeout = setTimeout(() => {
+ this.client.removeListener(Events.GuildMembersChunk, handler);
+ this.client.decrementMaxListeners();
+ reject(new Error('GUILD_MEMBERS_TIMEOUT'));
+ }, time).unref();
+ this.client.incrementMaxListeners();
+ this.client.on(Events.GuildMembersChunk, handler);
+ });
+ }
+}
+
+module.exports = GuildMemberManager;
diff --git a/src/managers/GuildMemberRoleManager.js b/src/managers/GuildMemberRoleManager.js
new file mode 100644
index 00000000..ce645b1
--- /dev/null
+++ b/src/managers/GuildMemberRoleManager.js
@@ -0,0 +1,192 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const DataManager = require('./DataManager');
+const { TypeError } = require('../errors');
+const { Role } = require('../structures/Role');
+
+/**
+ * Manages API methods for roles of a GuildMember and stores their cache.
+ * @extends {DataManager}
+ */
+class GuildMemberRoleManager extends DataManager {
+ constructor(member) {
+ super(member.client, Role);
+
+ /**
+ * The GuildMember this manager belongs to
+ * @type {GuildMember}
+ */
+ this.member = member;
+
+ /**
+ * The Guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = member.guild;
+ }
+
+ /**
+ * The roles of this member
+ * @type {Collection}
+ * @readonly
+ */
+ get cache() {
+ const everyone = this.guild.roles.everyone;
+ return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone);
+ }
+
+ /**
+ * The role of the member used to hoist them in a separate category in the users list
+ * @type {?Role}
+ * @readonly
+ */
+ get hoist() {
+ const hoistedRoles = this.cache.filter(role => role.hoist);
+ if (!hoistedRoles.size) return null;
+ return hoistedRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev));
+ }
+
+ /**
+ * The role of the member used to set their role icon
+ * @type {?Role}
+ * @readonly
+ */
+ get icon() {
+ const iconRoles = this.cache.filter(role => role.icon || role.unicodeEmoji);
+ if (!iconRoles.size) return null;
+ return iconRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev));
+ }
+
+ /**
+ * The role of the member used to set their color
+ * @type {?Role}
+ * @readonly
+ */
+ get color() {
+ const coloredRoles = this.cache.filter(role => role.color);
+ if (!coloredRoles.size) return null;
+ return coloredRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev));
+ }
+
+ /**
+ * The role of the member with the highest position
+ * @type {Role}
+ * @readonly
+ */
+ get highest() {
+ return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first());
+ }
+
+ /**
+ * The premium subscriber role of the guild, if present on the member
+ * @type {?Role}
+ * @readonly
+ */
+ get premiumSubscriberRole() {
+ return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null;
+ }
+
+ /**
+ * The managed role this member created when joining the guild, if any
+ * Only ever available on bots
+ * @type {?Role}
+ * @readonly
+ */
+ get botRole() {
+ if (!this.member.user.bot) return null;
+ return this.cache.find(role => role.tags?.botId === this.member.user.id) ?? null;
+ }
+
+ /**
+ * Adds a role (or multiple roles) to the member.
+ * @param {RoleResolvable|RoleResolvable[]|Collection} roleOrRoles The role or roles to add
+ * @param {string} [reason] Reason for adding the role(s)
+ * @returns {Promise}
+ */
+ async add(roleOrRoles, reason) {
+ if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) {
+ const resolvedRoles = [];
+ for (const role of roleOrRoles.values()) {
+ const resolvedRole = this.guild.roles.resolveId(role);
+ if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role);
+ resolvedRoles.push(resolvedRole);
+ }
+
+ const newRoles = [...new Set(resolvedRoles.concat(...this.cache.keys()))];
+ return this.set(newRoles, reason);
+ } else {
+ roleOrRoles = this.guild.roles.resolveId(roleOrRoles);
+ if (roleOrRoles === null) {
+ throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
+ }
+
+ await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].put({ reason });
+
+ const clone = this.member._clone();
+ clone._roles = [...this.cache.keys(), roleOrRoles];
+ return clone;
+ }
+ }
+
+ /**
+ * Removes a role (or multiple roles) from the member.
+ * @param {RoleResolvable|RoleResolvable[]|Collection} roleOrRoles The role or roles to remove
+ * @param {string} [reason] Reason for removing the role(s)
+ * @returns {Promise}
+ */
+ async remove(roleOrRoles, reason) {
+ if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) {
+ const resolvedRoles = [];
+ for (const role of roleOrRoles.values()) {
+ const resolvedRole = this.guild.roles.resolveId(role);
+ if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role);
+ resolvedRoles.push(resolvedRole);
+ }
+
+ const newRoles = this.cache.filter(role => !resolvedRoles.includes(role.id));
+ return this.set(newRoles, reason);
+ } else {
+ roleOrRoles = this.guild.roles.resolveId(roleOrRoles);
+ if (roleOrRoles === null) {
+ throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
+ }
+
+ await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].delete({ reason });
+
+ const clone = this.member._clone();
+ const newRoles = this.cache.filter(role => role.id !== roleOrRoles);
+ clone._roles = [...newRoles.keys()];
+ return clone;
+ }
+ }
+
+ /**
+ * Sets the roles applied to the member.
+ * @param {Collection|RoleResolvable[]} roles The roles or role ids to apply
+ * @param {string} [reason] Reason for applying the roles
+ * @returns {Promise}
+ * @example
+ * // Set the member's roles to a single role
+ * guildMember.roles.set(['391156570408615936'])
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Remove all the roles from a member
+ * guildMember.roles.set([])
+ * .then(member => console.log(`Member roles is now of ${member.roles.cache.size} size`))
+ * .catch(console.error);
+ */
+ set(roles, reason) {
+ return this.member.edit({ roles }, reason);
+ }
+
+ clone() {
+ const clone = new this.constructor(this.member);
+ clone.member._roles = [...this.cache.keys()];
+ return clone;
+ }
+}
+
+module.exports = GuildMemberRoleManager;
diff --git a/src/managers/GuildScheduledEventManager.js b/src/managers/GuildScheduledEventManager.js
new file mode 100644
index 00000000..98fdb2b
--- /dev/null
+++ b/src/managers/GuildScheduledEventManager.js
@@ -0,0 +1,309 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { GuildScheduledEventEntityType, Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError, Error } = require('../errors');
+const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
+const DataResolver = require('../util/DataResolver');
+
+/**
+ * Manages API methods for GuildScheduledEvents and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildScheduledEventManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, GuildScheduledEvent, iterable);
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name GuildScheduledEventManager#cache
+ */
+
+ /**
+ * Data that resolves to give a GuildScheduledEvent object. This can be:
+ * * A Snowflake
+ * * A GuildScheduledEvent object
+ * @typedef {Snowflake|GuildScheduledEvent} GuildScheduledEventResolvable
+ */
+
+ /**
+ * Options used to create a guild scheduled event.
+ * @typedef {Object} GuildScheduledEventCreateOptions
+ * @property {string} name The name of the guild scheduled event
+ * @property {DateResolvable} scheduledStartTime The time to schedule the event at
+ * @property {DateResolvable} [scheduledEndTime] The time to end the event at
+ * This is required if `entityType` is {@link GuildScheduledEventEntityType.External}
+ * @property {PrivacyLevel|number} privacyLevel The privacy level of the guild scheduled event
+ * @property {GuildScheduledEventEntityType|number} entityType The scheduled entity type of the event
+ * @property {string} [description] The description of the guild scheduled event
+ * @property {GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event
+ * This is required if `entityType` is {@link GuildScheduledEventEntityType.StageInstance} or
+ * {@link GuildScheduledEventEntityType.Voice}
+ * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
+ * guild scheduled event
+ * This is required if `entityType` is {@link GuildScheduledEventEntityType.External}
+ * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
+ * @property {string} [reason] The reason for creating the guild scheduled event
+ */
+
+ /**
+ * Options used to set entity metadata of a guild scheduled event.
+ * @typedef {Object} GuildScheduledEventEntityMetadataOptions
+ * @property {string} [location] The location of the guild scheduled event
+ * This is required if `entityType` is {@link GuildScheduledEventEntityType.External}
+ */
+
+ /**
+ * Creates a new guild scheduled event.
+ * @param {GuildScheduledEventCreateOptions} options Options for creating the guild scheduled event
+ * @returns {Promise}
+ */
+ async create(options) {
+ if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ let {
+ privacyLevel,
+ entityType,
+ channel,
+ name,
+ scheduledStartTime,
+ description,
+ scheduledEndTime,
+ entityMetadata,
+ reason,
+ image,
+ } = options;
+
+ let entity_metadata, channel_id;
+ if (entityType === GuildScheduledEventEntityType.External) {
+ channel_id = typeof channel === 'undefined' ? channel : null;
+ entity_metadata = { location: entityMetadata?.location };
+ } else {
+ channel_id = this.guild.channels.resolveId(channel);
+ if (!channel_id) throw new Error('GUILD_VOICE_CHANNEL_RESOLVE');
+ entity_metadata = typeof entityMetadata === 'undefined' ? entityMetadata : null;
+ }
+
+ const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').post({
+ body: {
+ channel_id,
+ name,
+ privacy_level: privacyLevel,
+ scheduled_start_time: new Date(scheduledStartTime).toISOString(),
+ scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime,
+ description,
+ entity_type: entityType,
+ entity_metadata,
+ image: image && (await DataResolver.resolveImage(image)),
+ },
+ reason,
+ })
+
+ return this._add(data);
+ }
+
+ /**
+ * Options used to fetch a single guild scheduled event from a guild.
+ * @typedef {BaseFetchOptions} FetchGuildScheduledEventOptions
+ * @property {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to fetch
+ * @property {boolean} [withUserCount=true] Whether to fetch the number of users subscribed to the scheduled event
+ */
+
+ /**
+ * Options used to fetch multiple guild scheduled events from a guild.
+ * @typedef {Object} FetchGuildScheduledEventsOptions
+ * @property {boolean} [cache] Whether or not to cache the fetched guild scheduled events
+ * @property {boolean} [withUserCount=true] Whether to fetch the number of users subscribed to each scheduled event
+ * should be returned
+ */
+
+ /**
+ * Obtains one or more guild scheduled events from Discord, or the guild cache if it's already available.
+ * @param {GuildScheduledEventResolvable|FetchGuildScheduledEventOptions|FetchGuildScheduledEventsOptions} [options]
+ * The id of the guild scheduled event or options
+ * @returns {Promise>}
+ */
+ async fetch(options = {}) {
+ const id = this.resolveId(options.guildScheduledEvent ?? options);
+
+ if (id) {
+ if (!options.force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+
+ const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', id).get({
+ query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
+ })
+ return this._add(data, options.cache);
+ }
+
+ const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').get({
+ query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
+ })
+
+ return data.reduce(
+ (coll, rawGuildScheduledEventData) =>
+ coll.set(
+ rawGuildScheduledEventData.id,
+ this.guild.scheduledEvents._add(rawGuildScheduledEventData, options.cache),
+ ),
+ new Collection(),
+ );
+ }
+
+ /**
+ * Options used to edit a guild scheduled event.
+ * @typedef {Object} GuildScheduledEventEditOptions
+ * @property {string} [name] The name of the guild scheduled event
+ * @property {DateResolvable} [scheduledStartTime] The time to schedule the event at
+ * @property {DateResolvable} [scheduledEndTime] The time to end the event at
+ * @property {PrivacyLevel|number} [privacyLevel] The privacy level of the guild scheduled event
+ * @property {GuildScheduledEventEntityType|number} [entityType] The scheduled entity type of the event
+ * @property {string} [description] The description of the guild scheduled event
+ * @property {?GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event
+ * @property {GuildScheduledEventStatus|number} [status] The status of the guild scheduled event
+ * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
+ * guild scheduled event
+ * This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is
+ * {@link GuildScheduledEventEntityType.External}
+ * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
+ * @property {string} [reason] The reason for editing the guild scheduled event
+ */
+
+ /**
+ * Edits a guild scheduled event.
+ * @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to edit
+ * @param {GuildScheduledEventEditOptions} options Options to edit the guild scheduled event
+ * @returns {Promise}
+ */
+ async edit(guildScheduledEvent, options) {
+ const guildScheduledEventId = this.resolveId(guildScheduledEvent);
+ if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
+
+ if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ let {
+ privacyLevel,
+ entityType,
+ channel,
+ status,
+ name,
+ scheduledStartTime,
+ description,
+ scheduledEndTime,
+ entityMetadata,
+ reason,
+ image,
+ } = options;
+
+ let entity_metadata;
+ if (entityMetadata) {
+ entity_metadata = {
+ location: entityMetadata.location,
+ };
+ }
+
+ const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).patch({
+ body: {
+ channel_id: typeof channel === 'undefined' ? channel : this.guild.channels.resolveId(channel),
+ name,
+ privacy_level: privacyLevel,
+ scheduled_start_time: scheduledStartTime ? new Date(scheduledStartTime).toISOString() : undefined,
+ scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime,
+ description,
+ entity_type: entityType,
+ status,
+ image: image && (await DataResolver.resolveImage(image)),
+ entity_metadata,
+ },
+ reason,
+ })
+
+ return this._add(data);
+ }
+
+ /**
+ * Deletes a guild scheduled event.
+ * @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to delete
+ * @returns {Promise}
+ */
+ async delete(guildScheduledEvent) {
+ const guildScheduledEventId = this.resolveId(guildScheduledEvent);
+ if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
+
+ await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).delete();
+ }
+
+ /**
+ * Options used to fetch subscribers of a guild scheduled event
+ * @typedef {Object} FetchGuildScheduledEventSubscribersOptions
+ * @property {number} [limit] The maximum numbers of users to fetch
+ * @property {boolean} [withMember] Whether to fetch guild member data of the users
+ * @property {Snowflake} [before] Consider only users before this user id
+ * @property {Snowflake} [after] Consider only users after this user id
+ * If both `before` and `after` are provided, only `before` is respected
+ */
+
+ /**
+ * Represents a subscriber of a {@link GuildScheduledEvent}
+ * @typedef {Object} GuildScheduledEventUser
+ * @property {Snowflake} guildScheduledEventId The id of the guild scheduled event which the user subscribed to
+ * @property {User} user The user that subscribed to the guild scheduled event
+ * @property {?GuildMember} member The guild member associated with the user, if any
+ */
+
+ /**
+ * Fetches subscribers of a guild scheduled event.
+ * @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to fetch subscribers of
+ * @param {FetchGuildScheduledEventSubscribersOptions} [options={}] Options for fetching the subscribers
+ * @returns {Promise>}
+ */
+ async fetchSubscribers(guildScheduledEvent, options = {}) {
+ const guildScheduledEventId = this.resolveId(guildScheduledEvent);
+ if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
+
+ let { limit, withMember, before, after } = options;
+
+ const query = new URLSearchParams();
+
+ if (limit) {
+ query.set('limit', limit);
+ }
+
+ if (typeof withMember !== 'undefined') {
+ query.set('with_member', withMember);
+ }
+
+ if (before) {
+ query.set('before', before);
+ }
+
+ if (after) {
+ query.set('after', after);
+ }
+
+ const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).users.get({
+ query,
+ });
+
+ return data.reduce(
+ (coll, rawData) =>
+ coll.set(rawData.user.id, {
+ guildScheduledEventId: rawData.guild_scheduled_event_id,
+ user: this.client.users._add(rawData.user),
+ member: rawData.member ? this.guild.members._add({ ...rawData.member, user: rawData.user }) : null,
+ }),
+ new Collection(),
+ );
+ }
+}
+
+module.exports = GuildScheduledEventManager;
diff --git a/src/managers/GuildStickerManager.js b/src/managers/GuildStickerManager.js
new file mode 100644
index 00000000..29202cf
--- /dev/null
+++ b/src/managers/GuildStickerManager.js
@@ -0,0 +1,183 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const MessagePayload = require('../structures/MessagePayload');
+const { Sticker } = require('../structures/Sticker');
+
+/**
+ * Manages API methods for Guild Stickers and stores their cache.
+ * @extends {CachedManager}
+ */
+class GuildStickerManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, Sticker, iterable);
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of Guild Stickers
+ * @type {Collection}
+ * @name GuildStickerManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache, { extras: [this.guild] });
+ }
+
+ /**
+ * Options for creating a guild sticker.
+ * @typedef {Object} GuildStickerCreateOptions
+ * @property {?string} [description] The description for the sticker
+ * @property {string} [reason] Reason for creating the sticker
+ */
+
+ /**
+ * Creates a new custom sticker in the guild.
+ * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} file The file for the sticker
+ * @param {string} name The name for the sticker
+ * @param {string} tags The Discord name of a unicode emoji representing the sticker's expression
+ * @param {GuildStickerCreateOptions} [options] Options
+ * @returns {Promise} The created sticker
+ * @example
+ * // Create a new sticker from a URL
+ * guild.stickers.create('https://i.imgur.com/w3duR07.png', 'rip', 'headstone')
+ * .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`))
+ * .catch(console.error);
+ * @example
+ * // Create a new sticker from a file on your computer
+ * guild.stickers.create('./memes/banana.png', 'banana', 'banana')
+ * .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`))
+ * .catch(console.error);
+ */
+ async create(file, name, tags, { description, reason } = {}) {
+ const resolvedFile = await MessagePayload.resolveFile(file);
+ if (!resolvedFile) throw new TypeError('REQ_RESOURCE_TYPE');
+ file = { ...resolvedFile, key: 'file' };
+
+ const body = { name, tags, description: description ?? '' };
+
+ const sticker = await this.client.api.guilds(this.guild.id).stickers.post({
+ appendToFormData: true,
+ body,
+ files: [file],
+ reason,
+ });
+ return this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker;
+ }
+
+ /**
+ * Data that resolves to give a Sticker object. This can be:
+ * * A Sticker object
+ * * A Snowflake
+ * @typedef {Sticker|Snowflake} StickerResolvable
+ */
+
+ /**
+ * Resolves a StickerResolvable to a Sticker object.
+ * @method resolve
+ * @memberof GuildStickerManager
+ * @instance
+ * @param {StickerResolvable} sticker The Sticker resolvable to identify
+ * @returns {?Sticker}
+ */
+
+ /**
+ * Resolves a StickerResolvable to a Sticker id string.
+ * @method resolveId
+ * @memberof GuildStickerManager
+ * @instance
+ * @param {StickerResolvable} sticker The Sticker resolvable to identify
+ * @returns {?Snowflake}
+ */
+
+ /**
+ * Edits a sticker.
+ * @param {StickerResolvable} sticker The sticker to edit
+ * @param {GuildStickerEditData} [data] The new data for the sticker
+ * @param {string} [reason] Reason for editing this sticker
+ * @returns {Promise}
+ */
+ async edit(sticker, data, reason) {
+ const stickerId = this.resolveId(sticker);
+ if (!stickerId) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
+
+ const d = await this.client.api.guilds(this.guild.id).stickers(stickerId).patch({
+ body: data,
+ reason,
+ });
+
+ const existing = this.cache.get(stickerId);
+ if (existing) {
+ const clone = existing._clone();
+ clone._patch(d);
+ return clone;
+ }
+ return this._add(d);
+ }
+
+ /**
+ * Deletes a sticker.
+ * @param {StickerResolvable} sticker The sticker to delete
+ * @param {string} [reason] Reason for deleting this sticker
+ * @returns {Promise}
+ */
+ async delete(sticker, reason) {
+ sticker = this.resolveId(sticker);
+ if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
+
+ await this.client.api.guilds(this.guild.id).stickers(sticker).delete({ reason });
+ }
+
+ /**
+ * Obtains one or more stickers from Discord, or the sticker cache if they're already available.
+ * @param {Snowflake} [id] The Sticker's id
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise>}
+ * @example
+ * // Fetch all stickers from the guild
+ * message.guild.stickers.fetch()
+ * .then(stickers => console.log(`There are ${stickers.size} stickers.`))
+ * .catch(console.error);
+ * @example
+ * // Fetch a single sticker
+ * message.guild.stickers.fetch('222078108977594368')
+ * .then(sticker => console.log(`The sticker name is: ${sticker.name}`))
+ * .catch(console.error);
+ */
+ async fetch(id, { cache = true, force = false } = {}) {
+ if (id) {
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+ const sticker = await this.client.api.guilds(this.guild.id).stickers(id).get();
+ return this._add(sticker, cache);
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).stickers.get();
+ return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)]));
+ }
+
+ /**
+ * Fetches the user who uploaded this sticker, if this is a guild sticker.
+ * @param {StickerResolvable} sticker The sticker to fetch the user for
+ * @returns {Promise}
+ */
+ async fetchUser(sticker) {
+ sticker = this.resolve(sticker);
+ if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
+ const data = await this.client.api.guilds(this.guild.id).stickers(sticker.id).get();
+ sticker._patch(data);
+ return sticker.user;
+ }
+}
+
+module.exports = GuildStickerManager;
diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js
new file mode 100644
index 00000000..6f67f2d
--- /dev/null
+++ b/src/managers/MessageManager.js
@@ -0,0 +1,235 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const { Message } = require('../structures/Message');
+const MessagePayload = require('../structures/MessagePayload');
+const Util = require('../util/Util');
+
+/**
+ * Manages API methods for Messages and holds their cache.
+ * @extends {CachedManager}
+ */
+class MessageManager extends CachedManager {
+ constructor(channel, iterable) {
+ super(channel.client, Message, iterable);
+
+ /**
+ * The channel that the messages belong to
+ * @type {TextBasedChannels}
+ */
+ this.channel = channel;
+ }
+
+ /**
+ * The cache of Messages
+ * @type {Collection}
+ * @name MessageManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache);
+ }
+
+ /**
+ * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and
+ * `after` are mutually exclusive. All the parameters are optional.
+ * @typedef {Object} ChannelLogsQueryOptions
+ * @property {number} [limit] Number of messages to acquire
+ * @property {Snowflake} [before] The message's id to get the messages that were posted before it
+ * @property {Snowflake} [after] The message's id to get the messages that were posted after it
+ * @property {Snowflake} [around] The message's id to get the messages that were posted around it
+ */
+
+ /**
+ * Gets a message, or messages, from this channel.
+ * The returned Collection does not contain reaction users of the messages if they were not cached.
+ * Those need to be fetched separately in such a case.
+ * @param {Snowflake|ChannelLogsQueryOptions} [message] The id of the message to fetch, or query parameters.
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise>}
+ * @example
+ * // Get message
+ * channel.messages.fetch('99539446449315840')
+ * .then(message => console.log(message.content))
+ * .catch(console.error);
+ * @example
+ * // Get messages
+ * channel.messages.fetch({ limit: 10 })
+ * .then(messages => console.log(`Received ${messages.size} messages`))
+ * .catch(console.error);
+ * @example
+ * // Get messages and filter by user id
+ * channel.messages.fetch()
+ * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`))
+ * .catch(console.error);
+ */
+ fetch(message, { cache = true, force = false } = {}) {
+ return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache);
+ }
+
+ /**
+ * Fetches the pinned messages of this channel and returns a collection of them.
+ * The returned Collection does not contain any reaction data of the messages.
+ * Those need to be fetched separately.
+ * @param {boolean} [cache=true] Whether to cache the message(s)
+ * @returns {Promise>}
+ * @example
+ * // Get pinned messages
+ * channel.messages.fetchPinned()
+ * .then(messages => console.log(`Received ${messages.size} messages`))
+ * .catch(console.error);
+ */
+ async fetchPinned(cache = true) {
+ const data = await this.client.api.channels(this.channel.id).pins.get();
+ const messages = new Collection();
+ for (const message of data) messages.set(message.id, this._add(message, cache));
+ return messages;
+ }
+
+ /**
+ * Data that can be resolved to a Message object. This can be:
+ * * A Message
+ * * A Snowflake
+ * @typedef {Message|Snowflake} MessageResolvable
+ */
+
+ /**
+ * Resolves a {@link MessageResolvable} to a {@link Message} object.
+ * @method resolve
+ * @memberof MessageManager
+ * @instance
+ * @param {MessageResolvable} message The message resolvable to resolve
+ * @returns {?Message}
+ */
+
+ /**
+ * Resolves a {@link MessageResolvable} to a {@link Message} id.
+ * @method resolveId
+ * @memberof MessageManager
+ * @instance
+ * @param {MessageResolvable} message The message resolvable to resolve
+ * @returns {?Snowflake}
+ */
+
+ /**
+ * Edits a message, even if it's not cached.
+ * @param {MessageResolvable} message The message to edit
+ * @param {string|MessageEditOptions|MessagePayload} options The options to edit the message
+ * @returns {Promise}
+ */
+ async edit(message, options) {
+ const messageId = this.resolveId(message);
+ if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
+
+ const { body, files } = await (options instanceof MessagePayload
+ ? options
+ : MessagePayload.create(message instanceof Message ? message : this, options)
+ )
+ .resolveBody()
+ .resolveFiles();
+ const d = await this.client.api.channels(this.channel.id).messages(messageId).patch({ body, files });
+
+ const existing = this.cache.get(messageId);
+ if (existing) {
+ const clone = existing._clone();
+ clone._patch(d);
+ return clone;
+ }
+ return this._add(d);
+ }
+
+ /**
+ * Publishes a message in an announcement channel to all channels following it, even if it's not cached.
+ * @param {MessageResolvable} message The message to publish
+ * @returns {Promise}
+ */
+ async crosspost(message) {
+ message = this.resolveId(message);
+ if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
+
+ const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post();
+ return this.cache.get(data.id) ?? this._add(data);
+ }
+
+ /**
+ * Pins a message to the channel's pinned messages, even if it's not cached.
+ * @param {MessageResolvable} message The message to pin
+ * @param {string} [reason] Reason for pinning
+ * @returns {Promise}
+ */
+ async pin(message, reason) {
+ message = this.resolveId(message);
+ if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
+
+ await this.client.api.channels(this.channel.id).pins(message).put({ reason });
+ }
+
+ /**
+ * Unpins a message from the channel's pinned messages, even if it's not cached.
+ * @param {MessageResolvable} message The message to unpin
+ * @param {string} [reason] Reason for unpinning
+ * @returns {Promise}
+ */
+ async unpin(message, reason) {
+ message = this.resolveId(message);
+ if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
+
+ await this.client.api.channels(this.channel.id).pins(message).delete({ reason });
+ }
+
+ /**
+ * Adds a reaction to a message, even if it's not cached.
+ * @param {MessageResolvable} message The message to react to
+ * @param {EmojiIdentifierResolvable} emoji The emoji to react with
+ * @returns {Promise}
+ */
+ async react(message, emoji) {
+ message = this.resolveId(message);
+ if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
+
+ emoji = Util.resolvePartialEmoji(emoji);
+ if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable');
+
+ const emojiId = emoji.id
+ ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}`
+ : encodeURIComponent(emoji.name);
+
+ await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put();
+ }
+
+ /**
+ * Deletes a message, even if it's not cached.
+ * @param {MessageResolvable} message The message to delete
+ * @returns {Promise}
+ */
+ async delete(message) {
+ message = this.resolveId(message);
+ if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
+
+ await this.client.api.channels(this.channel.id).messages(message).delete();
+ }
+
+ async _fetchId(messageId, cache, force) {
+ if (!force) {
+ const existing = this.cache.get(messageId);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.channels(this.channel.id).messages(messageId).get();
+ return this._add(data, cache);
+ }
+
+ async _fetchMany(options = {}, cache) {
+ const data = await this.client.api.channels(this.channel.id).messages.get({
+ query: new URLSearchParams(options),
+ });
+ const messages = new Collection();
+ for (const message of data) messages.set(message.id, this._add(message, cache));
+ return messages;
+ }
+}
+
+module.exports = MessageManager;
diff --git a/src/managers/PermissionOverwriteManager.js b/src/managers/PermissionOverwriteManager.js
new file mode 100644
index 00000000..1dfb767
--- /dev/null
+++ b/src/managers/PermissionOverwriteManager.js
@@ -0,0 +1,162 @@
+'use strict';
+
+const process = require('node:process');
+const { Collection } = require('@discordjs/collection');
+const { OverwriteType, Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const PermissionOverwrites = require('../structures/PermissionOverwrites');
+const { Role } = require('../structures/Role');
+
+let cacheWarningEmitted = false;
+
+/**
+ * Manages API methods for guild channel permission overwrites and stores their cache.
+ * @extends {CachedManager}
+ */
+class PermissionOverwriteManager extends CachedManager {
+ constructor(channel, iterable) {
+ super(channel.client, PermissionOverwrites);
+ if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') {
+ cacheWarningEmitted = true;
+ process.emitWarning(
+ `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
+ 'UnsupportedCacheOverwriteWarning',
+ );
+ }
+
+ /**
+ * The channel of the permission overwrite this manager belongs to
+ * @type {GuildChannel}
+ */
+ this.channel = channel;
+
+ if (iterable) {
+ for (const item of iterable) {
+ this._add(item);
+ }
+ }
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name PermissionOverwriteManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache, { extras: [this.channel] });
+ }
+
+ /**
+ * Replaces the permission overwrites in this channel.
+ * @param {OverwriteResolvable[]|Collection} overwrites
+ * Permission overwrites the channel gets updated with
+ * @param {string} [reason] Reason for updating the channel overwrites
+ * @returns {Promise}
+ * @example
+ * message.channel.permissionOverwrites.set([
+ * {
+ * id: message.author.id,
+ * deny: [PermissionsFlagsBit.ViewChannel],
+ * },
+ * ], 'Needed to change permissions');
+ */
+ set(overwrites, reason) {
+ if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) {
+ return Promise.reject(
+ new TypeError('INVALID_TYPE', 'overwrites', 'Array or Collection of Permission Overwrites', true),
+ );
+ }
+ return this.channel.edit({ permissionOverwrites: overwrites, reason });
+ }
+
+ /**
+ * Extra information about the overwrite
+ * @typedef {Object} GuildChannelOverwriteOptions
+ * @property {string} [reason] Reason for creating/editing this overwrite
+ * @property {number} [type] The type of overwrite, either `0` for a role or `1` for a member. Use this to bypass
+ * automatic resolution of type that results in an error for uncached structure
+ */
+
+ /**
+ * Creates or edits permission overwrites for a user or role in this channel.
+ * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
+ * @param {PermissionOverwriteOptions} options The options for the update
+ * @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update
+ * @param {PermissionOverwrites} [existing] The existing overwrites to merge with this update
+ * @returns {Promise}
+ * @private
+ */
+ async upsert(userOrRole, options, overwriteOptions = {}, existing) {
+ let userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
+ let { type, reason } = overwriteOptions;
+ if (typeof type !== 'number') {
+ userOrRole = this.channel.guild.roles.resolve(userOrRole) ?? this.client.users.resolve(userOrRole);
+ if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
+ type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member;
+ }
+
+ const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options, existing);
+
+ await this.client.api.channels(this.channel.id).permissions(userOrRoleId).put({
+ body: { id: userOrRoleId, type, allow, deny },
+ reason,
+ });
+ return this.channel;
+ }
+
+ /**
+ * Creates permission overwrites for a user or role in this channel, or replaces them if already present.
+ * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
+ * @param {PermissionOverwriteOptions} options The options for the update
+ * @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update
+ * @returns {Promise}
+ * @example
+ * // Create or Replace permission overwrites for a message author
+ * message.channel.permissionOverwrites.create(message.author, {
+ * SEND_MESSAGES: false
+ * })
+ * .then(channel => console.log(channel.permissionOverwrites.cache.get(message.author.id)))
+ * .catch(console.error);
+ */
+ create(userOrRole, options, overwriteOptions) {
+ return this.upsert(userOrRole, options, overwriteOptions);
+ }
+
+ /**
+ * Edits permission overwrites for a user or role in this channel, or creates an entry if not already present.
+ * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
+ * @param {PermissionOverwriteOptions} options The options for the update
+ * @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update
+ * @returns {Promise}
+ * @example
+ * // Edit or Create permission overwrites for a message author
+ * message.channel.permissionOverwrites.edit(message.author, {
+ * SEND_MESSAGES: false
+ * })
+ * .then(channel => console.log(channel.permissionOverwrites.cache.get(message.author.id)))
+ * .catch(console.error);
+ */
+ edit(userOrRole, options, overwriteOptions) {
+ userOrRole = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
+ const existing = this.cache.get(userOrRole);
+ return this.upsert(userOrRole, options, overwriteOptions, existing);
+ }
+
+ /**
+ * Deletes permission overwrites for a user or role in this channel.
+ * @param {UserResolvable|RoleResolvable} userOrRole The user or role to delete
+ * @param {string} [reason] The reason for deleting the overwrite
+ * @returns {Promise}
+ */
+ async delete(userOrRole, reason) {
+ const userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
+ if (!userOrRoleId) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
+
+ await this.client.api.channels(this.channel.id).permissions(userOrRoleId).delete({ reason });
+ return this.channel;
+ }
+}
+
+module.exports = PermissionOverwriteManager;
diff --git a/src/managers/PresenceManager.js b/src/managers/PresenceManager.js
new file mode 100644
index 00000000..2d64834
--- /dev/null
+++ b/src/managers/PresenceManager.js
@@ -0,0 +1,58 @@
+'use strict';
+
+const CachedManager = require('./CachedManager');
+const { Presence } = require('../structures/Presence');
+
+/**
+ * Manages API methods for Presences and holds their cache.
+ * @extends {CachedManager}
+ */
+class PresenceManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, Presence, iterable);
+ }
+
+ /**
+ * The cache of Presences
+ * @type {Collection}
+ * @name PresenceManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache, { id: data.user.id });
+ }
+
+ /**
+ * Data that can be resolved to a Presence object. This can be:
+ * * A Presence
+ * * A UserResolvable
+ * * A Snowflake
+ * @typedef {Presence|UserResolvable|Snowflake} PresenceResolvable
+ */
+
+ /**
+ * Resolves a {@link PresenceResolvable} to a {@link Presence} object.
+ * @param {PresenceResolvable} presence The presence resolvable to resolve
+ * @returns {?Presence}
+ */
+ resolve(presence) {
+ const presenceResolvable = super.resolve(presence);
+ if (presenceResolvable) return presenceResolvable;
+ const UserResolvable = this.client.users.resolveId(presence);
+ return super.resolve(UserResolvable);
+ }
+
+ /**
+ * Resolves a {@link PresenceResolvable} to a {@link Presence} id.
+ * @param {PresenceResolvable} presence The presence resolvable to resolve
+ * @returns {?Snowflake}
+ */
+ resolveId(presence) {
+ const presenceResolvable = super.resolveId(presence);
+ if (presenceResolvable) return presenceResolvable;
+ const userResolvable = this.client.users.resolveId(presence);
+ return this.cache.has(userResolvable) ? userResolvable : null;
+ }
+}
+
+module.exports = PresenceManager;
diff --git a/src/managers/ReactionManager.js b/src/managers/ReactionManager.js
new file mode 100644
index 00000000..b30635e
--- /dev/null
+++ b/src/managers/ReactionManager.js
@@ -0,0 +1,67 @@
+'use strict';
+
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const MessageReaction = require('../structures/MessageReaction');
+
+/**
+ * Manages API methods for reactions and holds their cache.
+ * @extends {CachedManager}
+ */
+class ReactionManager extends CachedManager {
+ constructor(message, iterable) {
+ super(message.client, MessageReaction, iterable);
+
+ /**
+ * The message that this manager belongs to
+ * @type {Message}
+ */
+ this.message = message;
+ }
+
+ _add(data, cache) {
+ return super._add(data, cache, { id: data.emoji.id ?? data.emoji.name, extras: [this.message] });
+ }
+
+ /**
+ * The reaction cache of this manager
+ * @type {Collection}
+ * @name ReactionManager#cache
+ */
+
+ /**
+ * Data that can be resolved to a MessageReaction object. This can be:
+ * * A MessageReaction
+ * * A Snowflake
+ * @typedef {MessageReaction|Snowflake} MessageReactionResolvable
+ */
+
+ /**
+ * Resolves a {@link MessageReactionResolvable} to a {@link MessageReaction} object.
+ * @method resolve
+ * @memberof ReactionManager
+ * @instance
+ * @param {MessageReactionResolvable} reaction The MessageReaction to resolve
+ * @returns {?MessageReaction}
+ */
+
+ /**
+ * Resolves a {@link MessageReactionResolvable} to a {@link MessageReaction} id.
+ * @method resolveId
+ * @memberof ReactionManager
+ * @instance
+ * @param {MessageReactionResolvable} reaction The MessageReaction to resolve
+ * @returns {?Snowflake}
+ */
+
+ /**
+ * Removes all reactions from a message.
+ * @returns {Promise}
+ */
+ async removeAll() {
+ await this.client.api.channels(this.message.channelId).messages(this.message.id).reactions.delete();
+ return this.message;
+ }
+}
+
+module.exports = ReactionManager;
diff --git a/src/managers/ReactionUserManager.js b/src/managers/ReactionUserManager.js
new file mode 100644
index 00000000..41f6a03
--- /dev/null
+++ b/src/managers/ReactionUserManager.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { Error } = require('../errors');
+const User = require('../structures/User');
+
+/**
+ * Manages API methods for users who reacted to a reaction and stores their cache.
+ * @extends {CachedManager}
+ */
+class ReactionUserManager extends CachedManager {
+ constructor(reaction, iterable) {
+ super(reaction.client, User, iterable);
+
+ /**
+ * The reaction that this manager belongs to
+ * @type {MessageReaction}
+ */
+ this.reaction = reaction;
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name ReactionUserManager#cache
+ */
+
+ /**
+ * Options used to fetch users who gave a reaction.
+ * @typedef {Object} FetchReactionUsersOptions
+ * @property {number} [limit=100] The maximum amount of users to fetch, defaults to `100`
+ * @property {Snowflake} [after] Limit fetching users to those with an id greater than the supplied id
+ */
+
+ /**
+ * Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their ids.
+ * @param {FetchReactionUsersOptions} [options] Options for fetching the users
+ * @returns {Promise>}
+ */
+ async fetch({ limit = 100, after } = {}) {
+ const message = this.reaction.message;
+ const query = new URLSearchParams({ limit });
+ if (after) {
+ query.set('after', after);
+ }
+ const data = await this.client.api.channels(message.channelId).messages(message.id).reactions(this.reaction.emoji.identifier).get({ query });
+ const users = new Collection();
+ for (const rawUser of data) {
+ const user = this.client.users._add(rawUser);
+ this.cache.set(user.id, user);
+ users.set(user.id, user);
+ }
+ return users;
+ }
+
+ /**
+ * Removes a user from this reaction.
+ * @param {UserResolvable} [user=this.client.user] The user to remove the reaction of
+ * @returns {Promise}
+ */
+ async remove(user = this.client.user) {
+ const userId = this.client.users.resolveId(user);
+ if (!userId) throw new Error('REACTION_RESOLVE_USER');
+ const message = this.reaction.message;
+ await this.client.api.channels[message.channelId]
+ .messages[message.id]
+ .reactions[this.reaction.emoji.identifier][userId === this.client.user.id ? '@me' : userId]
+ .delete();
+ return this.reaction;
+ }
+}
+
+module.exports = ReactionUserManager;
diff --git a/src/managers/RoleManager.js b/src/managers/RoleManager.js
new file mode 100644
index 00000000..1dd64eb
--- /dev/null
+++ b/src/managers/RoleManager.js
@@ -0,0 +1,351 @@
+'use strict';
+
+const process = require('node:process');
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const { Role } = require('../structures/Role');
+const DataResolver = require('../util/DataResolver');
+const PermissionsBitField = require('../util/PermissionsBitField');
+const { resolveColor } = require('../util/Util');
+const Util = require('../util/Util');
+
+let cacheWarningEmitted = false;
+
+/**
+ * Manages API methods for roles and stores their cache.
+ * @extends {CachedManager}
+ */
+class RoleManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, Role, iterable);
+ if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') {
+ cacheWarningEmitted = true;
+ process.emitWarning(
+ `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`,
+ 'UnsupportedCacheOverwriteWarning',
+ );
+ }
+
+ /**
+ * The guild belonging to this manager
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The role cache of this manager
+ * @type {Collection}
+ * @name RoleManager#cache
+ */
+
+ _add(data, cache) {
+ return super._add(data, cache, { extras: [this.guild] });
+ }
+
+ /**
+ * Obtains a role from Discord, or the role cache if they're already available.
+ * @param {Snowflake} [id] The role's id
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise>}
+ * @example
+ * // Fetch all roles from the guild
+ * message.guild.roles.fetch()
+ * .then(roles => console.log(`There are ${roles.size} roles.`))
+ * .catch(console.error);
+ * @example
+ * // Fetch a single role
+ * message.guild.roles.fetch('222078108977594368')
+ * .then(role => console.log(`The role color is: ${role.color}`))
+ * .catch(console.error);
+ */
+ async fetch(id, { cache = true, force = false } = {}) {
+ if (id && !force) {
+ const existing = this.cache.get(id);
+ if (existing) return existing;
+ }
+
+ // We cannot fetch a single role, as of this commit's date, Discord API throws with 405
+ const data = await this.client.api.guilds(this.guild.id).roles.get();
+ const roles = new Collection();
+ for (const role of data) roles.set(role.id, this._add(role, cache));
+ return id ? roles.get(id) ?? null : roles;
+ }
+
+ /**
+ * Data that can be resolved to a Role object. This can be:
+ * * A Role
+ * * A Snowflake
+ * @typedef {Role|Snowflake} RoleResolvable
+ */
+
+ /**
+ * Resolves a {@link RoleResolvable} to a {@link Role} object.
+ * @method resolve
+ * @memberof RoleManager
+ * @instance
+ * @param {RoleResolvable} role The role resolvable to resolve
+ * @returns {?Role}
+ */
+
+ /**
+ * Resolves a {@link RoleResolvable} to a {@link Role} id.
+ * @method resolveId
+ * @memberof RoleManager
+ * @instance
+ * @param {RoleResolvable} role The role resolvable to resolve
+ * @returns {?Snowflake}
+ */
+
+ /**
+ * Options used to create a new role.
+ * @typedef {Object} CreateRoleOptions
+ * @property {string} [name] The name of the new role
+ * @property {ColorResolvable} [color] The data to create the role with
+ * @property {boolean} [hoist] Whether or not the new role should be hoisted
+ * @property {PermissionResolvable} [permissions] The permissions for the new role
+ * @property {number} [position] The position of the new role
+ * @property {boolean} [mentionable] Whether or not the new role should be mentionable
+ * @property {?(BufferResolvable|Base64Resolvable|EmojiResolvable)} [icon] The icon for the role
+ * The `EmojiResolvable` should belong to the same guild as the role.
+ * If not, pass the emoji's URL directly
+ * @property {?string} [unicodeEmoji] The unicode emoji for the role
+ * @property {string} [reason] The reason for creating this role
+ */
+
+ /**
+ * Creates a new role in the guild with given information.
+ * The position will silently reset to 1 if an invalid one is provided, or none.
+ * @param {CreateRoleOptions} [options] Options for creating the new role
+ * @returns {Promise}
+ * @example
+ * // Create a new role
+ * guild.roles.create()
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // Create a new role with data and a reason
+ * guild.roles.create({
+ * name: 'Super Cool Blue People',
+ * color: Colors.Blue,
+ * reason: 'we needed a role for Super Cool People',
+ * })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async create(options = {}) {
+ let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options;
+ color &&= resolveColor(color);
+ if (typeof permissions !== 'undefined') permissions = new PermissionsBitField(permissions);
+ if (icon) {
+ const guildEmojiURL = this.guild.emojis.resolve(icon)?.url;
+ icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon);
+ if (typeof icon !== 'string') icon = undefined;
+ }
+
+ const data = await this.client.api.guilds(this.guild.id).roles.post({
+ body: {
+ name,
+ color,
+ hoist,
+ permissions,
+ mentionable,
+ icon,
+ unicode_emoji: unicodeEmoji,
+ },
+ reason,
+ })
+ const { role } = this.client.actions.GuildRoleCreate.handle({
+ guild_id: this.guild.id,
+ role: data,
+ });
+ if (position) return this.setPosition(role, position, { reason });
+ return role;
+ }
+
+ /**
+ * Edits a role of the guild.
+ * @param {RoleResolvable} role The role to edit
+ * @param {RoleData} data The new data for the role
+ * @param {string} [reason] Reason for editing this role
+ * @returns {Promise}
+ * @example
+ * // Edit a role
+ * guild.roles.edit('222079219327434752', { name: 'buddies' })
+ * .then(updated => console.log(`Edited role name to ${updated.name}`))
+ * .catch(console.error);
+ */
+ async edit(role, data, reason) {
+ role = this.resolve(role);
+ if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
+
+ if (typeof data.position === 'number') {
+ await this.setPosition(role, data.position, { reason });
+ }
+
+ let icon = data.icon;
+ if (icon) {
+ const guildEmojiURL = this.guild.emojis.resolve(icon)?.url;
+ icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon);
+ if (typeof icon !== 'string') icon = undefined;
+ }
+
+ const body = {
+ name: data.name,
+ color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color),
+ hoist: data.hoist,
+ permissions: typeof data.permissions === 'undefined' ? undefined : new PermissionsBitField(data.permissions),
+ mentionable: data.mentionable,
+ icon,
+ unicode_emoji: data.unicodeEmoji,
+ };
+
+ const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ body, reason });
+
+ const clone = role._clone();
+ clone._patch(d);
+ return clone;
+ }
+
+ /**
+ * Deletes a role.
+ * @param {RoleResolvable} role The role to delete
+ * @param {string} [reason] Reason for deleting the role
+ * @returns {Promise}
+ * @example
+ * // Delete a role
+ * guild.roles.delete('222079219327434752', 'The role needed to go')
+ * .then(() => console.log('Deleted the role'))
+ * .catch(console.error);
+ */
+ async delete(role, reason) {
+ const id = this.resolveId(role);
+ await this.client.api.guilds(this.guild.id).roles(id).delete({ reason });
+ this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id });
+ }
+
+ /**
+ * Sets the new position of the role.
+ * @param {RoleResolvable} role The role to change the position of
+ * @param {number} position The new position for the role
+ * @param {SetRolePositionOptions} [options] Options for setting the position
+ * @returns {Promise}
+ * @example
+ * // Set the position of the role
+ * guild.roles.setPosition('222197033908436994', 1)
+ * .then(updated => console.log(`Role position: ${updated.position}`))
+ * .catch(console.error);
+ */
+ async setPosition(role, position, { relative, reason } = {}) {
+ role = this.resolve(role);
+ if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
+ const updatedRoles = await Util.setPosition(
+ role,
+ position,
+ relative,
+ this.guild._sortedRoles(),
+ this.client,
+ Routes.guildRoles(this.guild.id),
+ reason,
+ );
+
+ this.client.actions.GuildRolesPositionUpdate.handle({
+ guild_id: this.guild.id,
+ roles: updatedRoles,
+ });
+ return role;
+ }
+
+ /**
+ * The data needed for updating a guild role's position
+ * @typedef {Object} GuildRolePosition
+ * @property {RoleResolvable} role The role's id
+ * @property {number} position The position to update
+ */
+
+ /**
+ * Batch-updates the guild's role positions
+ * @param {GuildRolePosition[]} rolePositions Role positions to update
+ * @returns {Promise}
+ * @example
+ * guild.roles.setPositions([{ role: roleId, position: updatedRoleIndex }])
+ * .then(guild => console.log(`Role positions updated for ${guild}`))
+ * .catch(console.error);
+ */
+ async setPositions(rolePositions) {
+ // Make sure rolePositions are prepared for API
+ rolePositions = rolePositions.map(o => ({
+ id: this.resolveId(o.role),
+ position: o.position,
+ }));
+
+ // Call the API to update role positions
+ await this.client.api.guilds(this.guild.id).roles.patch({ body: rolePositions });
+ return this.client.actions.GuildRolesPositionUpdate.handle({
+ guild_id: this.guild.id,
+ roles: rolePositions,
+ }).guild;
+ }
+
+ /**
+ * Compares the positions of two roles.
+ * @param {RoleResolvable} role1 First role to compare
+ * @param {RoleResolvable} role2 Second role to compare
+ * @returns {number} Negative number if the first role's position is lower (second role's is higher),
+ * positive number if the first's is higher (second's is lower), 0 if equal
+ */
+ comparePositions(role1, role2) {
+ const resolvedRole1 = this.resolve(role1);
+ const resolvedRole2 = this.resolve(role2);
+ if (!resolvedRole1 || !resolvedRole2) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake');
+
+ if (resolvedRole1.position === resolvedRole2.position) {
+ return Number(BigInt(resolvedRole2.id) - BigInt(resolvedRole1.id));
+ }
+
+ return resolvedRole1.position - resolvedRole2.position;
+ }
+
+ /**
+ * Gets the managed role a user created when joining the guild, if any
+ * Only ever available for bots
+ * @param {UserResolvable} user The user to access the bot role for
+ * @returns {?Role}
+ */
+ botRoleFor(user) {
+ const userId = this.client.users.resolveId(user);
+ if (!userId) return null;
+ return this.cache.find(role => role.tags?.botId === userId) ?? null;
+ }
+
+ /**
+ * The `@everyone` role of the guild
+ * @type {Role}
+ * @readonly
+ */
+ get everyone() {
+ return this.cache.get(this.guild.id);
+ }
+
+ /**
+ * The premium subscriber role of the guild, if any
+ * @type {?Role}
+ * @readonly
+ */
+ get premiumSubscriberRole() {
+ return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null;
+ }
+
+ /**
+ * The role with the highest position in the cache
+ * @type {Role}
+ * @readonly
+ */
+ get highest() {
+ return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first());
+ }
+}
+
+module.exports = RoleManager;
diff --git a/src/managers/StageInstanceManager.js b/src/managers/StageInstanceManager.js
new file mode 100644
index 00000000..68f1f77
--- /dev/null
+++ b/src/managers/StageInstanceManager.js
@@ -0,0 +1,152 @@
+'use strict';
+
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError, Error } = require('../errors');
+const { StageInstance } = require('../structures/StageInstance');
+
+/**
+ * Manages API methods for {@link StageInstance} objects and holds their cache.
+ * @extends {CachedManager}
+ */
+class StageInstanceManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, StageInstance, iterable);
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name StageInstanceManager#cache
+ */
+
+ /**
+ * Options used to create a stage instance.
+ * @typedef {Object} StageInstanceCreateOptions
+ * @property {string} topic The topic of the stage instance
+ * @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance
+ */
+
+ /**
+ * Data that can be resolved to a Stage Channel object. This can be:
+ * * A StageChannel
+ * * A Snowflake
+ * @typedef {StageChannel|Snowflake} StageChannelResolvable
+ */
+
+ /**
+ * Creates a new stage instance.
+ * @param {StageChannelResolvable} channel The stage channel to associate the created stage instance to
+ * @param {StageInstanceCreateOptions} options The options to create the stage instance
+ * @returns {Promise}
+ * @example
+ * // Create a stage instance
+ * guild.stageInstances.create('1234567890123456789', {
+ * topic: 'A very creative topic',
+ * privacyLevel: GuildPrivacyLevel.GuildOnly
+ * })
+ * .then(stageInstance => console.log(stageInstance))
+ * .catch(console.error);
+ */
+ async create(channel, options) {
+ const channelId = this.guild.channels.resolveId(channel);
+ if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
+ if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ let { topic, privacyLevel } = options;
+
+ const data = await this.client.api['stage-instances'].post({
+ body: {
+ channel_id: channelId,
+ topic,
+ privacy_level: privacyLevel,
+ },
+ });
+
+ return this._add(data);
+ }
+
+ /**
+ * Fetches the stage instance associated with a stage channel, if it exists.
+ * @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be fetched
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ * @example
+ * // Fetch a stage instance
+ * guild.stageInstances.fetch('1234567890123456789')
+ * .then(stageInstance => console.log(stageInstance))
+ * .catch(console.error);
+ */
+ async fetch(channel, { cache = true, force = false } = {}) {
+ const channelId = this.guild.channels.resolveId(channel);
+ if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
+
+ if (!force) {
+ const existing = this.cache.find(stageInstance => stageInstance.channelId === channelId);
+ if (existing) return existing;
+ }
+
+ const data = await this.client.api('stage-instances', channelId).get();
+ return this._add(data, cache);
+ }
+
+ /**
+ * Options used to edit an existing stage instance.
+ * @typedef {Object} StageInstanceEditOptions
+ * @property {string} [topic] The new topic of the stage instance
+ * @property {PrivacyLevel|number} [privacyLevel] The new privacy level of the stage instance
+ */
+
+ /**
+ * Edits an existing stage instance.
+ * @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be edited
+ * @param {StageInstanceEditOptions} options The options to edit the stage instance
+ * @returns {Promise}
+ * @example
+ * // Edit a stage instance
+ * guild.stageInstances.edit('1234567890123456789', { topic: 'new topic' })
+ * .then(stageInstance => console.log(stageInstance))
+ * .catch(console.error);
+ */
+ async edit(channel, options) {
+ if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
+ const channelId = this.guild.channels.resolveId(channel);
+ if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
+
+ let { topic, privacyLevel } = options;
+
+ const data = await this.client.api('stage-instances', channelId).patch({
+ body: {
+ topic,
+ privacy_level: privacyLevel,
+ },
+ })
+
+ if (this.cache.has(data.id)) {
+ const clone = this.cache.get(data.id)._clone();
+ clone._patch(data);
+ return clone;
+ }
+
+ return this._add(data);
+ }
+
+ /**
+ * Deletes an existing stage instance.
+ * @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be deleted
+ * @returns {Promise}
+ */
+ async delete(channel) {
+ const channelId = this.guild.channels.resolveId(channel);
+ if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
+
+ await this.client.api('stage-instances', channelId).delete();
+ }
+}
+
+module.exports = StageInstanceManager;
diff --git a/src/managers/ThreadManager.js b/src/managers/ThreadManager.js
new file mode 100644
index 00000000..34cd12e
--- /dev/null
+++ b/src/managers/ThreadManager.js
@@ -0,0 +1,267 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { ChannelType, Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const ThreadChannel = require('../structures/ThreadChannel');
+
+/**
+ * Manages API methods for {@link ThreadChannel} objects and stores their cache.
+ * @extends {CachedManager}
+ */
+class ThreadManager extends CachedManager {
+ constructor(channel, iterable) {
+ super(channel.client, ThreadChannel, iterable);
+
+ /**
+ * The channel this Manager belongs to
+ * @type {NewsChannel|TextChannel}
+ */
+ this.channel = channel;
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name ThreadManager#cache
+ */
+
+ _add(thread) {
+ const existing = this.cache.get(thread.id);
+ if (existing) return existing;
+ this.cache.set(thread.id, thread);
+ return thread;
+ }
+
+ /**
+ * Data that can be resolved to a Thread Channel object. This can be:
+ * * A ThreadChannel object
+ * * A Snowflake
+ * @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable
+ */
+
+ /**
+ * Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} object.
+ * @method resolve
+ * @memberof ThreadManager
+ * @instance
+ * @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
+ * @returns {?ThreadChannel}
+ */
+
+ /**
+ * Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} id.
+ * @method resolveId
+ * @memberof ThreadManager
+ * @instance
+ * @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
+ * @returns {?Snowflake}
+ */
+
+ /**
+ * Options for creating a thread. Only one of `startMessage` or `type` can be defined.
+ * @typedef {StartThreadOptions} ThreadCreateOptions
+ * @property {MessageResolvable} [startMessage] The message to start a thread from. If this is defined then type
+ * of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored
+ * @property {ThreadChannelTypes|number} [type] The type of thread to create.
+ * Defaults to {@link ChannelType.GuildPublicThread} if created in a {@link TextChannel}
+ * When creating threads in a {@link NewsChannel} this is ignored and is always
+ * {@link ChannelType.GuildNewsThread}
+ * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
+ * Can only be set when type will be {@link ChannelType.GuildPrivateThread}
+ */
+
+ /**
+ * Creates a new thread in the channel.
+ * @param {ThreadCreateOptions} [options] Options to create a new thread
+ * @returns {Promise}
+ * @example
+ * // Create a new public thread
+ * channel.threads
+ * .create({
+ * name: 'food-talk',
+ * autoArchiveDuration: 60,
+ * reason: 'Needed a separate thread for food',
+ * })
+ * .then(threadChannel => console.log(threadChannel))
+ * .catch(console.error);
+ * @example
+ * // Create a new private thread
+ * channel.threads
+ * .create({
+ * name: 'mod-talk',
+ * autoArchiveDuration: 60,
+ * type: ChannelType.GuildPrivateThread,
+ * reason: 'Needed a separate thread for moderation',
+ * })
+ * .then(threadChannel => console.log(threadChannel))
+ * .catch(console.error);
+ */
+ async create({
+ name,
+ autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
+ startMessage,
+ type,
+ invitable,
+ reason,
+ rateLimitPerUser,
+ } = {}) {
+ if (type && typeof type !== 'string' && typeof type !== 'number') {
+ throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
+ }
+ let resolvedType =
+ this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread;
+ let startMessageId;
+ if (startMessage) {
+ startMessageId = this.channel.messages.resolveId(startMessage);
+ if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
+ } else if (this.channel.type !== ChannelType.GuildNews) {
+ resolvedType = type ?? resolvedType;
+ }
+ if (autoArchiveDuration === 'MAX') {
+ autoArchiveDuration = 1440;
+ if (this.channel.guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE')) {
+ autoArchiveDuration = 10080;
+ } else if (this.channel.guild.features.includes('THREE_DAY_THREAD_ARCHIVE')) {
+ autoArchiveDuration = 4320;
+ }
+ }
+
+ const data = await this.client.api.channels(this.channel.id).messages(startMessageId).threads.post({
+ body: {
+ name,
+ auto_archive_duration: autoArchiveDuration,
+ type: resolvedType,
+ invitable: resolvedType === ChannelType.GuildPrivateThread ? invitable : undefined,
+ rate_limit_per_user: rateLimitPerUser,
+ },
+ reason,
+ });
+
+ return this.client.actions.ThreadCreate.handle(data).thread;
+ }
+
+ /**
+ * The options for fetching multiple threads, the properties are mutually exclusive
+ * @typedef {Object} FetchThreadsOptions
+ * @property {FetchArchivedThreadOptions} [archived] The options used to fetch archived threads
+ * @property {boolean} [active] When true, fetches active threads. If `archived` is set, this is ignored!
+ */
+
+ /**
+ * Obtains a thread from Discord, or the channel cache if it's already available.
+ * @param {ThreadChannelResolvable|FetchThreadsOptions} [options] The options to fetch threads. If it is a
+ * ThreadChannelResolvable then the specified thread will be fetched. Fetches all active threads if `undefined`
+ * @param {BaseFetchOptions} [cacheOptions] Additional options for this fetch. The `force` field gets ignored
+ * if `options` is not a {@link ThreadChannelResolvable}
+ * @returns {Promise(ThreadChannel|FetchedThreads)>}
+ * @example
+ * // Fetch a thread by its id
+ * channel.threads.fetch('831955138126104859')
+ * .then(channel => console.log(channel.name))
+ * .catch(console.error);
+ */
+ fetch(options, { cache = true, force = false } = {}) {
+ if (!options) return this.fetchActive(cache);
+ const channel = this.client.channels.resolveId(options);
+ if (channel) return this.client.channels.fetch(channel, cache, force);
+ if (options.archived) {
+ return this.fetchArchived(options.archived, cache);
+ }
+ return this.fetchActive(cache);
+ }
+
+ /**
+ * Data that can be resolved to a Date object. This can be:
+ * * A Date object
+ * * A number representing a timestamp
+ * * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string
+ * @typedef {Date|number|string} DateResolvable
+ */
+
+ /**
+ * The options used to fetch archived threads.
+ * @typedef {Object} FetchArchivedThreadOptions
+ * @property {string} [type='public'] The type of threads to fetch, either `public` or `private`
+ * @property {boolean} [fetchAll=false] Whether to fetch **all** archived threads when type is `private`.
+ * Requires `MANAGE_THREADS` if true
+ * @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were created before this Date
+ * or Snowflake. Must be a {@link ThreadChannelResolvable} when type is `private` and fetchAll is `false`
+ * @property {number} [limit] Maximum number of threads to return
+ */
+
+ /**
+ * The data returned from a thread fetch that returns multiple threads.
+ * @typedef {Object} FetchedThreads
+ * @property {Collection} threads The threads that were fetched, with any members returned
+ * @property {?boolean} hasMore Whether there are potentially additional threads that require a subsequent call
+ */
+
+ /**
+ * Obtains a set of archived threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel.
+ * @param {FetchArchivedThreadOptions} [options] The options to fetch archived threads
+ * @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
+ * @returns {Promise}
+ */
+ async fetchArchived({ type = 'public', fetchAll = false, before, limit } = {}, cache = true) {
+ let path = this.client.api.channels(this.channel.id);
+ if (type === 'private' && !fetchAll) {
+ path = path.users('@me');
+ }
+ let timestamp;
+ let id;
+ const query = new URLSearchParams();
+ if (typeof before !== 'undefined') {
+ if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
+ id = this.resolveId(before);
+ timestamp = this.resolve(before)?.archivedAt?.toISOString();
+ const toUse = type === 'private' && !fetchAll ? id : timestamp;
+ if (toUse) {
+ query.set('before', toUse);
+ }
+ } else {
+ try {
+ timestamp = new Date(before).toISOString();
+ if (type === 'public' || fetchAll) {
+ query.set('before', timestamp);
+ }
+ } catch {
+ throw new TypeError('INVALID_TYPE', 'before', 'DateResolvable or ThreadChannelResolvable');
+ }
+ }
+ }
+
+ if (limit) {
+ query.set('limit', limit);
+ }
+ const raw = await path.threads.archived(type).get({ query });
+ return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
+ }
+
+ /**
+ * Obtains the accessible active threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel.
+ * @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
+ * @returns {Promise}
+ */
+ async fetchActive(cache = true) {
+ const raw = await this.client.api.guilds(this.channel.guild.id).threads.active.get();
+ return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
+ }
+
+ static _mapThreads(rawThreads, client, { parent, guild, cache }) {
+ const threads = rawThreads.threads.reduce((coll, raw) => {
+ const thread = client.channels._add(raw, guild ?? parent?.guild, { cache });
+ if (parent && thread.parentId !== parent.id) return coll;
+ return coll.set(thread.id, thread);
+ }, new Collection());
+ // Discord sends the thread id as id in this object
+ for (const rawMember of rawThreads.members) client.channels.cache.get(rawMember.id)?.members._add(rawMember);
+ return {
+ threads,
+ hasMore: rawThreads.has_more ?? false,
+ };
+ }
+}
+
+module.exports = ThreadManager;
diff --git a/src/managers/ThreadMemberManager.js b/src/managers/ThreadMemberManager.js
new file mode 100644
index 00000000..366894d
--- /dev/null
+++ b/src/managers/ThreadMemberManager.js
@@ -0,0 +1,128 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { TypeError } = require('../errors');
+const ThreadMember = require('../structures/ThreadMember');
+
+/**
+ * Manages API methods for GuildMembers and stores their cache.
+ * @extends {CachedManager}
+ */
+class ThreadMemberManager extends CachedManager {
+ constructor(thread, iterable) {
+ super(thread.client, ThreadMember, iterable);
+
+ /**
+ * The thread this manager belongs to
+ * @type {ThreadChannel}
+ */
+ this.thread = thread;
+ }
+
+ /**
+ * The cache of this Manager
+ * @type {Collection}
+ * @name ThreadMemberManager#cache
+ */
+
+ _add(data, cache = true) {
+ const existing = this.cache.get(data.user_id);
+ if (cache) existing?._patch(data);
+ if (existing) return existing;
+
+ const member = new ThreadMember(this.thread, data);
+ if (cache) this.cache.set(data.user_id, member);
+ return member;
+ }
+
+ /**
+ * Data that resolves to give a ThreadMember object. This can be:
+ * * A ThreadMember object
+ * * A User resolvable
+ * @typedef {ThreadMember|UserResolvable} ThreadMemberResolvable
+ */
+
+ /**
+ * Resolves a {@link ThreadMemberResolvable} to a {@link ThreadMember} object.
+ * @param {ThreadMemberResolvable} member The user that is part of the thread
+ * @returns {?GuildMember}
+ */
+ resolve(member) {
+ const memberResolvable = super.resolve(member);
+ if (memberResolvable) return memberResolvable;
+ const userResolvable = this.client.users.resolveId(member);
+ if (userResolvable) return super.resolve(userResolvable);
+ return null;
+ }
+
+ /**
+ * Resolves a {@link ThreadMemberResolvable} to a {@link ThreadMember} id string.
+ * @param {ThreadMemberResolvable} member The user that is part of the guild
+ * @returns {?Snowflake}
+ */
+ resolveId(member) {
+ const memberResolvable = super.resolveId(member);
+ if (memberResolvable) return memberResolvable;
+ const userResolvable = this.client.users.resolveId(member);
+ return this.cache.has(userResolvable) ? userResolvable : null;
+ }
+
+ /**
+ * Adds a member to the thread.
+ * @param {UserResolvable|'@me'} member The member to add
+ * @param {string} [reason] The reason for adding this member
+ * @returns {Promise}
+ */
+ async add(member, reason) {
+ const id = member === '@me' ? member : this.client.users.resolveId(member);
+ if (!id) throw new TypeError('INVALID_TYPE', 'member', 'UserResolvable');
+ await this.client.api.channels(this.thread.id, 'thread-members', id).put({ reason });
+ return id;
+ }
+
+ /**
+ * Remove a user from the thread.
+ * @param {Snowflake|'@me'} id The id of the member to remove
+ * @param {string} [reason] The reason for removing this member from the thread
+ * @returns {Promise}
+ */
+ async remove(id, reason) {
+ await this.client.api.channels(this.thread.id, 'thread-members', id).delete({ reason });
+ return id;
+ }
+
+ async _fetchOne(memberId, cache, force) {
+ if (!force) {
+ const existing = this.cache.get(memberId);
+ if (existing) return existing;
+ }
+
+ const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get();
+ return this._add(data, cache);
+ }
+
+ async _fetchMany(cache) {
+ const raw = await this.client.api.channels(this.thread.id, 'thread-members');
+ return raw.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection());
+ }
+
+ /**
+ * @typedef {BaseFetchOptions} ThreadMemberFetchOptions
+ * @property {UserResolvable} [member] The specific user to fetch from the thread
+ */
+
+ /**
+ * Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent.
+ * @param {ThreadMemberFetchOptions|boolean} [options] Additional options for this fetch, when a `boolean` is provided
+ * all members are fetched with `options.cache` set to the boolean value
+ * @returns {Promise>}
+ */
+ fetch({ member, cache = true, force = false } = {}) {
+ const id = this.resolveId(member);
+ return id ? this._fetchOne(id, cache, force) : this._fetchMany(member ?? cache);
+ }
+}
+
+module.exports = ThreadMemberManager;
diff --git a/src/managers/UserManager.js b/src/managers/UserManager.js
new file mode 100644
index 00000000..fc6b8e9
--- /dev/null
+++ b/src/managers/UserManager.js
@@ -0,0 +1,140 @@
+'use strict';
+
+const { ChannelType, Routes } = require('discord-api-types/v9');
+const CachedManager = require('./CachedManager');
+const { GuildMember } = require('../structures/GuildMember');
+const { Message } = require('../structures/Message');
+const ThreadMember = require('../structures/ThreadMember');
+const User = require('../structures/User');
+
+/**
+ * Manages API methods for users and stores their cache.
+ * @extends {CachedManager}
+ */
+class UserManager extends CachedManager {
+ constructor(client, iterable) {
+ super(client, User, iterable);
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name UserManager#cache
+ */
+
+ /**
+ * Data that resolves to give a User object. This can be:
+ * * A User object
+ * * A Snowflake
+ * * A Message object (resolves to the message author)
+ * * A GuildMember object
+ * * A ThreadMember object
+ * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
+ */
+
+ /**
+ * The DM between the client's user and a user
+ * @param {Snowflake} userId The user id
+ * @returns {?DMChannel}
+ * @private
+ */
+ dmChannel(userId) {
+ return this.client.channels.cache.find(c => c.type === ChannelType.DM && c.recipient.id === userId) ?? null;
+ }
+
+ /**
+ * Creates a {@link DMChannel} between the client and a user.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ */
+ async createDM(user, { cache = true, force = false } = {}) {
+ const id = this.resolveId(user);
+
+ if (!force) {
+ const dmChannel = this.dmChannel(id);
+ if (dmChannel && !dmChannel.partial) return dmChannel;
+ }
+
+ const data = await this.client.api.users('@me').channels.post({ body: { recipient_id: id } });
+ return this.client.channels._add(data, null, { cache });
+ }
+
+ /**
+ * Deletes a {@link DMChannel} (if one exists) between the client and a user. Resolves with the channel if successful.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {Promise}
+ */
+ async deleteDM(user) {
+ const id = this.resolveId(user);
+ const dmChannel = this.dmChannel(id);
+ if (!dmChannel) throw new Error('USER_NO_DM_CHANNEL');
+ await this.client.channels(dmChannel.id).delete();
+ this.client.channels._remove(dmChannel.id);
+ return dmChannel;
+ }
+
+ /**
+ * Obtains a user from Discord, or the user cache if it's already available.
+ * @param {UserResolvable} user The user to fetch
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ */
+ async fetch(user, { cache = true, force = false } = {}) {
+ const id = this.resolveId(user);
+ if (!force) {
+ const existing = this.cache.get(id);
+ if (existing && !existing.partial) return existing;
+ }
+
+ const data = await this.client.api.users(id).get();
+ const userObject = this._add(data, cache);
+ if(!this.user.bot) await userObject.getProfile().catch(() => {});
+ return userObject;
+ }
+
+ /**
+ * Fetches a user's flags.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @param {BaseFetchOptions} [options] Additional options for this fetch
+ * @returns {Promise}
+ */
+ async fetchFlags(user, options) {
+ return (await this.fetch(user, options)).flags;
+ }
+
+ /**
+ * Sends a message to a user.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @param {string|MessagePayload|MessageOptions} options The options to provide
+ * @returns {Promise}
+ */
+ async send(user, options) {
+ return (await this.createDM(user)).send(options);
+ }
+
+ /**
+ * Resolves a {@link UserResolvable} to a {@link User} object.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?User}
+ */
+ resolve(user) {
+ if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
+ if (user instanceof Message) return user.author;
+ return super.resolve(user);
+ }
+
+ /**
+ * Resolves a {@link UserResolvable} to a {@link User} id.
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?Snowflake}
+ */
+ resolveId(user) {
+ if (user instanceof ThreadMember) return user.id;
+ if (user instanceof GuildMember) return user.user.id;
+ if (user instanceof Message) return user.author.id;
+ return super.resolveId(user);
+ }
+}
+
+module.exports = UserManager;
diff --git a/src/managers/VoiceStateManager.js b/src/managers/VoiceStateManager.js
new file mode 100644
index 00000000..c42fdd2
--- /dev/null
+++ b/src/managers/VoiceStateManager.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const CachedManager = require('./CachedManager');
+const VoiceState = require('../structures/VoiceState');
+
+/**
+ * Manages API methods for VoiceStates and stores their cache.
+ * @extends {CachedManager}
+ */
+class VoiceStateManager extends CachedManager {
+ constructor(guild, iterable) {
+ super(guild.client, VoiceState, iterable);
+
+ /**
+ * The guild this manager belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ }
+
+ /**
+ * The cache of this manager
+ * @type {Collection}
+ * @name VoiceStateManager#cache
+ */
+
+ _add(data, cache = true) {
+ const existing = this.cache.get(data.user_id);
+ if (existing) return existing._patch(data);
+
+ const entry = new this.holds(this.guild, data);
+ if (cache) this.cache.set(data.user_id, entry);
+ return entry;
+ }
+}
+
+module.exports = VoiceStateManager;
diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js
new file mode 100644
index 00000000..beeeb50
--- /dev/null
+++ b/src/rest/APIRequest.js
@@ -0,0 +1,80 @@
+'use strict';
+
+const https = require('node:https');
+const { setTimeout } = require('node:timers');
+const FormData = require('form-data');
+const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
+const { UserAgent } = require('../util/Constants');
+
+let agent = null;
+
+class APIRequest {
+ constructor(rest, method, path, options) {
+ this.rest = rest;
+ this.client = rest.client;
+ this.method = method;
+ this.route = options.route;
+ this.options = options;
+ this.retries = 0;
+
+ let queryString = '';
+ if (options.query) {
+ const query = Object.entries(options.query)
+ .filter(([, value]) => value !== null && typeof value !== 'undefined')
+ .flatMap(([key, value]) => (Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]));
+ queryString = new URLSearchParams(query).toString();
+ }
+ this.path = `${path}${queryString && `?${queryString}`}`;
+ }
+
+ async make() {
+ agent ??= new https.Agent({ ...this.client.options.http.agent, keepAlive: true });
+
+ const API =
+ this.options.versioned === false
+ ? this.client.options.http.api
+ : `${this.client.options.http.api}/v${this.client.options.http.version}`;
+ const url = API + this.path;
+
+ let headers = {
+ ...this.client.options.http.headers,
+ 'User-Agent': UserAgent,
+ };
+
+ if (this.options.auth !== false) headers.Authorization = this.rest.getAuth();
+ if (this.options.reason) headers['X-Audit-Log-Reason'] = encodeURIComponent(this.options.reason);
+ if (this.options.headers) headers = Object.assign(headers, this.options.headers);
+
+ let body;
+ if (this.options.files?.length) {
+ body = new FormData();
+ for (const [index, file] of this.options.files.entries()) {
+ if (file?.file) body.append(file.key ?? `files[${index}]`, file.file, file.name);
+ }
+ if (typeof this.options.data !== 'undefined') {
+ if (this.options.dontUsePayloadJSON) {
+ for (const [key, value] of Object.entries(this.options.data)) body.append(key, value);
+ } else {
+ body.append('payload_json', JSON.stringify(this.options.data));
+ }
+ }
+ headers = Object.assign(headers, body.getHeaders());
+ // eslint-disable-next-line eqeqeq
+ } else if (this.options.data != null) {
+ body = JSON.stringify(this.options.data);
+ headers['Content-Type'] = 'application/json';
+ }
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref();
+ return fetch(url, {
+ method: this.method,
+ headers,
+ agent,
+ body,
+ signal: controller.signal,
+ }).finally(() => clearTimeout(timeout));
+ }
+}
+
+module.exports = APIRequest;
\ No newline at end of file
diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js
new file mode 100644
index 00000000..6923a30
--- /dev/null
+++ b/src/rest/APIRouter.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const noop = () => {}; // eslint-disable-line no-empty-function
+const methods = ['get', 'post', 'delete', 'patch', 'put'];
+const reflectors = [
+ 'toString',
+ 'valueOf',
+ 'inspect',
+ 'constructor',
+ Symbol.toPrimitive,
+ Symbol.for('nodejs.util.inspect.custom'),
+];
+
+function buildRoute(manager) {
+ const route = [''];
+ const handler = {
+ get(target, name) {
+ if (reflectors.includes(name)) return () => route.join('/');
+ if (methods.includes(name)) {
+ const routeBucket = [];
+ for (let i = 0; i < route.length; i++) {
+ // Reactions routes and sub-routes all share the same bucket
+ if (route[i - 1] === 'reactions') break;
+ // Literal ids should only be taken account if they are the Major id (the Channel/Guild id)
+ if (/\d{16,19}/g.test(route[i]) && !/channels|guilds/.test(route[i - 1])) routeBucket.push(':id');
+ // All other parts of the route should be considered as part of the bucket identifier
+ else routeBucket.push(route[i]);
+ }
+ return options =>
+ manager.request(
+ name,
+ route.join('/'),
+ Object.assign(
+ {
+ versioned: manager.versioned,
+ route: routeBucket.join('/'),
+ },
+ options,
+ ),
+ );
+ }
+ route.push(name);
+ return new Proxy(noop, handler);
+ },
+ apply(target, _, args) {
+ route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq
+ return new Proxy(noop, handler);
+ },
+ };
+ return new Proxy(noop, handler);
+}
+
+module.exports = buildRoute;
\ No newline at end of file
diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js
new file mode 100644
index 00000000..4e75562
--- /dev/null
+++ b/src/rest/DiscordAPIError.js
@@ -0,0 +1,82 @@
+'use strict';
+
+/**
+ * Represents an error from the Discord API.
+ * @extends Error
+ */
+class DiscordAPIError extends Error {
+ constructor(error, status, request) {
+ super();
+ const flattened = this.constructor.flattenErrors(error.errors ?? error).join('\n');
+ this.name = 'DiscordAPIError';
+ this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message ?? flattened;
+
+ /**
+ * The HTTP method used for the request
+ * @type {string}
+ */
+ this.method = request.method;
+
+ /**
+ * The path of the request relative to the HTTP endpoint
+ * @type {string}
+ */
+ this.path = request.path;
+
+ /**
+ * HTTP error code returned by Discord
+ * @type {number}
+ */
+ this.code = error.code;
+
+ /**
+ * The HTTP status code
+ * @type {number}
+ */
+ this.httpStatus = status;
+
+ /**
+ * The data associated with the request that caused this error
+ * @type {HTTPErrorData}
+ */
+ this.requestData = {
+ json: request.options.data,
+ files: request.options.files ?? [],
+ };
+ }
+
+ /**
+ * Flattens an errors object returned from the API into an array.
+ * @param {APIError} obj Discord errors object
+ * @param {string} [key] Used internally to determine key names of nested fields
+ * @returns {string[]}
+ * @private
+ */
+ static flattenErrors(obj, key = '') {
+ let messages = [];
+
+ for (const [k, v] of Object.entries(obj)) {
+ if (k === 'message') continue;
+ const newKey = key ? (isNaN(k) ? `${key}.${k}` : `${key}[${k}]`) : k;
+
+ if (v._errors) {
+ messages.push(`${newKey}: ${v._errors.map(e => e.message).join(' ')}`);
+ } else if (v.code ?? v.message) {
+ messages.push(`${v.code ? `${v.code}: ` : ''}${v.message}`.trim());
+ } else if (typeof v === 'string') {
+ messages.push(v);
+ } else {
+ messages = messages.concat(this.flattenErrors(v, newKey));
+ }
+ }
+
+ return messages;
+ }
+}
+
+module.exports = DiscordAPIError;
+
+/**
+ * @external APIError
+ * @see {@link https://discord.com/developers/docs/reference#error-messages}
+ */
\ No newline at end of file
diff --git a/src/rest/HTTPError.js b/src/rest/HTTPError.js
new file mode 100644
index 00000000..cec44e9
--- /dev/null
+++ b/src/rest/HTTPError.js
@@ -0,0 +1,61 @@
+'use strict';
+
+/**
+ * Represents an HTTP error from a request.
+ * @extends Error
+ */
+class HTTPError extends Error {
+ constructor(message, name, code, request) {
+ super(message);
+
+ /**
+ * The name of the error
+ * @type {string}
+ */
+ this.name = name;
+
+ /**
+ * HTTP error code returned from the request
+ * @type {number}
+ */
+ this.code = code ?? 500;
+
+ /**
+ * The HTTP method used for the request
+ * @type {string}
+ */
+ this.method = request.method;
+
+ /**
+ * The path of the request relative to the HTTP endpoint
+ * @type {string}
+ */
+ this.path = request.path;
+
+ /**
+ * The HTTP data that was sent to Discord
+ * @typedef {Object} HTTPErrorData
+ * @property {*} json The JSON data that was sent
+ * @property {HTTPAttachmentData[]} files The files that were sent with this request, if any
+ */
+
+ /**
+ * The attachment data that is sent to Discord
+ * @typedef {Object} HTTPAttachmentData
+ * @property {string|Buffer|Stream} attachment The source of this attachment data
+ * @property {string} name The file name
+ * @property {Buffer|Stream} file The file buffer
+ */
+
+ /**
+ * The data associated with the request that caused this error
+ * @type {HTTPErrorData}
+ */
+ this.requestData = {
+ json: request.options.data,
+ files: request.options.files ?? [],
+ };
+ }
+}
+
+module.exports = HTTPError;
\ No newline at end of file
diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js
new file mode 100644
index 00000000..84cecf2
--- /dev/null
+++ b/src/rest/RESTManager.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const { setInterval } = require('node:timers');
+const { Collection } = require('@discordjs/collection');
+const APIRequest = require('./APIRequest');
+const routeBuilder = require('./APIRouter');
+const RequestHandler = require('./RequestHandler');
+const { Error } = require('../errors');
+const { Endpoints } = require('../util/Constants');
+
+class RESTManager {
+ constructor(client) {
+ this.client = client;
+ this.handlers = new Collection();
+ this.versioned = true;
+ this.globalLimit = client.options.restGlobalRateLimit > 0 ? client.options.restGlobalRateLimit : Infinity;
+ this.globalRemaining = this.globalLimit;
+ this.globalReset = null;
+ this.globalDelay = null;
+ if (client.options.restSweepInterval > 0) {
+ this.sweepInterval = setInterval(() => {
+ this.handlers.sweep(handler => handler._inactive);
+ }, client.options.restSweepInterval * 1_000).unref();
+ }
+ }
+
+ get api() {
+ return routeBuilder(this);
+ }
+
+ getAuth() {
+ const token = this.client.token ?? this.client.accessToken;
+ if (token && !this.client.bot) return `${token}`;
+ else if(token && this.client.bot) return `Bot ${token}`;
+ throw new Error('TOKEN_MISSING');
+ }
+
+ get cdn() {
+ return Endpoints.CDN(this.client.options.http.cdn);
+ }
+
+ request(method, url, options = {}) {
+ const apiRequest = new APIRequest(this, method, url, options);
+ let handler = this.handlers.get(apiRequest.route);
+
+ if (!handler) {
+ handler = new RequestHandler(this);
+ this.handlers.set(apiRequest.route, handler);
+ }
+
+ return handler.push(apiRequest);
+ }
+
+ get endpoint() {
+ return this.client.options.http.api;
+ }
+
+ set endpoint(endpoint) {
+ this.client.options.http.api = endpoint;
+ }
+}
+
+module.exports = RESTManager;
\ No newline at end of file
diff --git a/src/rest/RateLimitError.js b/src/rest/RateLimitError.js
new file mode 100644
index 00000000..0be61d0
--- /dev/null
+++ b/src/rest/RateLimitError.js
@@ -0,0 +1,55 @@
+'use strict';
+
+/**
+ * Represents a RateLimit error from a request.
+ * @extends Error
+ */
+class RateLimitError extends Error {
+ constructor({ timeout, limit, method, path, route, global }) {
+ super(`A ${global ? 'global ' : ''}rate limit was hit on route ${route}`);
+
+ /**
+ * The name of the error
+ * @type {string}
+ */
+ this.name = 'RateLimitError';
+
+ /**
+ * Time until this rate limit ends, in ms
+ * @type {number}
+ */
+ this.timeout = timeout;
+
+ /**
+ * The HTTP method used for the request
+ * @type {string}
+ */
+ this.method = method;
+
+ /**
+ * The path of the request relative to the HTTP endpoint
+ * @type {string}
+ */
+ this.path = path;
+
+ /**
+ * The route of the request relative to the HTTP endpoint
+ * @type {string}
+ */
+ this.route = route;
+
+ /**
+ * Whether this rate limit is global
+ * @type {boolean}
+ */
+ this.global = global;
+
+ /**
+ * The maximum amount of requests of this endpoint
+ * @type {number}
+ */
+ this.limit = limit;
+ }
+}
+
+module.exports = RateLimitError;
\ No newline at end of file
diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js
new file mode 100644
index 00000000..59d197f
--- /dev/null
+++ b/src/rest/RequestHandler.js
@@ -0,0 +1,379 @@
+'use strict';
+
+const { setTimeout } = require('node:timers');
+const { setTimeout: sleep } = require('node:timers/promises');
+const { AsyncQueue } = require('@sapphire/async-queue');
+const DiscordAPIError = require('./DiscordAPIError');
+const HTTPError = require('./HTTPError');
+const RateLimitError = require('./RateLimitError');
+const {
+ Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
+} = require('../util/Constants');
+
+function parseResponse(res) {
+ if (res.headers.get('content-type').startsWith('application/json')) return res.json();
+ return res.buffer();
+}
+
+function getAPIOffset(serverDate) {
+ return new Date(serverDate).getTime() - Date.now();
+}
+
+function calculateReset(reset, resetAfter, serverDate) {
+ // Use direct reset time when available, server date becomes irrelevant in this case
+ if (resetAfter) {
+ return Date.now() + Number(resetAfter) * 1_000;
+ }
+ return new Date(Number(reset) * 1_000).getTime() - getAPIOffset(serverDate);
+}
+
+/* Invalid request limiting is done on a per-IP basis, not a per-token basis.
+ * The best we can do is track invalid counts process-wide (on the theory that
+ * users could have multiple bots run from one process) rather than per-bot.
+ * Therefore, store these at file scope here rather than in the client's
+ * RESTManager object.
+ */
+let invalidCount = 0;
+let invalidCountResetTime = null;
+
+class RequestHandler {
+ constructor(manager) {
+ this.manager = manager;
+ this.queue = new AsyncQueue();
+ this.reset = -1;
+ this.remaining = -1;
+ this.limit = -1;
+ }
+
+ async push(request) {
+ await this.queue.wait();
+ try {
+ return await this.execute(request);
+ } finally {
+ this.queue.shift();
+ }
+ }
+
+ get globalLimited() {
+ return this.manager.globalRemaining <= 0 && Date.now() < this.manager.globalReset;
+ }
+
+ get localLimited() {
+ return this.remaining <= 0 && Date.now() < this.reset;
+ }
+
+ get limited() {
+ return this.globalLimited || this.localLimited;
+ }
+
+ get _inactive() {
+ return this.queue.remaining === 0 && !this.limited;
+ }
+
+ globalDelayFor(ms) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ this.manager.globalDelay = null;
+ resolve();
+ }, ms).unref();
+ });
+ }
+
+ /*
+ * Determines whether the request should be queued or whether a RateLimitError should be thrown
+ */
+ async onRateLimit(request, limit, timeout, isGlobal) {
+ const { options } = this.manager.client;
+ if (!options.rejectOnRateLimit) return;
+
+ const rateLimitData = {
+ timeout,
+ limit,
+ method: request.method,
+ path: request.path,
+ route: request.route,
+ global: isGlobal,
+ };
+ const shouldThrow =
+ typeof options.rejectOnRateLimit === 'function'
+ ? await options.rejectOnRateLimit(rateLimitData)
+ : options.rejectOnRateLimit.some(route => rateLimitData.route.startsWith(route.toLowerCase()));
+ if (shouldThrow) {
+ throw new RateLimitError(rateLimitData);
+ }
+ }
+
+ async execute(request) {
+ /*
+ * After calculations have been done, pre-emptively stop further requests
+ * Potentially loop until this task can run if e.g. the global rate limit is hit twice
+ */
+ while (this.limited) {
+ const isGlobal = this.globalLimited;
+ let limit, timeout, delayPromise;
+
+ if (isGlobal) {
+ // Set the variables based on the global rate limit
+ limit = this.manager.globalLimit;
+ timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
+ } else {
+ // Set the variables based on the route-specific rate limit
+ limit = this.limit;
+ timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
+ }
+
+ if (this.manager.client.listenerCount(RATE_LIMIT)) {
+ /**
+ * Emitted when the client hits a rate limit while making a request
+ * @event BaseClient#rateLimit
+ * @param {RateLimitData} rateLimitData Object containing the rate limit info
+ */
+ this.manager.client.emit(RATE_LIMIT, {
+ timeout,
+ limit,
+ method: request.method,
+ path: request.path,
+ route: request.route,
+ global: isGlobal,
+ });
+ }
+
+ if (isGlobal) {
+ // If this is the first task to reach the global timeout, set the global delay
+ if (!this.manager.globalDelay) {
+ // The global delay function should clear the global delay state when it is resolved
+ this.manager.globalDelay = this.globalDelayFor(timeout);
+ }
+ delayPromise = this.manager.globalDelay;
+ } else {
+ delayPromise = sleep(timeout);
+ }
+
+ // Determine whether a RateLimitError should be thrown
+ await this.onRateLimit(request, limit, timeout, isGlobal); // eslint-disable-line no-await-in-loop
+
+ // Wait for the timeout to expire in order to avoid an actual 429
+ await delayPromise; // eslint-disable-line no-await-in-loop
+ }
+
+ // As the request goes out, update the global usage information
+ if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
+ this.manager.globalReset = Date.now() + 1_000;
+ this.manager.globalRemaining = this.manager.globalLimit;
+ }
+ this.manager.globalRemaining--;
+
+ /**
+ * Represents a request that will or has been made to the Discord API
+ * @typedef {Object} APIRequest
+ * @property {HTTPMethod} method The HTTP method used in this request
+ * @property {string} path The full path used to make the request
+ * @property {string} route The API route identifying the rate limit for this request
+ * @property {Object} options Additional options for this request
+ * @property {number} retries The number of times this request has been attempted
+ */
+
+ if (this.manager.client.listenerCount(API_REQUEST)) {
+ /**
+ * Emitted before every API request.
+ * This event can emit several times for the same request, e.g. when hitting a rate limit.
+ * This is an informational event that is emitted quite frequently,
+ * it is highly recommended to check `request.path` to filter the data.
+ * @event BaseClient#apiRequest
+ * @param {APIRequest} request The request that is about to be sent
+ */
+ this.manager.client.emit(API_REQUEST, {
+ method: request.method,
+ path: request.path,
+ route: request.route,
+ options: request.options,
+ retries: request.retries,
+ });
+ }
+
+ // Perform the request
+ let res;
+ try {
+ res = await request.make();
+ } catch (error) {
+ // Retry the specified number of times for request abortions
+ if (request.retries === this.manager.client.options.retryLimit) {
+ throw new HTTPError(error.message, error.constructor.name, error.status, request);
+ }
+
+ request.retries++;
+ return this.execute(request);
+ }
+
+ if (this.manager.client.listenerCount(API_RESPONSE)) {
+ /**
+ * Emitted after every API request has received a response.
+ * This event does not necessarily correlate to completion of the request, e.g. when hitting a rate limit.
+ * This is an informational event that is emitted quite frequently,
+ * it is highly recommended to check `request.path` to filter the data.
+ * @event BaseClient#apiResponse
+ * @param {APIRequest} request The request that triggered this response
+ * @param {Response} response The response received from the Discord API
+ */
+ this.manager.client.emit(
+ API_RESPONSE,
+ {
+ method: request.method,
+ path: request.path,
+ route: request.route,
+ options: request.options,
+ retries: request.retries,
+ },
+ res.clone(),
+ );
+ }
+
+ let sublimitTimeout;
+ if (res.headers) {
+ const serverDate = res.headers.get('date');
+ const limit = res.headers.get('x-ratelimit-limit');
+ const remaining = res.headers.get('x-ratelimit-remaining');
+ const reset = res.headers.get('x-ratelimit-reset');
+ const resetAfter = res.headers.get('x-ratelimit-reset-after');
+ this.limit = limit ? Number(limit) : Infinity;
+ this.remaining = remaining ? Number(remaining) : 1;
+
+ this.reset = reset || resetAfter ? calculateReset(reset, resetAfter, serverDate) : Date.now();
+
+ // https://github.com/discord/discord-api-docs/issues/182
+ if (!resetAfter && request.route.includes('reactions')) {
+ this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
+ }
+
+ // Handle retryAfter, which means we have actually hit a rate limit
+ let retryAfter = res.headers.get('retry-after');
+ retryAfter = retryAfter ? Number(retryAfter) * 1_000 : -1;
+ if (retryAfter > 0) {
+ // If the global rate limit header is set, that means we hit the global rate limit
+ if (res.headers.get('x-ratelimit-global')) {
+ this.manager.globalRemaining = 0;
+ this.manager.globalReset = Date.now() + retryAfter;
+ } else if (!this.localLimited) {
+ /*
+ * This is a sublimit (e.g. 2 channel name changes/10 minutes) since the headers don't indicate a
+ * route-wide rate limit. Don't update remaining or reset to avoid rate limiting the whole
+ * endpoint, just set a reset time on the request itself to avoid retrying too soon.
+ */
+ sublimitTimeout = retryAfter;
+ }
+ }
+ }
+
+ // Count the invalid requests
+ if (res.status === 401 || res.status === 403 || res.status === 429) {
+ if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
+ invalidCountResetTime = Date.now() + 1_000 * 60 * 10;
+ invalidCount = 0;
+ }
+ invalidCount++;
+
+ const emitInvalid =
+ this.manager.client.listenerCount(INVALID_REQUEST_WARNING) &&
+ this.manager.client.options.invalidRequestWarningInterval > 0 &&
+ invalidCount % this.manager.client.options.invalidRequestWarningInterval === 0;
+ if (emitInvalid) {
+ /**
+ * @typedef {Object} InvalidRequestWarningData
+ * @property {number} count Number of invalid requests that have been made in the window
+ * @property {number} remainingTime Time in ms remaining before the count resets
+ */
+
+ /**
+ * Emitted periodically when the process sends invalid requests to let users avoid the
+ * 10k invalid requests in 10 minutes threshold that causes a ban
+ * @event BaseClient#invalidRequestWarning
+ * @param {InvalidRequestWarningData} invalidRequestWarningData Object containing the invalid request info
+ */
+ this.manager.client.emit(INVALID_REQUEST_WARNING, {
+ count: invalidCount,
+ remainingTime: invalidCountResetTime - Date.now(),
+ });
+ }
+ }
+
+ // Handle 2xx and 3xx responses
+ if (res.ok) {
+ // Nothing wrong with the request, proceed with the next one
+ return parseResponse(res);
+ }
+
+ // Handle 4xx responses
+ if (res.status >= 400 && res.status < 500) {
+ // Handle ratelimited requests
+ if (res.status === 429) {
+ const isGlobal = this.globalLimited;
+ let limit, timeout;
+ if (isGlobal) {
+ // Set the variables based on the global rate limit
+ limit = this.manager.globalLimit;
+ timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
+ } else {
+ // Set the variables based on the route-specific rate limit
+ limit = this.limit;
+ timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
+ }
+
+ this.manager.client.emit(
+ DEBUG,
+ `Hit a 429 while executing a request.
+ Global : ${isGlobal}
+ Method : ${request.method}
+ Path : ${request.path}
+ Route : ${request.route}
+ Limit : ${limit}
+ Timeout : ${timeout}ms
+ Sublimit: ${sublimitTimeout ? `${sublimitTimeout}ms` : 'None'}`,
+ );
+
+ await this.onRateLimit(request, limit, timeout, isGlobal);
+
+ // If caused by a sublimit, wait it out here so other requests on the route can be handled
+ if (sublimitTimeout) {
+ await sleep(sublimitTimeout);
+ }
+ return this.execute(request);
+ }
+
+ // Handle possible malformed requests
+ let data;
+ try {
+ data = await parseResponse(res);
+ } catch (err) {
+ throw new HTTPError(err.message, err.constructor.name, err.status, request);
+ }
+
+ throw new DiscordAPIError(data, res.status, request);
+ }
+
+ // Handle 5xx responses
+ if (res.status >= 500 && res.status < 600) {
+ // Retry the specified number of times for possible serverside issues
+ if (request.retries === this.manager.client.options.retryLimit) {
+ throw new HTTPError(res.statusText, res.constructor.name, res.status, request);
+ }
+
+ request.retries++;
+ return this.execute(request);
+ }
+
+ // Fallback in the rare case a status code outside the range 200..=599 is returned
+ return null;
+ }
+}
+
+module.exports = RequestHandler;
+
+/**
+ * @external HTTPMethod
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods}
+ */
+
+/**
+ * @external Response
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response}
+ */
\ No newline at end of file
diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js
new file mode 100644
index 00000000..ceaab75
--- /dev/null
+++ b/src/sharding/Shard.js
@@ -0,0 +1,443 @@
+'use strict';
+
+const EventEmitter = require('node:events');
+const path = require('node:path');
+const process = require('node:process');
+const { setTimeout, clearTimeout } = require('node:timers');
+const { setTimeout: sleep } = require('node:timers/promises');
+const { Error } = require('../errors');
+const Util = require('../util/Util');
+let childProcess = null;
+let Worker = null;
+
+/**
+ * A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains
+ * an instance of the bot and its {@link Client}. When its child process/worker exits for any reason, the shard will
+ * spawn a new one to replace it as necessary.
+ * @extends EventEmitter
+ */
+class Shard extends EventEmitter {
+ constructor(manager, id) {
+ super();
+
+ if (manager.mode === 'process') childProcess = require('node:child_process');
+ else if (manager.mode === 'worker') Worker = require('node:worker_threads').Worker;
+
+ /**
+ * Manager that created the shard
+ * @type {ShardingManager}
+ */
+ this.manager = manager;
+
+ /**
+ * The shard's id in the manager
+ * @type {number}
+ */
+ this.id = id;
+
+ /**
+ * Arguments for the shard's process (only when {@link ShardingManager#mode} is `process`)
+ * @type {string[]}
+ */
+ this.args = manager.shardArgs ?? [];
+
+ /**
+ * Arguments for the shard's process executable (only when {@link ShardingManager#mode} is `process`)
+ * @type {string[]}
+ */
+ this.execArgv = manager.execArgv;
+
+ /**
+ * Environment variables for the shard's process, or workerData for the shard's worker
+ * @type {Object}
+ */
+ this.env = Object.assign({}, process.env, {
+ SHARDING_MANAGER: true,
+ SHARDS: this.id,
+ SHARD_COUNT: this.manager.totalShards,
+ DISCORD_TOKEN: this.manager.token,
+ });
+
+ /**
+ * Whether the shard's {@link Client} is ready
+ * @type {boolean}
+ */
+ this.ready = false;
+
+ /**
+ * Process of the shard (if {@link ShardingManager#mode} is `process`)
+ * @type {?ChildProcess}
+ */
+ this.process = null;
+
+ /**
+ * Worker of the shard (if {@link ShardingManager#mode} is `worker`)
+ * @type {?Worker}
+ */
+ this.worker = null;
+
+ /**
+ * Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with
+ * @type {Map}
+ * @private
+ */
+ this._evals = new Map();
+
+ /**
+ * Ongoing promises for calls to {@link Shard#fetchClientValue}, mapped by the `prop` they were called with
+ * @type {Map}
+ * @private
+ */
+ this._fetches = new Map();
+
+ /**
+ * Listener function for the {@link ChildProcess}' `exit` event
+ * @type {Function}
+ * @private
+ */
+ this._exitListener = null;
+ }
+
+ /**
+ * Forks a child process or creates a worker thread for the shard.
+ * You should not need to call this manually.
+ * @param {number} [timeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready
+ * before resolving (`-1` or `Infinity` for no wait)
+ * @returns {Promise}
+ */
+ spawn(timeout = 30_000) {
+ if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id);
+ if (this.worker) throw new Error('SHARDING_WORKER_EXISTS', this.id);
+
+ this._exitListener = this._handleExit.bind(this, undefined, timeout);
+
+ if (this.manager.mode === 'process') {
+ this.process = childProcess
+ .fork(path.resolve(this.manager.file), this.args, {
+ env: this.env,
+ execArgv: this.execArgv,
+ })
+ .on('message', this._handleMessage.bind(this))
+ .on('exit', this._exitListener);
+ } else if (this.manager.mode === 'worker') {
+ this.worker = new Worker(path.resolve(this.manager.file), { workerData: this.env })
+ .on('message', this._handleMessage.bind(this))
+ .on('exit', this._exitListener);
+ }
+
+ this._evals.clear();
+ this._fetches.clear();
+
+ const child = this.process ?? this.worker;
+
+ /**
+ * Emitted upon the creation of the shard's child process/worker.
+ * @event Shard#spawn
+ * @param {ChildProcess|Worker} process Child process/worker that was created
+ */
+ this.emit('spawn', child);
+
+ if (timeout === -1 || timeout === Infinity) return Promise.resolve(child);
+ return new Promise((resolve, reject) => {
+ const cleanup = () => {
+ clearTimeout(spawnTimeoutTimer);
+ this.off('ready', onReady);
+ this.off('disconnect', onDisconnect);
+ this.off('death', onDeath);
+ };
+
+ const onReady = () => {
+ cleanup();
+ resolve(child);
+ };
+
+ const onDisconnect = () => {
+ cleanup();
+ reject(new Error('SHARDING_READY_DISCONNECTED', this.id));
+ };
+
+ const onDeath = () => {
+ cleanup();
+ reject(new Error('SHARDING_READY_DIED', this.id));
+ };
+
+ const onTimeout = () => {
+ cleanup();
+ reject(new Error('SHARDING_READY_TIMEOUT', this.id));
+ };
+
+ const spawnTimeoutTimer = setTimeout(onTimeout, timeout);
+ this.once('ready', onReady);
+ this.once('disconnect', onDisconnect);
+ this.once('death', onDeath);
+ });
+ }
+
+ /**
+ * Immediately kills the shard's process/worker and does not restart it.
+ */
+ kill() {
+ if (this.process) {
+ this.process.removeListener('exit', this._exitListener);
+ this.process.kill();
+ } else {
+ this.worker.removeListener('exit', this._exitListener);
+ this.worker.terminate();
+ }
+
+ this._handleExit(false);
+ }
+
+ /**
+ * Options used to respawn a shard.
+ * @typedef {Object} ShardRespawnOptions
+ * @property {number} [delay=500] How long to wait between killing the process/worker and
+ * restarting it (in milliseconds)
+ * @property {number} [timeout=30000] The amount in milliseconds to wait until the {@link Client}
+ * has become ready before resolving (`-1` or `Infinity` for no wait)
+ */
+
+ /**
+ * Kills and restarts the shard's process/worker.
+ * @param {ShardRespawnOptions} [options] Options for respawning the shard
+ * @returns {Promise}
+ */
+ async respawn({ delay = 500, timeout = 30_000 } = {}) {
+ this.kill();
+ if (delay > 0) await sleep(delay);
+ return this.spawn(timeout);
+ }
+
+ /**
+ * Sends a message to the shard's process/worker.
+ * @param {*} message Message to send to the shard
+ * @returns {Promise}
+ */
+ send(message) {
+ return new Promise((resolve, reject) => {
+ if (this.process) {
+ this.process.send(message, err => {
+ if (err) reject(err);
+ else resolve(this);
+ });
+ } else {
+ this.worker.postMessage(message);
+ resolve(this);
+ }
+ });
+ }
+
+ /**
+ * Fetches a client property value of the shard.
+ * @param {string} prop Name of the client property to get, using periods for nesting
+ * @returns {Promise<*>}
+ * @example
+ * shard.fetchClientValue('guilds.cache.size')
+ * .then(count => console.log(`${count} guilds in shard ${shard.id}`))
+ * .catch(console.error);
+ */
+ fetchClientValue(prop) {
+ // Shard is dead (maybe respawning), don't cache anything and error immediately
+ if (!this.process && !this.worker) return Promise.reject(new Error('SHARDING_NO_CHILD_EXISTS', this.id));
+
+ // Cached promise from previous call
+ if (this._fetches.has(prop)) return this._fetches.get(prop);
+
+ const promise = new Promise((resolve, reject) => {
+ const child = this.process ?? this.worker;
+
+ const listener = message => {
+ if (message?._fetchProp !== prop) return;
+ child.removeListener('message', listener);
+ this.decrementMaxListeners(child);
+ this._fetches.delete(prop);
+ if (!message._error) resolve(message._result);
+ else reject(Util.makeError(message._error));
+ };
+
+ this.incrementMaxListeners(child);
+ child.on('message', listener);
+
+ this.send({ _fetchProp: prop }).catch(err => {
+ child.removeListener('message', listener);
+ this.decrementMaxListeners(child);
+ this._fetches.delete(prop);
+ reject(err);
+ });
+ });
+
+ this._fetches.set(prop, promise);
+ return promise;
+ }
+
+ /**
+ * Evaluates a script or function on the shard, in the context of the {@link Client}.
+ * @param {string|Function} script JavaScript to run on the shard
+ * @param {*} [context] The context for the eval
+ * @returns {Promise<*>} Result of the script execution
+ */
+ eval(script, context) {
+ // Stringify the script if it's a Function
+ const _eval = typeof script === 'function' ? `(${script})(this, ${JSON.stringify(context)})` : script;
+
+ // Shard is dead (maybe respawning), don't cache anything and error immediately
+ if (!this.process && !this.worker) return Promise.reject(new Error('SHARDING_NO_CHILD_EXISTS', this.id));
+
+ // Cached promise from previous call
+ if (this._evals.has(_eval)) return this._evals.get(_eval);
+
+ const promise = new Promise((resolve, reject) => {
+ const child = this.process ?? this.worker;
+
+ const listener = message => {
+ if (message?._eval !== _eval) return;
+ child.removeListener('message', listener);
+ this.decrementMaxListeners(child);
+ this._evals.delete(_eval);
+ if (!message._error) resolve(message._result);
+ else reject(Util.makeError(message._error));
+ };
+
+ this.incrementMaxListeners(child);
+ child.on('message', listener);
+
+ this.send({ _eval }).catch(err => {
+ child.removeListener('message', listener);
+ this.decrementMaxListeners(child);
+ this._evals.delete(_eval);
+ reject(err);
+ });
+ });
+
+ this._evals.set(_eval, promise);
+ return promise;
+ }
+
+ /**
+ * Handles a message received from the child process/worker.
+ * @param {*} message Message received
+ * @private
+ */
+ _handleMessage(message) {
+ if (message) {
+ // Shard is ready
+ if (message._ready) {
+ this.ready = true;
+ /**
+ * Emitted upon the shard's {@link Client#event:shardReady} event.
+ * @event Shard#ready
+ */
+ this.emit('ready');
+ return;
+ }
+
+ // Shard has disconnected
+ if (message._disconnect) {
+ this.ready = false;
+ /**
+ * Emitted upon the shard's {@link Client#event:shardDisconnect} event.
+ * @event Shard#disconnect
+ */
+ this.emit('disconnect');
+ return;
+ }
+
+ // Shard is attempting to reconnect
+ if (message._reconnecting) {
+ this.ready = false;
+ /**
+ * Emitted upon the shard's {@link Client#event:shardReconnecting} event.
+ * @event Shard#reconnecting
+ */
+ this.emit('reconnecting');
+ return;
+ }
+
+ // Shard is requesting a property fetch
+ if (message._sFetchProp) {
+ const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard };
+ this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then(
+ results => this.send({ ...resp, _result: results }),
+ err => this.send({ ...resp, _error: Util.makePlainError(err) }),
+ );
+ return;
+ }
+
+ // Shard is requesting an eval broadcast
+ if (message._sEval) {
+ const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard };
+ this.manager._performOnShards('eval', [message._sEval], message._sEvalShard).then(
+ results => this.send({ ...resp, _result: results }),
+ err => this.send({ ...resp, _error: Util.makePlainError(err) }),
+ );
+ return;
+ }
+
+ // Shard is requesting a respawn of all shards
+ if (message._sRespawnAll) {
+ const { shardDelay, respawnDelay, timeout } = message._sRespawnAll;
+ this.manager.respawnAll({ shardDelay, respawnDelay, timeout }).catch(() => {
+ // Do nothing
+ });
+ return;
+ }
+ }
+
+ /**
+ * Emitted upon receiving a message from the child process/worker.
+ * @event Shard#message
+ * @param {*} message Message that was received
+ */
+ this.emit('message', message);
+ }
+
+ /**
+ * Handles the shard's process/worker exiting.
+ * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again
+ * @param {number} [timeout] The amount in milliseconds to wait until the {@link Client}
+ * has become ready (`-1` or `Infinity` for no wait)
+ * @private
+ */
+ _handleExit(respawn = this.manager.respawn, timeout) {
+ /**
+ * Emitted upon the shard's child process/worker exiting.
+ * @event Shard#death
+ * @param {ChildProcess|Worker} process Child process/worker that exited
+ */
+ this.emit('death', this.process ?? this.worker);
+
+ this.ready = false;
+ this.process = null;
+ this.worker = null;
+ this._evals.clear();
+ this._fetches.clear();
+
+ if (respawn) this.spawn(timeout).catch(err => this.emit('error', err));
+ }
+
+ /**
+ * Increments max listeners by one for a given emitter, if they are not zero.
+ * @param {EventEmitter|process} emitter The emitter that emits the events.
+ * @private
+ */
+ incrementMaxListeners(emitter) {
+ const maxListeners = emitter.getMaxListeners();
+ if (maxListeners !== 0) {
+ emitter.setMaxListeners(maxListeners + 1);
+ }
+ }
+
+ /**
+ * Decrements max listeners by one for a given emitter, if they are not zero.
+ * @param {EventEmitter|process} emitter The emitter that emits the events.
+ * @private
+ */
+ decrementMaxListeners(emitter) {
+ const maxListeners = emitter.getMaxListeners();
+ if (maxListeners !== 0) {
+ emitter.setMaxListeners(maxListeners - 1);
+ }
+ }
+}
+
+module.exports = Shard;
diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js
new file mode 100644
index 00000000..0772af6
--- /dev/null
+++ b/src/sharding/ShardClientUtil.js
@@ -0,0 +1,278 @@
+'use strict';
+
+const process = require('node:process');
+const { Error } = require('../errors');
+const Events = require('../util/Events');
+const Util = require('../util/Util');
+
+/**
+ * Helper class for sharded clients spawned as a child process/worker, such as from a {@link ShardingManager}.
+ * Utilises IPC to send and receive data to/from the master process and other shards.
+ */
+class ShardClientUtil {
+ constructor(client, mode) {
+ /**
+ * Client for the shard
+ * @type {Client}
+ */
+ this.client = client;
+
+ /**
+ * Mode the shard was spawned with
+ * @type {ShardingManagerMode}
+ */
+ this.mode = mode;
+
+ /**
+ * Message port for the master process (only when {@link ShardClientUtil#mode} is `worker`)
+ * @type {?MessagePort}
+ */
+ this.parentPort = null;
+
+ if (mode === 'process') {
+ process.on('message', this._handleMessage.bind(this));
+ client.on('ready', () => {
+ process.send({ _ready: true });
+ });
+ client.on('disconnect', () => {
+ process.send({ _disconnect: true });
+ });
+ client.on('reconnecting', () => {
+ process.send({ _reconnecting: true });
+ });
+ } else if (mode === 'worker') {
+ this.parentPort = require('node:worker_threads').parentPort;
+ this.parentPort.on('message', this._handleMessage.bind(this));
+ client.on('ready', () => {
+ this.parentPort.postMessage({ _ready: true });
+ });
+ client.on('disconnect', () => {
+ this.parentPort.postMessage({ _disconnect: true });
+ });
+ client.on('reconnecting', () => {
+ this.parentPort.postMessage({ _reconnecting: true });
+ });
+ }
+ }
+
+ /**
+ * Array of shard ids of this client
+ * @type {number[]}
+ * @readonly
+ */
+ get ids() {
+ return this.client.options.shards;
+ }
+
+ /**
+ * Total number of shards
+ * @type {number}
+ * @readonly
+ */
+ get count() {
+ return this.client.options.shardCount;
+ }
+
+ /**
+ * Sends a message to the master process.
+ * @param {*} message Message to send
+ * @returns {Promise}
+ * @emits Shard#message
+ */
+ send(message) {
+ return new Promise((resolve, reject) => {
+ if (this.mode === 'process') {
+ process.send(message, err => {
+ if (err) reject(err);
+ else resolve();
+ });
+ } else if (this.mode === 'worker') {
+ this.parentPort.postMessage(message);
+ resolve();
+ }
+ });
+ }
+
+ /**
+ * Fetches a client property value of each shard, or a given shard.
+ * @param {string} prop Name of the client property to get, using periods for nesting
+ * @param {number} [shard] Shard to fetch property from, all if undefined
+ * @returns {Promise<*|Array<*>>}
+ * @example
+ * client.shard.fetchClientValues('guilds.cache.size')
+ * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
+ * .catch(console.error);
+ * @see {@link ShardingManager#fetchClientValues}
+ */
+ fetchClientValues(prop, shard) {
+ return new Promise((resolve, reject) => {
+ const parent = this.parentPort ?? process;
+
+ const listener = message => {
+ if (message?._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
+ parent.removeListener('message', listener);
+ this.decrementMaxListeners(parent);
+ if (!message._error) resolve(message._result);
+ else reject(Util.makeError(message._error));
+ };
+ this.incrementMaxListeners(parent);
+ parent.on('message', listener);
+
+ this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
+ parent.removeListener('message', listener);
+ this.decrementMaxListeners(parent);
+ reject(err);
+ });
+ });
+ }
+
+ /**
+ * Evaluates a script or function on all shards, or a given shard, in the context of the {@link Client}s.
+ * @param {Function} script JavaScript to run on each shard
+ * @param {BroadcastEvalOptions} [options={}] The options for the broadcast
+ * @returns {Promise<*|Array<*>>} Results of the script execution
+ * @example
+ * client.shard.broadcastEval(client => client.guilds.cache.size)
+ * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
+ * .catch(console.error);
+ * @see {@link ShardingManager#broadcastEval}
+ */
+ broadcastEval(script, options = {}) {
+ return new Promise((resolve, reject) => {
+ const parent = this.parentPort ?? process;
+ if (typeof script !== 'function') {
+ reject(new TypeError('SHARDING_INVALID_EVAL_BROADCAST'));
+ return;
+ }
+ script = `(${script})(this, ${JSON.stringify(options.context)})`;
+
+ const listener = message => {
+ if (message?._sEval !== script || message._sEvalShard !== options.shard) return;
+ parent.removeListener('message', listener);
+ this.decrementMaxListeners(parent);
+ if (!message._error) resolve(message._result);
+ else reject(Util.makeError(message._error));
+ };
+ this.incrementMaxListeners(parent);
+ parent.on('message', listener);
+ this.send({ _sEval: script, _sEvalShard: options.shard }).catch(err => {
+ parent.removeListener('message', listener);
+ this.decrementMaxListeners(parent);
+ reject(err);
+ });
+ });
+ }
+
+ /**
+ * Requests a respawn of all shards.
+ * @param {MultipleShardRespawnOptions} [options] Options for respawning shards
+ * @returns {Promise} Resolves upon the message being sent
+ * @see {@link ShardingManager#respawnAll}
+ */
+ respawnAll({ shardDelay = 5_000, respawnDelay = 500, timeout = 30_000 } = {}) {
+ return this.send({ _sRespawnAll: { shardDelay, respawnDelay, timeout } });
+ }
+
+ /**
+ * Handles an IPC message.
+ * @param {*} message Message received
+ * @private
+ */
+ async _handleMessage(message) {
+ if (!message) return;
+ if (message._fetchProp) {
+ try {
+ const props = message._fetchProp.split('.');
+ let value = this.client;
+ for (const prop of props) value = value[prop];
+ this._respond('fetchProp', { _fetchProp: message._fetchProp, _result: value });
+ } catch (err) {
+ this._respond('fetchProp', { _fetchProp: message._fetchProp, _error: Util.makePlainError(err) });
+ }
+ } else if (message._eval) {
+ try {
+ this._respond('eval', { _eval: message._eval, _result: await this.client._eval(message._eval) });
+ } catch (err) {
+ this._respond('eval', { _eval: message._eval, _error: Util.makePlainError(err) });
+ }
+ }
+ }
+
+ /**
+ * Sends a message to the master process, emitting an error from the client upon failure.
+ * @param {string} type Type of response to send
+ * @param {*} message Message to send
+ * @private
+ */
+ _respond(type, message) {
+ this.send(message).catch(err => {
+ const error = new Error(`Error when sending ${type} response to master process: ${err.message}`);
+ error.stack = err.stack;
+ /**
+ * Emitted when the client encounters an error.
+ * Errors thrown within this event do not have a catch handler, it is
+ * recommended to not use async functions as `error` event handlers. See the
+ * [Node.js docs](https://nodejs.org/api/events.html#capture-rejections-of-promises) for details.
+ * @event Client#error
+ * @param {Error} error The error encountered
+ */
+ this.client.emit(Events.Error, error);
+ });
+ }
+
+ /**
+ * Creates/gets the singleton of this class.
+ * @param {Client} client The client to use
+ * @param {ShardingManagerMode} mode Mode the shard was spawned with
+ * @returns {ShardClientUtil}
+ */
+ static singleton(client, mode) {
+ if (!this._singleton) {
+ this._singleton = new this(client, mode);
+ } else {
+ client.emit(
+ Events.Warn,
+ 'Multiple clients created in child process/worker; only the first will handle sharding helpers.',
+ );
+ }
+ return this._singleton;
+ }
+
+ /**
+ * Get the shard id for a given guild id.
+ * @param {Snowflake} guildId Snowflake guild id to get shard id for
+ * @param {number} shardCount Number of shards
+ * @returns {number}
+ */
+ static shardIdForGuildId(guildId, shardCount) {
+ const shard = Number(BigInt(guildId) >> 22n) % shardCount;
+ if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildId, shardCount);
+ return shard;
+ }
+
+ /**
+ * Increments max listeners by one for a given emitter, if they are not zero.
+ * @param {EventEmitter|process} emitter The emitter that emits the events.
+ * @private
+ */
+ incrementMaxListeners(emitter) {
+ const maxListeners = emitter.getMaxListeners();
+ if (maxListeners !== 0) {
+ emitter.setMaxListeners(maxListeners + 1);
+ }
+ }
+
+ /**
+ * Decrements max listeners by one for a given emitter, if they are not zero.
+ * @param {EventEmitter|process} emitter The emitter that emits the events.
+ * @private
+ */
+ decrementMaxListeners(emitter) {
+ const maxListeners = emitter.getMaxListeners();
+ if (maxListeners !== 0) {
+ emitter.setMaxListeners(maxListeners - 1);
+ }
+ }
+}
+
+module.exports = ShardClientUtil;
diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js
new file mode 100644
index 00000000..db8581c
--- /dev/null
+++ b/src/sharding/ShardingManager.js
@@ -0,0 +1,318 @@
+'use strict';
+
+const EventEmitter = require('node:events');
+const fs = require('node:fs');
+const path = require('node:path');
+const process = require('node:process');
+const { setTimeout: sleep } = require('node:timers/promises');
+const { Collection } = require('@discordjs/collection');
+const Shard = require('./Shard');
+const { Error, TypeError, RangeError } = require('../errors');
+const Util = require('../util/Util');
+
+/**
+ * This is a utility class that makes multi-process sharding of a bot an easy and painless experience.
+ * It works by spawning a self-contained {@link ChildProcess} or {@link Worker} for each individual shard, each
+ * containing its own instance of your bot's {@link Client}. They all have a line of communication with the master
+ * process, and there are several useful methods that utilise it in order to simplify tasks that are normally difficult
+ * with sharding. It can spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a
+ * path to your main bot script to launch for each one.
+ * @extends {EventEmitter}
+ */
+class ShardingManager extends EventEmitter {
+ /**
+ * The mode to spawn shards with for a {@link ShardingManager}. Can be either one of:
+ * * 'process' to use child processes
+ * * 'worker' to use [Worker threads](https://nodejs.org/api/worker_threads.html)
+ * @typedef {string} ShardingManagerMode
+ */
+
+ /**
+ * The options to spawn shards with for a {@link ShardingManager}.
+ * @typedef {Object} ShardingManagerOptions
+ * @property {string|number} [totalShards='auto'] Number of total shards of all shard managers or "auto"
+ * @property {string|number[]} [shardList='auto'] List of shards to spawn or "auto"
+ * @property {ShardingManagerMode} [mode='process'] Which mode to use for shards
+ * @property {boolean} [respawn=true] Whether shards should automatically respawn upon exiting
+ * @property {string[]} [shardArgs=[]] Arguments to pass to the shard script when spawning
+ * (only available when mode is set to 'process')
+ * @property {string[]} [execArgv=[]] Arguments to pass to the shard script executable when spawning
+ * (only available when mode is set to 'process')
+ * @property {string} [token] Token to use for automatic shard count and passing to shards
+ */
+
+ /**
+ * @param {string} file Path to your shard script file
+ * @param {ShardingManagerOptions} [options] Options for the sharding manager
+ */
+ constructor(file, options = {}) {
+ super();
+ options = Util.mergeDefault(
+ {
+ totalShards: 'auto',
+ mode: 'process',
+ respawn: true,
+ shardArgs: [],
+ execArgv: [],
+ token: process.env.DISCORD_TOKEN,
+ },
+ options,
+ );
+
+ /**
+ * Path to the shard script file
+ * @type {string}
+ */
+ this.file = file;
+ if (!file) throw new Error('CLIENT_INVALID_OPTION', 'File', 'specified.');
+ if (!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file);
+ const stats = fs.statSync(this.file);
+ if (!stats.isFile()) throw new Error('CLIENT_INVALID_OPTION', 'File', 'a file');
+
+ /**
+ * List of shards this sharding manager spawns
+ * @type {string|number[]}
+ */
+ this.shardList = options.shardList ?? 'auto';
+ if (this.shardList !== 'auto') {
+ if (!Array.isArray(this.shardList)) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array.');
+ }
+ this.shardList = [...new Set(this.shardList)];
+ if (this.shardList.length < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardList', 'at least 1 id.');
+ if (
+ this.shardList.some(
+ shardId => typeof shardId !== 'number' || isNaN(shardId) || !Number.isInteger(shardId) || shardId < 0,
+ )
+ ) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of positive integers.');
+ }
+ }
+
+ /**
+ * Amount of shards that all sharding managers spawn in total
+ * @type {number}
+ */
+ this.totalShards = options.totalShards || 'auto';
+ if (this.totalShards !== 'auto') {
+ if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
+ }
+ if (this.totalShards < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.');
+ if (!Number.isInteger(this.totalShards)) {
+ throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.');
+ }
+ }
+
+ /**
+ * Mode for shards to spawn with
+ * @type {ShardingManagerMode}
+ */
+ this.mode = options.mode;
+ if (this.mode !== 'process' && this.mode !== 'worker') {
+ throw new RangeError('CLIENT_INVALID_OPTION', 'Sharding mode', '"process" or "worker"');
+ }
+
+ /**
+ * Whether shards should automatically respawn upon exiting
+ * @type {boolean}
+ */
+ this.respawn = options.respawn;
+
+ /**
+ * An array of arguments to pass to shards (only when {@link ShardingManager#mode} is `process`)
+ * @type {string[]}
+ */
+ this.shardArgs = options.shardArgs;
+
+ /**
+ * An array of arguments to pass to the executable (only when {@link ShardingManager#mode} is `process`)
+ * @type {string[]}
+ */
+ this.execArgv = options.execArgv;
+
+ /**
+ * Token to use for obtaining the automatic shard count, and passing to shards
+ * @type {?string}
+ */
+ this.token = options.token?.replace(/^Bot\s*/i, '') ?? null;
+
+ /**
+ * A collection of shards that this manager has spawned
+ * @type {Collection}
+ */
+ this.shards = new Collection();
+
+ process.env.SHARDING_MANAGER = true;
+ process.env.SHARDING_MANAGER_MODE = this.mode;
+ process.env.DISCORD_TOKEN = this.token;
+ }
+
+ /**
+ * Creates a single shard.
+ * Using this method is usually not necessary if you use the spawn method.
+ * @param {number} [id=this.shards.size] Id of the shard to create
+ * This is usually not necessary to manually specify.
+ * @returns {Shard} Note that the created shard needs to be explicitly spawned using its spawn method.
+ */
+ createShard(id = this.shards.size) {
+ const shard = new Shard(this, id);
+ this.shards.set(id, shard);
+ /**
+ * Emitted upon creating a shard.
+ * @event ShardingManager#shardCreate
+ * @param {Shard} shard Shard that was created
+ */
+ this.emit('shardCreate', shard);
+ return shard;
+ }
+
+ /**
+ * Options used to spawn multiple shards.
+ * @typedef {Object} MultipleShardSpawnOptions
+ * @property {number|string} [amount=this.totalShards] Number of shards to spawn
+ * @property {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds)
+ * @property {number} [timeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready
+ */
+
+ /**
+ * Spawns multiple shards.
+ * @param {MultipleShardSpawnOptions} [options] Options for spawning shards
+ * @returns {Promise>}
+ */
+ async spawn({ amount = this.totalShards, delay = 5500, timeout = 30_000 } = {}) {
+ // Obtain/verify the number of shards to spawn
+ if (amount === 'auto') {
+ amount = await Util.fetchRecommendedShards(this.token);
+ } else {
+ if (typeof amount !== 'number' || isNaN(amount)) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
+ }
+ if (amount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.');
+ if (!Number.isInteger(amount)) {
+ throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.');
+ }
+ }
+
+ // Make sure this many shards haven't already been spawned
+ if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
+ if (this.shardList === 'auto' || this.totalShards === 'auto' || this.totalShards !== amount) {
+ this.shardList = [...Array(amount).keys()];
+ }
+ if (this.totalShards === 'auto' || this.totalShards !== amount) {
+ this.totalShards = amount;
+ }
+
+ if (this.shardList.some(shardId => shardId >= amount)) {
+ throw new RangeError(
+ 'CLIENT_INVALID_OPTION',
+ 'Amount of shards',
+ 'bigger than the highest shardId in the shardList option.',
+ );
+ }
+
+ // Spawn the shards
+ for (const shardId of this.shardList) {
+ const promises = [];
+ const shard = this.createShard(shardId);
+ promises.push(shard.spawn(timeout));
+ if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(sleep(delay));
+ await Promise.all(promises); // eslint-disable-line no-await-in-loop
+ }
+
+ return this.shards;
+ }
+
+ /**
+ * Sends a message to all shards.
+ * @param {*} message Message to be sent to the shards
+ * @returns {Promise}
+ */
+ broadcast(message) {
+ const promises = [];
+ for (const shard of this.shards.values()) promises.push(shard.send(message));
+ return Promise.all(promises);
+ }
+
+ /**
+ * Options for {@link ShardingManager#broadcastEval} and {@link ShardClientUtil#broadcastEval}.
+ * @typedef {Object} BroadcastEvalOptions
+ * @property {number} [shard] Shard to run script on, all if undefined
+ * @property {*} [context] The JSON-serializable values to call the script with
+ */
+
+ /**
+ * Evaluates a script on all shards, or a given shard, in the context of the {@link Client}s.
+ * @param {Function} script JavaScript to run on each shard
+ * @param {BroadcastEvalOptions} [options={}] The options for the broadcast
+ * @returns {Promise<*|Array<*>>} Results of the script execution
+ */
+ broadcastEval(script, options = {}) {
+ if (typeof script !== 'function') return Promise.reject(new TypeError('SHARDING_INVALID_EVAL_BROADCAST'));
+ return this._performOnShards('eval', [`(${script})(this, ${JSON.stringify(options.context)})`], options.shard);
+ }
+
+ /**
+ * Fetches a client property value of each shard, or a given shard.
+ * @param {string} prop Name of the client property to get, using periods for nesting
+ * @param {number} [shard] Shard to fetch property from, all if undefined
+ * @returns {Promise<*|Array<*>>}
+ * @example
+ * manager.fetchClientValues('guilds.cache.size')
+ * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
+ * .catch(console.error);
+ */
+ fetchClientValues(prop, shard) {
+ return this._performOnShards('fetchClientValue', [prop], shard);
+ }
+
+ /**
+ * Runs a method with given arguments on all shards, or a given shard.
+ * @param {string} method Method name to run on each shard
+ * @param {Array<*>} args Arguments to pass through to the method call
+ * @param {number} [shard] Shard to run on, all if undefined
+ * @returns {Promise<*|Array<*>>} Results of the method execution
+ * @private
+ */
+ _performOnShards(method, args, shard) {
+ if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS'));
+
+ if (typeof shard === 'number') {
+ if (this.shards.has(shard)) return this.shards.get(shard)[method](...args);
+ return Promise.reject(new Error('SHARDING_SHARD_NOT_FOUND', shard));
+ }
+
+ if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS'));
+
+ const promises = [];
+ for (const sh of this.shards.values()) promises.push(sh[method](...args));
+ return Promise.all(promises);
+ }
+
+ /**
+ * Options used to respawn all shards.
+ * @typedef {Object} MultipleShardRespawnOptions
+ * @property {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
+ * @property {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
+ * (in milliseconds)
+ * @property {number} [timeout=30000] The amount in milliseconds to wait for a shard to become ready before
+ * continuing to another (`-1` or `Infinity` for no wait)
+ */
+
+ /**
+ * Kills all running shards and respawns them.
+ * @param {MultipleShardRespawnOptions} [options] Options for respawning shards
+ * @returns {Promise>}
+ */
+ async respawnAll({ shardDelay = 5_000, respawnDelay = 500, timeout = 30_000 } = {}) {
+ let s = 0;
+ for (const shard of this.shards.values()) {
+ const promises = [shard.respawn({ delay: respawnDelay, timeout })];
+ if (++s < this.shards.size && shardDelay > 0) promises.push(sleep(shardDelay));
+ await Promise.all(promises); // eslint-disable-line no-await-in-loop
+ }
+ return this.shards;
+ }
+}
+
+module.exports = ShardingManager;
diff --git a/src/structures/ActionRow.js b/src/structures/ActionRow.js
new file mode 100644
index 00000000..450a9f3
--- /dev/null
+++ b/src/structures/ActionRow.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const { ActionRow: BuildersActionRow } = require('@discordjs/builders');
+const Transformers = require('../util/Transformers');
+
+class ActionRow extends BuildersActionRow {
+ constructor(data) {
+ super(Transformers.toSnakeCase(data));
+ }
+}
+
+module.exports = ActionRow;
diff --git a/src/structures/AnonymousGuild.js b/src/structures/AnonymousGuild.js
new file mode 100644
index 00000000..b919d82
--- /dev/null
+++ b/src/structures/AnonymousGuild.js
@@ -0,0 +1,95 @@
+'use strict';
+
+const BaseGuild = require('./BaseGuild');
+
+/**
+ * Bundles common attributes and methods between {@link Guild} and {@link InviteGuild}
+ * @extends {BaseGuild}
+ * @abstract
+ */
+class AnonymousGuild extends BaseGuild {
+ constructor(client, data, immediatePatch = true) {
+ super(client, data);
+ if (immediatePatch) this._patch(data);
+ }
+
+ _patch(data) {
+ if ('features' in data) this.features = data.features;
+
+ if ('splash' in data) {
+ /**
+ * The hash of the guild invite splash image
+ * @type {?string}
+ */
+ this.splash = data.splash;
+ }
+
+ if ('banner' in data) {
+ /**
+ * The hash of the guild banner
+ * @type {?string}
+ */
+ this.banner = data.banner;
+ }
+
+ if ('description' in data) {
+ /**
+ * The description of the guild, if any
+ * @type {?string}
+ */
+ this.description = data.description;
+ }
+
+ if ('verification_level' in data) {
+ /**
+ * The verification level of the guild
+ * @type {GuildVerificationLevel}
+ */
+ this.verificationLevel = data.verification_level;
+ }
+
+ if ('vanity_url_code' in data) {
+ /**
+ * The vanity invite code of the guild, if any
+ * @type {?string}
+ */
+ this.vanityURLCode = data.vanity_url_code;
+ }
+
+ if ('nsfw_level' in data) {
+ /**
+ * The NSFW level of this guild
+ * @type {GuildNSFWLevel}
+ */
+ this.nsfwLevel = data.nsfw_level;
+ }
+
+ if ('premium_subscription_count' in data) {
+ /**
+ * The total number of boosts for this server
+ * @type {?number}
+ */
+ this.premiumSubscriptionCount = data.premium_subscription_count;
+ }
+ }
+
+ /**
+ * The URL to this guild's banner.
+ * @param {ImageURLOptions} [options={}] Options for the image URL
+ * @returns {?string}
+ */
+ bannerURL(options = {}) {
+ return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options);
+ }
+
+ /**
+ * The URL to this guild's invite splash image.
+ * @param {ImageURLOptions} [options={}] Options for the image URL
+ * @returns {?string}
+ */
+ splashURL(options = {}) {
+ return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
+ }
+}
+
+module.exports = AnonymousGuild;
diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js
new file mode 100644
index 00000000..f43304d
--- /dev/null
+++ b/src/structures/ApplicationCommand.js
@@ -0,0 +1,415 @@
+'use strict';
+
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const { ApplicationCommandOptionType } = require('discord-api-types/v9');
+const Base = require('./Base');
+const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
+
+/**
+ * Represents an application command.
+ * @extends {Base}
+ */
+class ApplicationCommand extends Base {
+ constructor(client, data, guild, guildId) {
+ super(client);
+
+ /**
+ * The command's id
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+
+ /**
+ * The parent application's id
+ * @type {Snowflake}
+ */
+ this.applicationId = data.application_id;
+
+ /**
+ * The guild this command is part of
+ * @type {?Guild}
+ */
+ this.guild = guild ?? null;
+
+ /**
+ * The guild's id this command is part of, this may be non-null when `guild` is `null` if the command
+ * was fetched from the `ApplicationCommandManager`
+ * @type {?Snowflake}
+ */
+ this.guildId = guild?.id ?? guildId ?? null;
+
+ /**
+ * The manager for permissions of this command on its guild or arbitrary guilds when the command is global
+ * @type {ApplicationCommandPermissionsManager}
+ */
+ this.permissions = new ApplicationCommandPermissionsManager(this);
+
+ /**
+ * The type of this application command
+ * @type {ApplicationCommandType}
+ */
+ this.type = data.type;
+
+ this._patch(data);
+ }
+
+ _patch(data) {
+ if ('name' in data) {
+ /**
+ * The name of this command
+ * @type {string}
+ */
+ this.name = data.name;
+ }
+
+ if ('description' in data) {
+ /**
+ * The description of this command
+ * @type {string}
+ */
+ this.description = data.description;
+ }
+
+ if ('options' in data) {
+ /**
+ * The options of this command
+ * @type {ApplicationCommandOption[]}
+ */
+ this.options = data.options.map(o => this.constructor.transformOption(o, true));
+ } else {
+ this.options ??= [];
+ }
+
+ if ('default_permission' in data) {
+ /**
+ * Whether the command is enabled by default when the app is added to a guild
+ * @type {boolean}
+ */
+ this.defaultPermission = data.default_permission;
+ }
+
+ if ('version' in data) {
+ /**
+ * Autoincrementing version identifier updated during substantial record changes
+ * @type {Snowflake}
+ */
+ this.version = data.version;
+ }
+ }
+
+ /**
+ * The timestamp the command was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return DiscordSnowflake.timestampFrom(this.id);
+ }
+
+ /**
+ * The time the command was created at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * The manager that this command belongs to
+ * @type {ApplicationCommandManager}
+ * @readonly
+ */
+ get manager() {
+ return (this.guild ?? this.client.application).commands;
+ }
+
+ /**
+ * Data for creating or editing an application command.
+ * @typedef {Object} ApplicationCommandData
+ * @property {string} name The name of the command, must be in all lowercase if type is
+ * {@link ApplicationCommandType.ChatInput}
+ * @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput}
+ * @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command
+ * @property {ApplicationCommandOptionData[]} [options] Options for the command
+ * @property {boolean} [defaultPermission=true] Whether the command is enabled by default when the app is added to a
+ * guild
+ */
+
+ /**
+ * An option for an application command or subcommand.
+ * In addition to the listed properties, when used as a parameter,
+ * API style `snake_case` properties can be used for compatibility with generators like `@discordjs/builders`.
+ * Note that providing a value for the `camelCase` counterpart for any `snake_case` property
+ * will discard the provided `snake_case` property.
+ * @typedef {Object} ApplicationCommandOptionData
+ * @property {ApplicationCommandOptionType} type The type of the option
+ * @property {string} name The name of the option
+ * @property {string} description The description of the option
+ * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
+ * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ * @property {boolean} [required] Whether the option is required
+ * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
+ * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group)
+ * @property {ChannelType[]} [channelTypes] When the option type is channel,
+ * the allowed types of channels that can be selected
+ * @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ */
+
+ /**
+ * Edits this application command.
+ * @param {ApplicationCommandData} data The data to update the command with
+ * @returns {Promise}
+ * @example
+ * // Edit the description of this command
+ * command.edit({
+ * description: 'New description',
+ * })
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ edit(data) {
+ return this.manager.edit(this, data, this.guildId);
+ }
+
+ /**
+ * Edits the name of this ApplicationCommand
+ * @param {string} name The new name of the command
+ * @returns {Promise}
+ */
+ setName(name) {
+ return this.edit({ name });
+ }
+
+ /**
+ * Edits the description of this ApplicationCommand
+ * @param {string} description The new description of the command
+ * @returns {Promise}
+ */
+ setDescription(description) {
+ return this.edit({ description });
+ }
+
+ /**
+ * Edits the default permission of this ApplicationCommand
+ * @param {boolean} [defaultPermission=true] The default permission for this command
+ * @returns {Promise}
+ */
+ setDefaultPermission(defaultPermission = true) {
+ return this.edit({ defaultPermission });
+ }
+
+ /**
+ * Edits the options of this ApplicationCommand
+ * @param {ApplicationCommandOptionData[]} options The options to set for this command
+ * @returns {Promise}
+ */
+ setOptions(options) {
+ return this.edit({ options });
+ }
+
+ /**
+ * Deletes this command.
+ * @returns {Promise}
+ * @example
+ * // Delete this command
+ * command.delete()
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ delete() {
+ return this.manager.delete(this, this.guildId);
+ }
+
+ /**
+ * Whether this command equals another command. It compares all properties, so for most operations
+ * it is advisable to just compare `command.id === command2.id` as it is much faster and is often
+ * what most users need.
+ * @param {ApplicationCommand|ApplicationCommandData|APIApplicationCommand} command The command to compare with
+ * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same
+ * order in the array The client may not always respect this ordering!
+ * @returns {boolean}
+ */
+ equals(command, enforceOptionOrder = false) {
+ // If given an id, check if the id matches
+ if (command.id && this.id !== command.id) return false;
+
+ // Check top level parameters
+ if (
+ command.name !== this.name ||
+ ('description' in command && command.description !== this.description) ||
+ ('version' in command && command.version !== this.version) ||
+ ('autocomplete' in command && command.autocomplete !== this.autocomplete) ||
+ (command.type && command.type !== this.type) ||
+ // Future proof for options being nullable
+ // TODO: remove ?? 0 on each when nullable
+ (command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
+ (command.defaultPermission ?? command.default_permission ?? true) !== this.defaultPermission
+ ) {
+ return false;
+ }
+
+ if (command.options) {
+ return this.constructor.optionsEqual(this.options, command.options, enforceOptionOrder);
+ }
+ return true;
+ }
+
+ /**
+ * Recursively checks that all options for an {@link ApplicationCommand} are equal to the provided options.
+ * In most cases it is better to compare using {@link ApplicationCommand#equals}
+ * @param {ApplicationCommandOptionData[]} existing The options on the existing command,
+ * should be {@link ApplicationCommand#options}
+ * @param {ApplicationCommandOptionData[]|APIApplicationCommandOption[]} options The options to compare against
+ * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same
+ * order in the array The client may not always respect this ordering!
+ * @returns {boolean}
+ */
+ static optionsEqual(existing, options, enforceOptionOrder = false) {
+ if (existing.length !== options.length) return false;
+ if (enforceOptionOrder) {
+ return existing.every((option, index) => this._optionEquals(option, options[index], enforceOptionOrder));
+ }
+ const newOptions = new Map(options.map(option => [option.name, option]));
+ for (const option of existing) {
+ const foundOption = newOptions.get(option.name);
+ if (!foundOption || !this._optionEquals(option, foundOption)) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks that an option for an {@link ApplicationCommand} is equal to the provided option
+ * In most cases it is better to compare using {@link ApplicationCommand#equals}
+ * @param {ApplicationCommandOptionData} existing The option on the existing command,
+ * should be from {@link ApplicationCommand#options}
+ * @param {ApplicationCommandOptionData|APIApplicationCommandOption} option The option to compare against
+ * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options or choices are in the same
+ * order in their array The client may not always respect this ordering!
+ * @returns {boolean}
+ * @private
+ */
+ static _optionEquals(existing, option, enforceOptionOrder = false) {
+ if (
+ option.name !== existing.name ||
+ option.type !== existing.type ||
+ option.description !== existing.description ||
+ option.autocomplete !== existing.autocomplete ||
+ (option.required ??
+ ([ApplicationCommandOptionType.Subcommand, ApplicationCommandOptionType.SubcommandGroup].includes(option.type)
+ ? undefined
+ : false)) !== existing.required ||
+ option.choices?.length !== existing.choices?.length ||
+ option.options?.length !== existing.options?.length ||
+ (option.channelTypes ?? option.channel_types)?.length !== existing.channelTypes?.length ||
+ (option.minValue ?? option.min_value) !== existing.minValue ||
+ (option.maxValue ?? option.max_value) !== existing.maxValue
+ ) {
+ return false;
+ }
+
+ if (existing.choices) {
+ if (
+ enforceOptionOrder &&
+ !existing.choices.every(
+ (choice, index) => choice.name === option.choices[index].name && choice.value === option.choices[index].value,
+ )
+ ) {
+ return false;
+ }
+ if (!enforceOptionOrder) {
+ const newChoices = new Map(option.choices.map(choice => [choice.name, choice]));
+ for (const choice of existing.choices) {
+ const foundChoice = newChoices.get(choice.name);
+ if (!foundChoice || foundChoice.value !== choice.value) return false;
+ }
+ }
+ }
+
+ if (existing.channelTypes) {
+ const newTypes = option.channelTypes ?? option.channel_types;
+ for (const type of existing.channelTypes) {
+ if (!newTypes.includes(type)) return false;
+ }
+ }
+
+ if (existing.options) {
+ return this.optionsEqual(existing.options, option.options, enforceOptionOrder);
+ }
+ return true;
+ }
+
+ /**
+ * An option for an application command or subcommand.
+ * @typedef {Object} ApplicationCommandOption
+ * @property {ApplicationCommandOptionType} type The type of the option
+ * @property {string} name The name of the option
+ * @property {string} description The description of the option
+ * @property {boolean} [required] Whether the option is required
+ * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
+ * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
+ * @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group)
+ * @property {ChannelType[]} [channelTypes] When the option type is channel,
+ * the allowed types of channels that can be selected
+ * @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ */
+
+ /**
+ * A choice for an application command option.
+ * @typedef {Object} ApplicationCommandOptionChoice
+ * @property {string} name The name of the choice
+ * @property {string|number} value The value of the choice
+ */
+
+ /**
+ * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API.
+ * @param {ApplicationCommandOptionData} option The option to transform
+ * @param {boolean} [received] Whether this option has been received from Discord
+ * @returns {APIApplicationCommandOption}
+ * @private
+ */
+ static transformOption(option, received) {
+ const channelTypesKey = received ? 'channelTypes' : 'channel_types';
+ const minValueKey = received ? 'minValue' : 'min_value';
+ const maxValueKey = received ? 'maxValue' : 'max_value';
+ return {
+ type: option.type,
+ name: option.name,
+ description: option.description,
+ required:
+ option.required ??
+ (option.type === ApplicationCommandOptionType.Subcommand ||
+ option.type === ApplicationCommandOptionType.SubcommandGroup
+ ? undefined
+ : false),
+ autocomplete: option.autocomplete,
+ choices: option.choices,
+ options: option.options?.map(o => this.transformOption(o, received)),
+ [channelTypesKey]: option.channelTypes ?? option.channel_types,
+ [minValueKey]: option.minValue ?? option.min_value,
+ [maxValueKey]: option.maxValue ?? option.max_value,
+ };
+ }
+}
+
+module.exports = ApplicationCommand;
+
+/* eslint-disable max-len */
+/**
+ * @external APIApplicationCommand
+ * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure}
+ */
+
+/**
+ * @external APIApplicationCommandOption
+ * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure}
+ */
diff --git a/src/structures/AutocompleteInteraction.js b/src/structures/AutocompleteInteraction.js
new file mode 100644
index 00000000..c05afcb
--- /dev/null
+++ b/src/structures/AutocompleteInteraction.js
@@ -0,0 +1,92 @@
+'use strict';
+
+const { InteractionResponseType, Routes } = require('discord-api-types/v9');
+const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
+const Interaction = require('./Interaction');
+
+/**
+ * Represents an autocomplete interaction.
+ * @extends {Interaction}
+ */
+class AutocompleteInteraction extends Interaction {
+ constructor(client, data) {
+ super(client, data);
+
+ /**
+ * The id of the channel this interaction was sent in
+ * @type {Snowflake}
+ * @name AutocompleteInteraction#channelId
+ */
+
+ /**
+ * The invoked application command's id
+ * @type {Snowflake}
+ */
+ this.commandId = data.data.id;
+
+ /**
+ * The invoked application command's name
+ * @type {string}
+ */
+ this.commandName = data.data.name;
+
+ /**
+ * The invoked application command's type
+ * @type {ApplicationCommandType.ChatInput}
+ */
+ this.commandType = data.data.type;
+
+ /**
+ * Whether this interaction has already received a response
+ * @type {boolean}
+ */
+ this.responded = false;
+
+ /**
+ * The options passed to the command
+ * @type {CommandInteractionOptionResolver}
+ */
+ this.options = new CommandInteractionOptionResolver(this.client, data.data.options ?? []);
+ }
+
+ /**
+ * The invoked application command, if it was fetched before
+ * @type {?ApplicationCommand}
+ */
+ get command() {
+ const id = this.commandId;
+ return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
+ }
+
+ /**
+ * Sends results for the autocomplete of this interaction.
+ * @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete
+ * @returns {Promise}
+ * @example
+ * // respond to autocomplete interaction
+ * interaction.respond([
+ * {
+ * name: 'Option 1',
+ * value: 'option1',
+ * },
+ * ])
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async respond(options) {
+ if (this.responded) throw new Error('INTERACTION_ALREADY_REPLIED');
+
+ await this.client.api.interactions(this.id, this.token).callback.post({
+ body: {
+ type: InteractionResponseType.ApplicationCommandAutocompleteResult,
+ data: {
+ choices: options,
+ },
+ },
+ auth: false,
+ })
+ this.responded = true;
+ }
+}
+
+module.exports = AutocompleteInteraction;
diff --git a/src/structures/Base.js b/src/structures/Base.js
new file mode 100644
index 00000000..cd43bf7
--- /dev/null
+++ b/src/structures/Base.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const Util = require('../util/Util');
+
+/**
+ * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
+ * @abstract
+ */
+class Base {
+ constructor(client) {
+ /**
+ * The client that instantiated this
+ * @name Base#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+ }
+
+ _clone() {
+ return Object.assign(Object.create(this), this);
+ }
+
+ _patch(data) {
+ return data;
+ }
+
+ _update(data) {
+ const clone = this._clone();
+ this._patch(data);
+ return clone;
+ }
+
+ toJSON(...props) {
+ return Util.flatten(this, ...props);
+ }
+
+ valueOf() {
+ return this.id;
+ }
+}
+
+module.exports = Base;
diff --git a/src/structures/BaseGuild.js b/src/structures/BaseGuild.js
new file mode 100644
index 00000000..e9d43d5
--- /dev/null
+++ b/src/structures/BaseGuild.js
@@ -0,0 +1,118 @@
+'use strict';
+
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const { Routes } = require('discord-api-types/v9');
+const Base = require('./Base');
+
+/**
+ * The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}.
+ * @extends {Base}
+ * @abstract
+ */
+class BaseGuild extends Base {
+ constructor(client, data) {
+ super(client);
+
+ /**
+ * The guild's id
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+
+ /**
+ * The name of this guild
+ * @type {string}
+ */
+ this.name = data.name;
+
+ /**
+ * The icon hash of this guild
+ * @type {?string}
+ */
+ this.icon = data.icon;
+
+ /**
+ * An array of features available to this guild
+ * @type {GuildFeature[]}
+ */
+ this.features = data.features;
+ }
+
+ /**
+ * The timestamp this guild was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return DiscordSnowflake.timestampFrom(this.id);
+ }
+
+ /**
+ * The time this guild was created at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * The acronym that shows up in place of a guild icon
+ * @type {string}
+ * @readonly
+ */
+ get nameAcronym() {
+ return this.name
+ .replace(/'s /g, ' ')
+ .replace(/\w+/g, e => e[0])
+ .replace(/\s/g, '');
+ }
+
+ /**
+ * Whether this guild is partnered
+ * @type {boolean}
+ * @readonly
+ */
+ get partnered() {
+ return this.features.includes('PARTNERED');
+ }
+
+ /**
+ * Whether this guild is verified
+ * @type {boolean}
+ * @readonly
+ */
+ get verified() {
+ return this.features.includes('VERIFIED');
+ }
+
+ /**
+ * The URL to this guild's icon.
+ * @param {ImageURLOptions} [options={}] Options for the image URL
+ * @returns {?string}
+ */
+ iconURL(options = {}) {
+ return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options);
+ }
+
+ /**
+ * Fetches this guild.
+ * @returns {Promise}
+ */
+ async fetch() {
+ const data = await this.client.api.guilds(this.id).get({
+ query: new URLSearchParams({ with_counts: true }),
+ });
+ return this.client.guilds._add(data);
+ }
+
+ /**
+ * When concatenated with a string, this automatically returns the guild's name instead of the Guild object.
+ * @returns {string}
+ */
+ toString() {
+ return this.name;
+ }
+}
+
+module.exports = BaseGuild;
diff --git a/src/structures/BaseGuildEmoji.js b/src/structures/BaseGuildEmoji.js
new file mode 100644
index 00000000..5a12bd9
--- /dev/null
+++ b/src/structures/BaseGuildEmoji.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const { Emoji } = require('./Emoji');
+
+/**
+ * Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}.
+ * @extends {Emoji}
+ * @abstract
+ */
+class BaseGuildEmoji extends Emoji {
+ constructor(client, data, guild) {
+ super(client, data);
+
+ /**
+ * The guild this emoji is a part of
+ * @type {Guild|GuildPreview}
+ */
+ this.guild = guild;
+
+ this.requiresColons = null;
+ this.managed = null;
+ this.available = null;
+
+ this._patch(data);
+ }
+
+ _patch(data) {
+ if ('name' in data) this.name = data.name;
+
+ if ('require_colons' in data) {
+ /**
+ * Whether or not this emoji requires colons surrounding it
+ * @type {?boolean}
+ */
+ this.requiresColons = data.require_colons;
+ }
+
+ if ('managed' in data) {
+ /**
+ * Whether this emoji is managed by an external service
+ * @type {?boolean}
+ */
+ this.managed = data.managed;
+ }
+
+ if ('available' in data) {
+ /**
+ * Whether this emoji is available
+ * @type {?boolean}
+ */
+ this.available = data.available;
+ }
+ }
+}
+
+module.exports = BaseGuildEmoji;
diff --git a/src/structures/BaseGuildTextChannel.js b/src/structures/BaseGuildTextChannel.js
new file mode 100644
index 00000000..d0f28bb
--- /dev/null
+++ b/src/structures/BaseGuildTextChannel.js
@@ -0,0 +1,229 @@
+'use strict';
+
+const GuildChannel = require('./GuildChannel');
+const TextBasedChannel = require('./interfaces/TextBasedChannel');
+const MessageManager = require('../managers/MessageManager');
+const ThreadManager = require('../managers/ThreadManager');
+
+/**
+ * Represents a text-based guild channel on Discord.
+ * @extends {GuildChannel}
+ * @implements {TextBasedChannel}
+ */
+class BaseGuildTextChannel extends GuildChannel {
+ constructor(guild, data, client) {
+ super(guild, data, client, false);
+
+ /**
+ * A manager of the messages sent to this channel
+ * @type {MessageManager}
+ */
+ this.messages = new MessageManager(this);
+
+ /**
+ * A manager of the threads belonging to this channel
+ * @type {ThreadManager}
+ */
+ this.threads = new ThreadManager(this);
+
+ /**
+ * If the guild considers this channel NSFW
+ * @type {boolean}
+ */
+ this.nsfw = Boolean(data.nsfw);
+
+ this._patch(data);
+ }
+
+ _patch(data) {
+ super._patch(data);
+
+ if ('topic' in data) {
+ /**
+ * The topic of the text channel
+ * @type {?string}
+ */
+ this.topic = data.topic;
+ }
+
+ if ('nsfw' in data) {
+ this.nsfw = Boolean(data.nsfw);
+ }
+
+ if ('last_message_id' in data) {
+ /**
+ * The last message id sent in the channel, if one was sent
+ * @type {?Snowflake}
+ */
+ this.lastMessageId = data.last_message_id;
+ }
+
+ if ('last_pin_timestamp' in data) {
+ /**
+ * The timestamp when the last pinned message was pinned, if there was one
+ * @type {?number}
+ */
+ this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null;
+ }
+
+ if ('default_auto_archive_duration' in data) {
+ /**
+ * The default auto archive duration for newly created threads in this channel
+ * @type {?ThreadAutoArchiveDuration}
+ */
+ this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
+ }
+
+ if ('messages' in data) {
+ for (const message of data.messages) this.messages._add(message);
+ }
+ }
+
+ /**
+ * Sets the default auto archive duration for all newly created threads in this channel.
+ * @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration
+ * @param {string} [reason] Reason for changing the channel's default auto archive duration
+ * @returns {Promise}
+ */
+ setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) {
+ return this.edit({ defaultAutoArchiveDuration }, reason);
+ }
+
+ /**
+ * Sets whether this channel is flagged as NSFW.
+ * @param {boolean} [nsfw=true] Whether the channel should be considered NSFW
+ * @param {string} [reason] Reason for changing the channel's NSFW flag
+ * @returns {Promise}
+ */
+ setNSFW(nsfw = true, reason) {
+ return this.edit({ nsfw }, reason);
+ }
+
+ /**
+ * Sets the type of this channel (only conversion between text and news is supported)
+ * @param {string} type The new channel type
+ * @param {string} [reason] Reason for changing the channel's type
+ * @returns {Promise}
+ */
+ setType(type, reason) {
+ return this.edit({ type }, reason);
+ }
+
+ /**
+ * Fetches all webhooks for the channel.
+ * @returns {Promise>}
+ * @example
+ * // Fetch webhooks
+ * channel.fetchWebhooks()
+ * .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
+ * .catch(console.error);
+ */
+ fetchWebhooks() {
+ return this.guild.channels.fetchWebhooks(this.id);
+ }
+
+ /**
+ * Options used to create a {@link Webhook} in a {@link TextChannel} or a {@link NewsChannel}.
+ * @typedef {Object} ChannelWebhookCreateOptions
+ * @property {?(BufferResolvable|Base64Resolvable)} [avatar] Avatar for the webhook
+ * @property {string} [reason] Reason for creating the webhook
+ */
+
+ /**
+ * Creates a webhook for the channel.
+ * @param {string} name The name of the webhook
+ * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
+ * @returns {Promise} Returns the created Webhook
+ * @example
+ * // Create a webhook for the current channel
+ * channel.createWebhook('Snek', {
+ * avatar: 'https://i.imgur.com/mI8XcpG.jpg',
+ * reason: 'Needed a cool new Webhook'
+ * })
+ * .then(console.log)
+ * .catch(console.error)
+ */
+ createWebhook(name, options = {}) {
+ return this.guild.channels.createWebhook(this.id, name, options);
+ }
+
+ /**
+ * Sets a new topic for the guild channel.
+ * @param {?string} topic The new topic for the guild channel
+ * @param {string} [reason] Reason for changing the guild channel's topic
+ * @returns {Promise}
+ * @example
+ * // Set a new channel topic
+ * channel.setTopic('needs more rate limiting')
+ * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
+ * .catch(console.error);
+ */
+ setTopic(topic, reason) {
+ return this.edit({ topic }, reason);
+ }
+
+ /**
+ * Data that can be resolved to an Application. This can be:
+ * * An Application
+ * * An Activity with associated Application
+ * * A Snowflake
+ * @typedef {Application|Snowflake} ApplicationResolvable
+ */
+
+ /**
+ * Options used to create an invite to a guild channel.
+ * @typedef {Object} CreateInviteOptions
+ * @property {boolean} [temporary] Whether members that joined via the invite should be automatically
+ * kicked after 24 hours if they have not yet received a role
+ * @property {number} [maxAge] How long the invite should last (in seconds, 0 for forever)
+ * @property {number} [maxUses] Maximum number of uses
+ * @property {boolean} [unique] Create a unique invite, or use an existing one with similar settings
+ * @property {UserResolvable} [targetUser] The user whose stream to display for this invite,
+ * required if `targetType` is {@link InviteTargetType.Stream}, the user must be streaming in the channel
+ * @property {ApplicationResolvable} [targetApplication] The embedded application to open for this invite,
+ * required if `targetType` is {@link InviteTargetType.Stream}, the application must have the
+ * {@link InviteTargetType.EmbeddedApplication} flag
+ * @property {InviteTargetType} [targetType] The type of the target for this voice channel invite
+ * @property {string} [reason] The reason for creating the invite
+ */
+
+ /**
+ * Creates an invite to this guild channel.
+ * @param {CreateInviteOptions} [options={}] The options for creating the invite
+ * @returns {Promise}
+ * @example
+ * // Create an invite to a channel
+ * channel.createInvite()
+ * .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
+ * .catch(console.error);
+ */
+ createInvite(options) {
+ return this.guild.invites.create(this.id, options);
+ }
+
+ /**
+ * Fetches a collection of invites to this guild channel.
+ * Resolves with a collection mapping invites by their codes.
+ * @param {boolean} [cache=true] Whether or not to cache the fetched invites
+ * @returns {Promise>}
+ */
+ fetchInvites(cache = true) {
+ return this.guild.invites.fetch({ channelId: this.id, cache });
+ }
+
+ // These are here only for documentation purposes - they are implemented by TextBasedChannel
+ /* eslint-disable no-empty-function */
+ get lastMessage() {}
+ get lastPinAt() {}
+ send() {}
+ sendTyping() {}
+ createMessageCollector() {}
+ awaitMessages() {}
+ createMessageComponentCollector() {}
+ awaitMessageComponent() {}
+ bulkDelete() {}
+}
+
+TextBasedChannel.applyToClass(BaseGuildTextChannel, true);
+
+module.exports = BaseGuildTextChannel;
diff --git a/src/structures/BaseGuildVoiceChannel.js b/src/structures/BaseGuildVoiceChannel.js
new file mode 100644
index 00000000..ad905cd
--- /dev/null
+++ b/src/structures/BaseGuildVoiceChannel.js
@@ -0,0 +1,123 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { PermissionFlagsBits } = require('discord-api-types/v9');
+const GuildChannel = require('./GuildChannel');
+
+/**
+ * Represents a voice-based guild channel on Discord.
+ * @extends {GuildChannel}
+ */
+class BaseGuildVoiceChannel extends GuildChannel {
+ _patch(data) {
+ super._patch(data);
+
+ if ('rtc_region' in data) {
+ /**
+ * The RTC region for this voice-based channel. This region is automatically selected if `null`.
+ * @type {?string}
+ */
+ this.rtcRegion = data.rtc_region;
+ }
+
+ if ('bitrate' in data) {
+ /**
+ * The bitrate of this voice-based channel
+ * @type {number}
+ */
+ this.bitrate = data.bitrate;
+ }
+
+ if ('user_limit' in data) {
+ /**
+ * The maximum amount of users allowed in this channel.
+ * @type {number}
+ */
+ this.userLimit = data.user_limit;
+ }
+ }
+
+ /**
+ * The members in this voice-based channel
+ * @type {Collection}
+ * @readonly
+ */
+ get members() {
+ const coll = new Collection();
+ for (const state of this.guild.voiceStates.cache.values()) {
+ if (state.channelId === this.id && state.member) {
+ coll.set(state.id, state.member);
+ }
+ }
+ return coll;
+ }
+
+ /**
+ * Checks if the voice-based channel is full
+ * @type {boolean}
+ * @readonly
+ */
+ get full() {
+ return this.userLimit > 0 && this.members.size >= this.userLimit;
+ }
+
+ /**
+ * Whether the channel is joinable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get joinable() {
+ if (!this.viewable) return false;
+ const permissions = this.permissionsFor(this.client.user);
+ if (!permissions) return false;
+
+ // This flag allows joining even if timed out
+ if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
+
+ return (
+ this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
+ permissions.has(PermissionFlagsBits.Connect, false)
+ );
+ }
+
+ /**
+ * Sets the RTC region of the channel.
+ * @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
+ * @returns {Promise}
+ * @example
+ * // Set the RTC region to europe
+ * channel.setRTCRegion('europe');
+ * @example
+ * // Remove a fixed region for this channel - let Discord decide automatically
+ * channel.setRTCRegion(null);
+ */
+ setRTCRegion(region) {
+ return this.edit({ rtcRegion: region });
+ }
+
+ /**
+ * Creates an invite to this guild channel.
+ * @param {CreateInviteOptions} [options={}] The options for creating the invite
+ * @returns {Promise}
+ * @example
+ * // Create an invite to a channel
+ * channel.createInvite()
+ * .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
+ * .catch(console.error);
+ */
+ createInvite(options) {
+ return this.guild.invites.create(this.id, options);
+ }
+
+ /**
+ * Fetches a collection of invites to this guild channel.
+ * Resolves with a collection mapping invites by their codes.
+ * @param {boolean} [cache=true] Whether or not to cache the fetched invites
+ * @returns {Promise>}
+ */
+ fetchInvites(cache = true) {
+ return this.guild.invites.fetch({ channelId: this.id, cache });
+ }
+}
+
+module.exports = BaseGuildVoiceChannel;
diff --git a/src/structures/ButtonComponent.js b/src/structures/ButtonComponent.js
new file mode 100644
index 00000000..a6cce10
--- /dev/null
+++ b/src/structures/ButtonComponent.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders');
+const Transformers = require('../util/Transformers');
+
+class ButtonComponent extends BuildersButtonComponent {
+ constructor(data) {
+ super(Transformers.toSnakeCase(data));
+ }
+}
+
+module.exports = ButtonComponent;
diff --git a/src/structures/ButtonInteraction.js b/src/structures/ButtonInteraction.js
new file mode 100644
index 00000000..db57592
--- /dev/null
+++ b/src/structures/ButtonInteraction.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const MessageComponentInteraction = require('./MessageComponentInteraction');
+
+/**
+ * Represents a button interaction.
+ * @extends {MessageComponentInteraction}
+ */
+class ButtonInteraction extends MessageComponentInteraction {}
+
+module.exports = ButtonInteraction;
diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js
new file mode 100644
index 00000000..6f53858
--- /dev/null
+++ b/src/structures/CategoryChannel.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const GuildChannel = require('./GuildChannel');
+const CategoryChannelChildManager = require('../managers/CategoryChannelChildManager');
+
+/**
+ * Represents a guild category channel on Discord.
+ * @extends {GuildChannel}
+ */
+class CategoryChannel extends GuildChannel {
+ /**
+ * A manager of the channels belonging to this category
+ * @type {CategoryChannelChildManager}
+ * @readonly
+ */
+ get children() {
+ return new CategoryChannelChildManager(this);
+ }
+
+ /**
+ * Sets the category parent of this channel.
+ * It is not currently possible to set the parent of a CategoryChannel.
+ * @method setParent
+ * @memberof CategoryChannel
+ * @instance
+ * @param {?CategoryChannelResolvable} channel The channel to set as parent
+ * @param {SetParentOptions} [options={}] The options for setting the parent
+ * @returns {Promise}
+ */
+}
+
+module.exports = CategoryChannel;
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
new file mode 100644
index 00000000..01086d3
--- /dev/null
+++ b/src/structures/Channel.js
@@ -0,0 +1,280 @@
+'use strict';
+
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const { ChannelType, Routes } = require('discord-api-types/v9');
+const Base = require('./Base');
+const { ThreadChannelTypes } = require('../util/Constants');
+let CategoryChannel;
+let DMChannel;
+let NewsChannel;
+let StageChannel;
+let StoreChannel;
+let TextChannel;
+let ThreadChannel;
+let VoiceChannel;
+
+/**
+ * Represents any channel on Discord.
+ * @extends {Base}
+ * @abstract
+ */
+class Channel extends Base {
+ constructor(client, data, immediatePatch = true) {
+ super(client);
+
+ /**
+ * The type of the channel
+ * @type {ChannelType}
+ */
+ this.type = data.type;
+
+ if (data && immediatePatch) this._patch(data);
+ }
+
+ _patch(data) {
+ /**
+ * The channel's id
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+ }
+
+ /**
+ * The timestamp the channel was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return DiscordSnowflake.timestampFrom(this.id);
+ }
+
+ /**
+ * The time the channel was created at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * The URL to the channel
+ * @type {string}
+ * @readonly
+ */
+ get url() {
+ return `https://discord.com/channels/${this.isDMBased() ? '@me' : this.guildId}/${this.id}`;
+ }
+
+ /**
+ * Whether this Channel is a partial
+ * This is always false outside of DM channels.
+ * @type {boolean}
+ * @readonly
+ */
+ get partial() {
+ return false;
+ }
+
+ /**
+ * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
+ * @returns {string}
+ * @example
+ * // Logs: Hello from <#123456789012345678>!
+ * console.log(`Hello from ${channel}!`);
+ */
+ toString() {
+ return `<#${this.id}>`;
+ }
+
+ /**
+ * Deletes this channel.
+ * @returns {Promise}
+ * @example
+ * // Delete the channel
+ * channel.delete()
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async delete() {
+ await this.client.api.channels(this.id).delete();
+ return this;
+ }
+
+ /**
+ * Fetches this channel.
+ * @param {boolean} [force=true] Whether to skip the cache check and request the API
+ * @returns {Promise}
+ */
+ fetch(force = true) {
+ return this.client.channels.fetch(this.id, { force });
+ }
+
+ /**
+ * Indicates whether this channel is a {@link TextChannel}.
+ * @returns {boolean}
+ */
+ isText() {
+ return this.type === ChannelType.GuildText;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link DMChannel}.
+ * @returns {boolean}
+ */
+ isDM() {
+ return this.type === ChannelType.DM;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link VoiceChannel}.
+ * @returns {boolean}
+ */
+ isVoice() {
+ return this.type === ChannelType.GuildVoice;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link PartialGroupDMChannel}.
+ * @returns {boolean}
+ */
+ isGroupDM() {
+ return this.type === ChannelType.GroupDM;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link CategoryChannel}.
+ * @returns {boolean}
+ */
+ isCategory() {
+ return this.type === ChannelType.GuildCategory;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link NewsChannel}.
+ * @returns {boolean}
+ */
+ isNews() {
+ return this.type === ChannelType.GuildNews;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link StoreChannel}.
+ * @returns {boolean}
+ */
+ isStore() {
+ return this.type === ChannelType.GuildStore;
+ }
+
+ /**
+ * Indicates whether this channel is a {@link ThreadChannel}.
+ * @returns {boolean}
+ */
+ isThread() {
+ return ThreadChannelTypes.includes(this.type);
+ }
+
+ /**
+ * Indicates whether this channel is a {@link StageChannel}.
+ * @returns {boolean}
+ */
+ isStage() {
+ return this.type === ChannelType.GuildStageVoice;
+ }
+
+ /**
+ * Indicates whether this channel is {@link TextBasedChannels text-based}.
+ * @returns {boolean}
+ */
+ isTextBased() {
+ return 'messages' in this;
+ }
+
+ /**
+ * Indicates whether this channel is DM-based (either a {@link DMChannel} or a {@link PartialGroupDMChannel}).
+ * @returns {boolean}
+ */
+ isDMBased() {
+ return [ChannelType.DM, ChannelType.GroupDM].includes(this.type);
+ }
+
+ /**
+ * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}.
+ * @returns {boolean}
+ */
+ isVoiceBased() {
+ return 'bitrate' in this;
+ }
+
+ static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
+ CategoryChannel ??= require('./CategoryChannel');
+ DMChannel ??= require('./DMChannel');
+ NewsChannel ??= require('./NewsChannel');
+ StageChannel ??= require('./StageChannel');
+ StoreChannel ??= require('./StoreChannel');
+ TextChannel ??= require('./TextChannel');
+ ThreadChannel ??= require('./ThreadChannel');
+ VoiceChannel ??= require('./VoiceChannel');
+
+ let channel;
+ if (!data.guild_id && !guild) {
+ if ((data.recipients && data.type !== ChannelType.GroupDM) || data.type === ChannelType.DM) {
+ channel = new DMChannel(client, data);
+ } else if (data.type === ChannelType.GroupDM) {
+ const PartialGroupDMChannel = require('./PartialGroupDMChannel');
+ channel = new PartialGroupDMChannel(client, data);
+ }
+ } else {
+ guild ??= client.guilds.cache.get(data.guild_id);
+
+ if (guild || allowUnknownGuild) {
+ switch (data.type) {
+ case ChannelType.GuildText: {
+ channel = new TextChannel(guild, data, client);
+ break;
+ }
+ case ChannelType.GuildVoice: {
+ channel = new VoiceChannel(guild, data, client);
+ break;
+ }
+ case ChannelType.GuildCategory: {
+ channel = new CategoryChannel(guild, data, client);
+ break;
+ }
+ case ChannelType.GuildNews: {
+ channel = new NewsChannel(guild, data, client);
+ break;
+ }
+ case ChannelType.GuildStore: {
+ channel = new StoreChannel(guild, data, client);
+ break;
+ }
+ case ChannelType.GuildStageVoice: {
+ channel = new StageChannel(guild, data, client);
+ break;
+ }
+ case ChannelType.GuildNewsThread:
+ case ChannelType.GuildPublicThread:
+ case ChannelType.GuildPrivateThread: {
+ channel = new ThreadChannel(guild, data, client, fromInteraction);
+ if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
+ break;
+ }
+ }
+ if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel);
+ }
+ }
+ return channel;
+ }
+
+ toJSON(...props) {
+ return super.toJSON({ createdTimestamp: true }, ...props);
+ }
+}
+
+exports.Channel = Channel;
+
+/**
+ * @external APIChannel
+ * @see {@link https://discord.com/developers/docs/resources/channel#channel-object}
+ */
diff --git a/src/structures/ChatInputCommandInteraction.js b/src/structures/ChatInputCommandInteraction.js
new file mode 100644
index 00000000..35175e4
--- /dev/null
+++ b/src/structures/ChatInputCommandInteraction.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const CommandInteraction = require('./CommandInteraction');
+const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
+
+/**
+ * Represents a command interaction.
+ * @extends {CommandInteraction}
+ */
+class ChatInputCommandInteraction extends CommandInteraction {
+ constructor(client, data) {
+ super(client, data);
+
+ /**
+ * The options passed to the command.
+ * @type {CommandInteractionOptionResolver}
+ */
+ this.options = new CommandInteractionOptionResolver(
+ this.client,
+ data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [],
+ this.transformResolved(data.data.resolved ?? {}),
+ );
+ }
+
+ /**
+ * Returns a string representation of the command interaction.
+ * This can then be copied by a user and executed again in a new command while keeping the option order.
+ * @returns {string}
+ */
+ toString() {
+ const properties = [
+ this.commandName,
+ this.options._group,
+ this.options._subcommand,
+ ...this.options._hoistedOptions.map(o => `${o.name}:${o.value}`),
+ ];
+ return `/${properties.filter(Boolean).join(' ')}`;
+ }
+}
+
+module.exports = ChatInputCommandInteraction;
diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js
new file mode 100644
index 00000000..de8270e
--- /dev/null
+++ b/src/structures/ClientApplication.js
@@ -0,0 +1,109 @@
+'use strict';
+
+const Team = require('./Team');
+const { Error } = require('../errors/DJSError');
+const Application = require('./interfaces/Application');
+const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
+const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
+
+/**
+ * Represents a Client OAuth2 Application.
+ * @extends {Application}
+ */
+class ClientApplication extends Application {
+ constructor(client, data) {
+ super(client, data);
+
+ /**
+ * The application command manager for this application
+ * @type {ApplicationCommandManager}
+ */
+ this.commands = new ApplicationCommandManager(this.client);
+ }
+
+ _patch(data) {
+ super._patch(data);
+
+ if(!data) return;
+
+ if ('flags' in data) {
+ /**
+ * The flags this application has
+ * @type {ApplicationFlagsBitField}
+ */
+ this.flags = new ApplicationFlagsBitField(data.flags).freeze();
+ }
+
+ if ('cover_image' in data) {
+ /**
+ * The hash of the application's cover image
+ * @type {?string}
+ */
+ this.cover = data.cover_image;
+ } else {
+ this.cover ??= null;
+ }
+
+ if ('rpc_origins' in data) {
+ /**
+ * The application's RPC origins, if enabled
+ * @type {string[]}
+ */
+ this.rpcOrigins = data.rpc_origins;
+ } else {
+ this.rpcOrigins ??= [];
+ }
+
+ if ('bot_require_code_grant' in data) {
+ /**
+ * If this application's bot requires a code grant when using the OAuth2 flow
+ * @type {?boolean}
+ */
+ this.botRequireCodeGrant = data.bot_require_code_grant;
+ } else {
+ this.botRequireCodeGrant ??= null;
+ }
+
+ if ('bot_public' in data) {
+ /**
+ * If this application's bot is public
+ * @type {?boolean}
+ */
+ this.botPublic = data.bot_public;
+ } else {
+ this.botPublic ??= null;
+ }
+
+ /**
+ * The owner of this OAuth application
+ * @type {?(User|Team)}
+ */
+ this.owner = data.team
+ ? new Team(this.client, data.team)
+ : data.owner
+ ? this.client.users._add(data.owner)
+ : this.owner ?? null;
+ }
+
+ /**
+ * Whether this application is partial
+ * @type {boolean}
+ * @readonly
+ */
+ get partial() {
+ return !this.name;
+ }
+
+ /**
+ * Obtains this application from Discord.
+ * @returns {Promise}
+ */
+ async fetch() {
+ if(!this.client.bot) throw new Error("INVALID_USER_METHOD");
+ const app = await this.client.api.oauth2.applications('@me').get();
+ this._patch(app);
+ return this;
+ }
+}
+
+module.exports = ClientApplication;
diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js
new file mode 100644
index 00000000..c9e67db
--- /dev/null
+++ b/src/structures/ClientPresence.js
@@ -0,0 +1,80 @@
+'use strict';
+
+const { GatewayOpcodes } = require('discord-api-types/v9');
+const { Presence } = require('./Presence');
+const { TypeError } = require('../errors');
+
+/**
+ * Represents the client's presence.
+ * @extends {Presence}
+ */
+class ClientPresence extends Presence {
+ constructor(client, data = {}) {
+ super(client, Object.assign(data, { status: data.status ?? 'online', user: { id: null } }));
+ }
+
+ /**
+ * Sets the client's presence
+ * @param {PresenceData} presence The data to set the presence to
+ * @returns {ClientPresence}
+ */
+ set(presence) {
+ const packet = this._parse(presence);
+ this._patch(packet);
+ if (typeof presence.shardId === 'undefined') {
+ this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet });
+ } else if (Array.isArray(presence.shardId)) {
+ for (const shardId of presence.shardId) {
+ this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet });
+ }
+ } else {
+ this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet });
+ }
+ return this;
+ }
+
+ /**
+ * Parses presence data into a packet ready to be sent to Discord
+ * @param {PresenceData} presence The data to parse
+ * @returns {APIPresence}
+ * @private
+ */
+ _parse({ status, since, afk, activities }) {
+ const data = {
+ activities: [],
+ afk: typeof afk === 'boolean' ? afk : false,
+ since: typeof since === 'number' && !Number.isNaN(since) ? since : null,
+ status: status ?? this.status,
+ };
+ if (activities?.length) {
+ for (const [i, activity] of activities.entries()) {
+ if (typeof activity.name !== 'string') throw new TypeError('INVALID_TYPE', `activities[${i}].name`, 'string');
+ activity.type ??= 0;
+
+ data.activities.push({
+ type: activity.type,
+ name: activity.name,
+ url: activity.url,
+ });
+ }
+ } else if (!activities && (status || afk || since) && this.activities.length) {
+ data.activities.push(
+ ...this.activities.map(a => ({
+ name: a.name,
+ type: a.type,
+ url: a.url ?? undefined,
+ })),
+ );
+ }
+
+ return data;
+ }
+}
+
+module.exports = ClientPresence;
+
+/* eslint-disable max-len */
+/**
+ * @external APIPresence
+ * @see {@link https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields}
+ */
diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js
new file mode 100644
index 00000000..30b82a4
--- /dev/null
+++ b/src/structures/ClientUser.js
@@ -0,0 +1,182 @@
+'use strict';
+
+const { Routes } = require('discord-api-types/v9');
+const User = require('./User');
+const DataResolver = require('../util/DataResolver');
+
+/**
+ * Represents the logged in client's Discord user.
+ * @extends {User}
+ */
+class ClientUser extends User {
+ _patch(data) {
+ super._patch(data);
+
+ if ('verified' in data) {
+ /**
+ * Whether or not this account has been verified
+ * @type {boolean}
+ */
+ this.verified = data.verified;
+ }
+
+ if ('mfa_enabled' in data) {
+ /**
+ * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account
+ * @type {?boolean}
+ */
+ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null;
+ } else {
+ this.mfaEnabled ??= null;
+ }
+
+ if ('token' in data) this.client.token = data.token;
+ }
+
+ /**
+ * Represents the client user's presence
+ * @type {ClientPresence}
+ * @readonly
+ */
+ get presence() {
+ return this.client.presence;
+ }
+
+ /**
+ * Data used to edit the logged in client
+ * @typedef {Object} ClientUserEditData
+ * @property {string} [username] The new username
+ * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar
+ */
+
+ /**
+ * Edits the logged in client.
+ * @param {ClientUserEditData} data The new data
+ * @returns {Promise}
+ */
+ async edit(data) {
+ if (typeof data.avatar !== 'undefined') data.avatar = await DataResolver.resolveImage(data.avatar);
+ const newData = await this.client.api.users('@me').patch({ data });
+ this.client.token = newData.token;
+ const { updated } = this.client.actions.UserUpdate.handle(newData);
+ return updated ?? this;
+ }
+
+ /**
+ * Sets the username of the logged in client.
+ * Changing usernames in Discord is heavily rate limited, with only 2 requests
+ * every hour. Use this sparingly!
+ * @param {string} username The new username
+ * @returns {Promise}
+ * @example
+ * // Set username
+ * client.user.setUsername('discordjs')
+ * .then(user => console.log(`My new username is ${user.username}`))
+ * .catch(console.error);
+ */
+ setUsername(username) {
+ return this.edit({ username });
+ }
+
+ /**
+ * Sets the avatar of the logged in client.
+ * @param {?(BufferResolvable|Base64Resolvable)} avatar The new avatar
+ * @returns {Promise}
+ * @example
+ * // Set avatar
+ * client.user.setAvatar('./avatar.png')
+ * .then(user => console.log(`New avatar set!`))
+ * .catch(console.error);
+ */
+ setAvatar(avatar) {
+ return this.edit({ avatar });
+ }
+
+ /**
+ * Options for setting activities
+ * @typedef {Object} ActivitiesOptions
+ * @property {string} [name] Name of the activity
+ * @property {ActivityType|number} [type] Type of the activity
+ * @property {string} [url] Twitch / YouTube stream URL
+ */
+
+ /**
+ * Data resembling a raw Discord presence.
+ * @typedef {Object} PresenceData
+ * @property {PresenceStatusData} [status] Status of the user
+ * @property {boolean} [afk] Whether the user is AFK
+ * @property {ActivitiesOptions[]} [activities] Activity the user is playing
+ * @property {number|number[]} [shardId] Shard id(s) to have the activity set on
+ */
+
+ /**
+ * Sets the full presence of the client user.
+ * @param {PresenceData} data Data for the presence
+ * @returns {ClientPresence}
+ * @example
+ * // Set the client user's presence
+ * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' });
+ */
+ setPresence(data) {
+ return this.client.presence.set(data);
+ }
+
+ /**
+ * A user's status. Must be one of:
+ * * `online`
+ * * `idle`
+ * * `invisible`
+ * * `dnd` (do not disturb)
+ * @typedef {string} PresenceStatusData
+ */
+
+ /**
+ * Sets the status of the client user.
+ * @param {PresenceStatusData} status Status to change to
+ * @param {number|number[]} [shardId] Shard id(s) to have the activity set on
+ * @returns {ClientPresence}
+ * @example
+ * // Set the client user's status
+ * client.user.setStatus('idle');
+ */
+ setStatus(status, shardId) {
+ return this.setPresence({ status, shardId });
+ }
+
+ /**
+ * Options for setting an activity.
+ * @typedef {Object} ActivityOptions
+ * @property {string} [name] Name of the activity
+ * @property {string} [url] Twitch / YouTube stream URL
+ * @property {ActivityType|number} [type] Type of the activity
+ * @property {number|number[]} [shardId] Shard Id(s) to have the activity set on
+ */
+
+ /**
+ * Sets the activity the client user is playing.
+ * @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity
+ * @param {ActivityOptions} [options] Options for setting the activity
+ * @returns {ClientPresence}
+ * @example
+ * // Set the client user's activity
+ * client.user.setActivity('discord.js', { type: ActivityType.Watching });
+ */
+ setActivity(name, options = {}) {
+ if (!name) return this.setPresence({ activities: [], shardId: options.shardId });
+
+ const activity = Object.assign({}, options, typeof name === 'object' ? name : { name });
+ return this.setPresence({ activities: [activity], shardId: activity.shardId });
+ }
+
+ /**
+ * Sets/removes the AFK flag for the client user.
+ * @param {boolean} [afk=true] Whether or not the user is AFK
+ * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on
+ * @returns {ClientPresence}
+ */
+ setAFK(afk = true, shardId) {
+ return this.setPresence({ afk, shardId });
+ }
+}
+
+module.exports = ClientUser;
diff --git a/src/structures/CommandInteraction.js b/src/structures/CommandInteraction.js
new file mode 100644
index 00000000..4987d09
--- /dev/null
+++ b/src/structures/CommandInteraction.js
@@ -0,0 +1,216 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const Interaction = require('./Interaction');
+const InteractionWebhook = require('./InteractionWebhook');
+const MessageAttachment = require('./MessageAttachment');
+const InteractionResponses = require('./interfaces/InteractionResponses');
+
+/**
+ * Represents a command interaction.
+ * @extends {Interaction}
+ * @implements {InteractionResponses}
+ * @abstract
+ */
+class CommandInteraction extends Interaction {
+ constructor(client, data) {
+ super(client, data);
+
+ /**
+ * The id of the channel this interaction was sent in
+ * @type {Snowflake}
+ * @name CommandInteraction#channelId
+ */
+
+ /**
+ * The invoked application command's id
+ * @type {Snowflake}
+ */
+ this.commandId = data.data.id;
+
+ /**
+ * The invoked application command's name
+ * @type {string}
+ */
+ this.commandName = data.data.name;
+
+ /**
+ * The invoked application command's type
+ * @type {ApplicationCommandType}
+ */
+ this.commandType = data.data.type;
+
+ /**
+ * Whether the reply to this interaction has been deferred
+ * @type {boolean}
+ */
+ this.deferred = false;
+
+ /**
+ * Whether this interaction has already been replied to
+ * @type {boolean}
+ */
+ this.replied = false;
+
+ /**
+ * Whether the reply to this interaction is ephemeral
+ * @type {?boolean}
+ */
+ this.ephemeral = null;
+
+ /**
+ * An associated interaction webhook, can be used to further interact with this interaction
+ * @type {InteractionWebhook}
+ */
+ this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
+ }
+
+ /**
+ * The invoked application command, if it was fetched before
+ * @type {?ApplicationCommand}
+ */
+ get command() {
+ const id = this.commandId;
+ return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
+ }
+
+ /**
+ * Represents the resolved data of a received command interaction.
+ * @typedef {Object} CommandInteractionResolvedData
+ * @property {Collection} [users] The resolved users
+ * @property {Collection} [members] The resolved guild members
+ * @property {Collection} [roles] The resolved roles
+ * @property {Collection} [channels] The resolved channels
+ * @property {Collection} [messages] The resolved messages
+ * @property {Collection} [attachments] The resolved attachments
+ */
+
+ /**
+ * Transforms the resolved received from the API.
+ * @param {APIInteractionDataResolved} resolved The received resolved objects
+ * @returns {CommandInteractionResolvedData}
+ * @private
+ */
+ transformResolved({ members, users, channels, roles, messages, attachments }) {
+ const result = {};
+
+ if (members) {
+ result.members = new Collection();
+ for (const [id, member] of Object.entries(members)) {
+ const user = users[id];
+ result.members.set(id, this.guild?.members._add({ user, ...member }) ?? member);
+ }
+ }
+
+ if (users) {
+ result.users = new Collection();
+ for (const user of Object.values(users)) {
+ result.users.set(user.id, this.client.users._add(user));
+ }
+ }
+
+ if (roles) {
+ result.roles = new Collection();
+ for (const role of Object.values(roles)) {
+ result.roles.set(role.id, this.guild?.roles._add(role) ?? role);
+ }
+ }
+
+ if (channels) {
+ result.channels = new Collection();
+ for (const channel of Object.values(channels)) {
+ result.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel);
+ }
+ }
+
+ if (messages) {
+ result.messages = new Collection();
+ for (const message of Object.values(messages)) {
+ result.messages.set(message.id, this.channel?.messages?._add(message) ?? message);
+ }
+ }
+
+ if (attachments) {
+ result.attachments = new Collection();
+ for (const attachment of Object.values(attachments)) {
+ const patched = new MessageAttachment(attachment.url, attachment.filename, attachment);
+ result.attachments.set(attachment.id, patched);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Represents an option of a received command interaction.
+ * @typedef {Object} CommandInteractionOption
+ * @property {string} name The name of the option
+ * @property {ApplicationCommandOptionType} type The type of the option
+ * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
+ * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
+ * {@link ApplicationCommandOptionType.Number} option
+ * @property {string|number|boolean} [value] The value of the option
+ * @property {CommandInteractionOption[]} [options] Additional options if this option is a
+ * subcommand (group)
+ * @property {User} [user] The resolved user
+ * @property {GuildMember|APIGuildMember} [member] The resolved member
+ * @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel
+ * @property {Role|APIRole} [role] The resolved role
+ * @property {MessageAttachment} [attachment] The resolved attachment
+ */
+
+ /**
+ * Transforms an option received from the API.
+ * @param {APIApplicationCommandOption} option The received option
+ * @param {APIInteractionDataResolved} resolved The resolved interaction data
+ * @returns {CommandInteractionOption}
+ * @private
+ */
+ transformOption(option, resolved) {
+ const result = {
+ name: option.name,
+ type: option.type,
+ };
+
+ if ('value' in option) result.value = option.value;
+ if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved));
+
+ if (resolved) {
+ const user = resolved.users?.[option.value];
+ if (user) result.user = this.client.users._add(user);
+
+ const member = resolved.members?.[option.value];
+ if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member;
+
+ const channel = resolved.channels?.[option.value];
+ if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel;
+
+ const role = resolved.roles?.[option.value];
+ if (role) result.role = this.guild?.roles._add(role) ?? role;
+
+ const attachment = resolved.attachments?.[option.value];
+ if (attachment) result.attachment = new MessageAttachment(attachment.url, attachment.filename, attachment);
+ }
+
+ return result;
+ }
+
+ // These are here only for documentation purposes - they are implemented by InteractionResponses
+ /* eslint-disable no-empty-function */
+ deferReply() {}
+ reply() {}
+ fetchReply() {}
+ editReply() {}
+ deleteReply() {}
+ followUp() {}
+}
+
+InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']);
+
+module.exports = CommandInteraction;
+
+/* eslint-disable max-len */
+/**
+ * @external APIInteractionDataResolved
+ * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure}
+ */
diff --git a/src/structures/CommandInteractionOptionResolver.js b/src/structures/CommandInteractionOptionResolver.js
new file mode 100644
index 00000000..2aa6938
--- /dev/null
+++ b/src/structures/CommandInteractionOptionResolver.js
@@ -0,0 +1,272 @@
+'use strict';
+
+const { ApplicationCommandOptionType } = require('discord-api-types/v9');
+const { TypeError } = require('../errors');
+
+/**
+ * A resolver for command interaction options.
+ */
+class CommandInteractionOptionResolver {
+ constructor(client, options, resolved) {
+ /**
+ * The client that instantiated this.
+ * @name CommandInteractionOptionResolver#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The name of the subcommand group.
+ * @type {?string}
+ * @private
+ */
+ this._group = null;
+
+ /**
+ * The name of the subcommand.
+ * @type {?string}
+ * @private
+ */
+ this._subcommand = null;
+
+ /**
+ * The bottom-level options for the interaction.
+ * If there is a subcommand (or subcommand and group), this is the options for the subcommand.
+ * @type {CommandInteractionOption[]}
+ * @private
+ */
+ this._hoistedOptions = options;
+
+ // Hoist subcommand group if present
+ if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.SubcommandGroup) {
+ this._group = this._hoistedOptions[0].name;
+ this._hoistedOptions = this._hoistedOptions[0].options ?? [];
+ }
+ // Hoist subcommand if present
+ if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.Subcommand) {
+ this._subcommand = this._hoistedOptions[0].name;
+ this._hoistedOptions = this._hoistedOptions[0].options ?? [];
+ }
+
+ /**
+ * The interaction options array.
+ * @name CommandInteractionOptionResolver#data
+ * @type {ReadonlyArray}
+ * @readonly
+ */
+ Object.defineProperty(this, 'data', { value: Object.freeze([...options]) });
+
+ /**
+ * The interaction resolved data
+ * @name CommandInteractionOptionResolver#resolved
+ * @type {Readonly}
+ */
+ Object.defineProperty(this, 'resolved', { value: Object.freeze(resolved) });
+ }
+
+ /**
+ * Gets an option by its name.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?CommandInteractionOption} The option, if found.
+ */
+ get(name, required = false) {
+ const option = this._hoistedOptions.find(opt => opt.name === name);
+ if (!option) {
+ if (required) {
+ throw new TypeError('COMMAND_INTERACTION_OPTION_NOT_FOUND', name);
+ }
+ return null;
+ }
+ return option;
+ }
+
+ /**
+ * Gets an option by name and property and checks its type.
+ * @param {string} name The name of the option.
+ * @param {ApplicationCommandOptionType} type The type of the option.
+ * @param {string[]} properties The properties to check for for `required`.
+ * @param {boolean} required Whether to throw an error if the option is not found.
+ * @returns {?CommandInteractionOption} The option, if found.
+ * @private
+ */
+ _getTypedOption(name, type, properties, required) {
+ const option = this.get(name, required);
+ if (!option) {
+ return null;
+ } else if (option.type !== type) {
+ throw new TypeError('COMMAND_INTERACTION_OPTION_TYPE', name, option.type, type);
+ } else if (required && properties.every(prop => option[prop] === null || typeof option[prop] === 'undefined')) {
+ throw new TypeError('COMMAND_INTERACTION_OPTION_EMPTY', name, option.type);
+ }
+ return option;
+ }
+
+ /**
+ * Gets the selected subcommand.
+ * @param {boolean} [required=true] Whether to throw an error if there is no subcommand.
+ * @returns {?string} The name of the selected subcommand, or null if not set and not required.
+ */
+ getSubcommand(required = true) {
+ if (required && !this._subcommand) {
+ throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND');
+ }
+ return this._subcommand;
+ }
+
+ /**
+ * Gets the selected subcommand group.
+ * @param {boolean} [required=false] Whether to throw an error if there is no subcommand group.
+ * @returns {?string} The name of the selected subcommand group, or null if not set and not required.
+ */
+ getSubcommandGroup(required = false) {
+ if (required && !this._group) {
+ throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP');
+ }
+ return this._group;
+ }
+
+ /**
+ * Gets a boolean option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?boolean} The value of the option, or null if not set and not required.
+ */
+ getBoolean(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.Boolean, ['value'], required);
+ return option?.value ?? null;
+ }
+
+ /**
+ * Gets a channel option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?(GuildChannel|ThreadChannel|APIChannel)}
+ * The value of the option, or null if not set and not required.
+ */
+ getChannel(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.Channel, ['channel'], required);
+ return option?.channel ?? null;
+ }
+
+ /**
+ * Gets a string option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?string} The value of the option, or null if not set and not required.
+ */
+ getString(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.String, ['value'], required);
+ return option?.value ?? null;
+ }
+
+ /**
+ * Gets an integer option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?number} The value of the option, or null if not set and not required.
+ */
+ getInteger(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.Integer, ['value'], required);
+ return option?.value ?? null;
+ }
+
+ /**
+ * Gets a number option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?number} The value of the option, or null if not set and not required.
+ */
+ getNumber(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.Number, ['value'], required);
+ return option?.value ?? null;
+ }
+
+ /**
+ * Gets a user option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?User} The value of the option, or null if not set and not required.
+ */
+ getUser(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['user'], required);
+ return option?.user ?? null;
+ }
+
+ /**
+ * Gets a member option.
+ * @param {string} name The name of the option.
+ * @returns {?(GuildMember|APIGuildMember)}
+ * The value of the option, or null if the user is not present in the guild or the option is not set.
+ */
+ getMember(name) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['member'], false);
+ return option?.member ?? null;
+ }
+
+ /**
+ * Gets a role option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?(Role|APIRole)} The value of the option, or null if not set and not required.
+ */
+ getRole(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.Role, ['role'], required);
+ return option?.role ?? null;
+ }
+
+ /**
+ * Gets an attachment option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?MessageAttachment} The value of the option, or null if not set and not required.
+ */
+ getAttachment(name, required = false) {
+ const option = this._getTypedOption(name, ApplicationCommandOptionType.Attachment, ['attachment'], required);
+ return option?.attachment ?? null;
+ }
+
+ /**
+ * Gets a mentionable option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?(User|GuildMember|APIGuildMember|Role|APIRole)}
+ * The value of the option, or null if not set and not required.
+ */
+ getMentionable(name, required = false) {
+ const option = this._getTypedOption(
+ name,
+ ApplicationCommandOptionType.Mentionable,
+ ['user', 'member', 'role'],
+ required,
+ );
+ return option?.member ?? option?.user ?? option?.role ?? null;
+ }
+
+ /**
+ * Gets a message option.
+ * @param {string} name The name of the option.
+ * @param {boolean} [required=false] Whether to throw an error if the option is not found.
+ * @returns {?(Message|APIMessage)}
+ * The value of the option, or null if not set and not required.
+ */
+ getMessage(name, required = false) {
+ const option = this._getTypedOption(name, '_MESSAGE', ['message'], required);
+ return option?.message ?? null;
+ }
+
+ /**
+ * Gets the focused option.
+ * @param {boolean} [getFull=false] Whether to get the full option object
+ * @returns {string|number|ApplicationCommandOptionChoice}
+ * The value of the option, or the whole option if getFull is true
+ */
+ getFocused(getFull = false) {
+ const focusedOption = this._hoistedOptions.find(option => option.focused);
+ if (!focusedOption) throw new TypeError('AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION');
+ return getFull ? focusedOption : focusedOption.value;
+ }
+}
+
+module.exports = CommandInteractionOptionResolver;
diff --git a/src/structures/ContextMenuCommandInteraction.js b/src/structures/ContextMenuCommandInteraction.js
new file mode 100644
index 00000000..360f97e
--- /dev/null
+++ b/src/structures/ContextMenuCommandInteraction.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const { ApplicationCommandOptionType } = require('discord-api-types/v9');
+const CommandInteraction = require('./CommandInteraction');
+const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
+
+/**
+ * Represents a context menu interaction.
+ * @extends {CommandInteraction}
+ */
+class ContextMenuCommandInteraction extends CommandInteraction {
+ constructor(client, data) {
+ super(client, data);
+ /**
+ * The target of the interaction, parsed into options
+ * @type {CommandInteractionOptionResolver}
+ */
+ this.options = new CommandInteractionOptionResolver(
+ this.client,
+ this.resolveContextMenuOptions(data.data),
+ this.transformResolved(data.data.resolved),
+ );
+
+ /**
+ * The id of the target of the interaction
+ * @type {Snowflake}
+ */
+ this.targetId = data.data.target_id;
+ }
+
+ /**
+ * Resolves and transforms options received from the API for a context menu interaction.
+ * @param {APIApplicationCommandInteractionData} data The interaction data
+ * @returns {CommandInteractionOption[]}
+ * @private
+ */
+ resolveContextMenuOptions({ target_id, resolved }) {
+ const result = [];
+
+ if (resolved.users?.[target_id]) {
+ result.push(
+ this.transformOption({ name: 'user', type: ApplicationCommandOptionType.User, value: target_id }, resolved),
+ );
+ }
+
+ if (resolved.messages?.[target_id]) {
+ result.push({
+ name: 'message',
+ type: '_MESSAGE',
+ value: target_id,
+ message: this.channel?.messages._add(resolved.messages[target_id]) ?? resolved.messages[target_id],
+ });
+ }
+
+ return result;
+ }
+}
+
+module.exports = ContextMenuCommandInteraction;
diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js
new file mode 100644
index 00000000..5117a0e
--- /dev/null
+++ b/src/structures/DMChannel.js
@@ -0,0 +1,102 @@
+'use strict';
+
+const { ChannelType } = require('discord-api-types/v9');
+const { Channel } = require('./Channel');
+const TextBasedChannel = require('./interfaces/TextBasedChannel');
+const MessageManager = require('../managers/MessageManager');
+
+/**
+ * Represents a direct message channel between two users.
+ * @extends {Channel}
+ * @implements {TextBasedChannel}
+ */
+class DMChannel extends Channel {
+ constructor(client, data) {
+ super(client, data);
+
+ // Override the channel type so partials have a known type
+ this.type = ChannelType.DM;
+
+ /**
+ * A manager of the messages belonging to this channel
+ * @type {MessageManager}
+ */
+ this.messages = new MessageManager(this);
+ }
+
+ _patch(data) {
+ super._patch(data);
+
+ if (data.recipients) {
+ /**
+ * The recipient on the other end of the DM
+ * @type {User}
+ */
+ this.recipient = this.client.users._add(data.recipients[0]);
+ }
+
+ if ('last_message_id' in data) {
+ /**
+ * The channel's last message id, if one was sent
+ * @type {?Snowflake}
+ */
+ this.lastMessageId = data.last_message_id;
+ }
+
+ if ('last_pin_timestamp' in data) {
+ /**
+ * The timestamp when the last pinned message was pinned, if there was one
+ * @type {?number}
+ */
+ this.lastPinTimestamp = Date.parse(data.last_pin_timestamp);
+ } else {
+ this.lastPinTimestamp ??= null;
+ }
+ }
+
+ /**
+ * Whether this DMChannel is a partial
+ * @type {boolean}
+ * @readonly
+ */
+ get partial() {
+ return typeof this.lastMessageId === 'undefined';
+ }
+
+ /**
+ * Fetch this DMChannel.
+ * @param {boolean} [force=true] Whether to skip the cache check and request the API
+ * @returns {Promise}
+ */
+ fetch(force = true) {
+ return this.recipient.createDM(force);
+ }
+
+ /**
+ * When concatenated with a string, this automatically returns the recipient's mention instead of the
+ * DMChannel object.
+ * @returns {string}
+ * @example
+ * // Logs: Hello from <@123456789012345678>!
+ * console.log(`Hello from ${channel}!`);
+ */
+ toString() {
+ return this.recipient.toString();
+ }
+
+ // These are here only for documentation purposes - they are implemented by TextBasedChannel
+ /* eslint-disable no-empty-function */
+ get lastMessage() {}
+ get lastPinAt() {}
+ send() {}
+ sendTyping() {}
+ createMessageCollector() {}
+ awaitMessages() {}
+ createMessageComponentCollector() {}
+ awaitMessageComponent() {}
+ // Doesn't work on DM channels; bulkDelete() {}
+}
+
+TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']);
+
+module.exports = DMChannel;
diff --git a/src/structures/Embed.js b/src/structures/Embed.js
new file mode 100644
index 00000000..b200237
--- /dev/null
+++ b/src/structures/Embed.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const { Embed: BuildersEmbed } = require('@discordjs/builders');
+const Transformers = require('../util/Transformers');
+
+class Embed extends BuildersEmbed {
+ constructor(data) {
+ super(Transformers.toSnakeCase(data));
+ }
+}
+
+module.exports = Embed;
diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js
new file mode 100644
index 00000000..409d292
--- /dev/null
+++ b/src/structures/Emoji.js
@@ -0,0 +1,108 @@
+'use strict';
+
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const Base = require('./Base');
+
+/**
+ * Represents raw emoji data from the API
+ * @typedef {APIEmoji} RawEmoji
+ * @property {?Snowflake} id The emoji's id
+ * @property {?string} name The emoji's name
+ * @property {?boolean} animated Whether the emoji is animated
+ */
+
+/**
+ * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}.
+ * @extends {Base}
+ */
+class Emoji extends Base {
+ constructor(client, emoji) {
+ super(client);
+ /**
+ * Whether or not the emoji is animated
+ * @type {?boolean}
+ */
+ this.animated = emoji.animated ?? null;
+
+ /**
+ * The emoji's name
+ * @type {?string}
+ */
+ this.name = emoji.name ?? null;
+
+ /**
+ * The emoji's id
+ * @type {?Snowflake}
+ */
+ this.id = emoji.id;
+ }
+
+ /**
+ * The identifier of this emoji, used for message reactions
+ * @type {string}
+ * @readonly
+ */
+ get identifier() {
+ if (this.id) return `${this.animated ? 'a:' : ''}${this.name}:${this.id}`;
+ return encodeURIComponent(this.name);
+ }
+
+ /**
+ * The URL to the emoji file if it's a custom emoji
+ * @type {?string}
+ * @readonly
+ */
+ get url() {
+ return this.id && this.client.rest.cdn.emoji(this.id, this.animated ? 'gif' : 'png');
+ }
+
+ /**
+ * The timestamp the emoji was created at, or null if unicode
+ * @type {?number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return this.id && DiscordSnowflake.timestampFrom(this.id);
+ }
+
+ /**
+ * The time the emoji was created at, or null if unicode
+ * @type {?Date}
+ * @readonly
+ */
+ get createdAt() {
+ return this.id && new Date(this.createdTimestamp);
+ }
+
+ /**
+ * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord
+ * instead of the Emoji object.
+ * @returns {string}
+ * @example
+ * // Send a custom emoji from a guild:
+ * const emoji = guild.emojis.cache.first();
+ * msg.channel.send(`Hello! ${emoji}`);
+ * @example
+ * // Send the emoji used in a reaction to the channel the reaction is part of
+ * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`);
+ */
+ toString() {
+ return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name;
+ }
+
+ toJSON() {
+ return super.toJSON({
+ guild: 'guildId',
+ createdTimestamp: true,
+ url: true,
+ identifier: true,
+ });
+ }
+}
+
+exports.Emoji = Emoji;
+
+/**
+ * @external APIEmoji
+ * @see {@link https://discord.com/developers/docs/resources/emoji#emoji-object}
+ */
diff --git a/src/structures/Guild.js b/src/structures/Guild.js
new file mode 100644
index 00000000..bd22e98
--- /dev/null
+++ b/src/structures/Guild.js
@@ -0,0 +1,1354 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { ChannelType, GuildPremiumTier, Routes } = require('discord-api-types/v9');
+const AnonymousGuild = require('./AnonymousGuild');
+const GuildAuditLogs = require('./GuildAuditLogs');
+const GuildPreview = require('./GuildPreview');
+const GuildTemplate = require('./GuildTemplate');
+const Integration = require('./Integration');
+const Webhook = require('./Webhook');
+const WelcomeScreen = require('./WelcomeScreen');
+const { Error, TypeError } = require('../errors');
+const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager');
+const GuildBanManager = require('../managers/GuildBanManager');
+const GuildChannelManager = require('../managers/GuildChannelManager');
+const GuildEmojiManager = require('../managers/GuildEmojiManager');
+const GuildInviteManager = require('../managers/GuildInviteManager');
+const GuildMemberManager = require('../managers/GuildMemberManager');
+const GuildScheduledEventManager = require('../managers/GuildScheduledEventManager');
+const GuildStickerManager = require('../managers/GuildStickerManager');
+const PresenceManager = require('../managers/PresenceManager');
+const RoleManager = require('../managers/RoleManager');
+const StageInstanceManager = require('../managers/StageInstanceManager');
+const VoiceStateManager = require('../managers/VoiceStateManager');
+const DataResolver = require('../util/DataResolver');
+const Partials = require('../util/Partials');
+const Status = require('../util/Status');
+const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField');
+const Util = require('../util/Util');
+
+/**
+ * Represents a guild (or a server) on Discord.
+ * It's recommended to see if a guild is available before performing operations or reading data from it. You can
+ * check this with {@link Guild#available}.
+ * @extends {AnonymousGuild}
+ */
+class Guild extends AnonymousGuild {
+ constructor(client, data) {
+ super(client, data, false);
+
+ /**
+ * A manager of the application commands belonging to this guild
+ * @type {GuildApplicationCommandManager}
+ */
+ this.commands = new GuildApplicationCommandManager(this);
+
+ /**
+ * A manager of the members belonging to this guild
+ * @type {GuildMemberManager}
+ */
+ this.members = new GuildMemberManager(this);
+
+ /**
+ * A manager of the channels belonging to this guild
+ * @type {GuildChannelManager}
+ */
+ this.channels = new GuildChannelManager(this);
+
+ /**
+ * A manager of the bans belonging to this guild
+ * @type {GuildBanManager}
+ */
+ this.bans = new GuildBanManager(this);
+
+ /**
+ * A manager of the roles belonging to this guild
+ * @type {RoleManager}
+ */
+ this.roles = new RoleManager(this);
+
+ /**
+ * A manager of the presences belonging to this guild
+ * @type {PresenceManager}
+ */
+ this.presences = new PresenceManager(this.client);
+
+ /**
+ * A manager of the voice states of this guild
+ * @type {VoiceStateManager}
+ */
+ this.voiceStates = new VoiceStateManager(this);
+
+ /**
+ * A manager of the stage instances of this guild
+ * @type {StageInstanceManager}
+ */
+ this.stageInstances = new StageInstanceManager(this);
+
+ /**
+ * A manager of the invites of this guild
+ * @type {GuildInviteManager}
+ */
+ this.invites = new GuildInviteManager(this);
+
+ /**
+ * A manager of the scheduled events of this guild
+ * @type {GuildScheduledEventManager}
+ */
+ this.scheduledEvents = new GuildScheduledEventManager(this);
+
+ if (!data) return;
+ if (data.unavailable) {
+ /**
+ * Whether the guild is available to access. If it is not available, it indicates a server outage
+ * @type {boolean}
+ */
+ this.available = false;
+ } else {
+ this._patch(data);
+ if (!data.channels) this.available = false;
+ }
+
+ /**
+ * The id of the shard this Guild belongs to.
+ * @type {number}
+ */
+ this.shardId = data.shardId;
+ }
+
+ /**
+ * The Shard this Guild belongs to.
+ * @type {WebSocketShard}
+ * @readonly
+ */
+ get shard() {
+ return this.client.ws.shards.get(this.shardId);
+ }
+
+ _patch(data) {
+ super._patch(data);
+ this.id = data.id;
+ if ('name' in data) this.name = data.name;
+ if ('icon' in data) this.icon = data.icon;
+ if ('unavailable' in data) {
+ this.available = !data.unavailable;
+ } else {
+ this.available ??= true;
+ }
+
+ if ('discovery_splash' in data) {
+ /**
+ * The hash of the guild discovery splash image
+ * @type {?string}
+ */
+ this.discoverySplash = data.discovery_splash;
+ }
+
+ if ('member_count' in data) {
+ /**
+ * The full amount of members in this guild
+ * @type {number}
+ */
+ this.memberCount = data.member_count;
+ }
+
+ if ('large' in data) {
+ /**
+ * Whether the guild is "large" (has more than {@link WebsocketOptions large_threshold} members, 50 by default)
+ * @type {boolean}
+ */
+ this.large = Boolean(data.large);
+ }
+
+ if ('premium_progress_bar_enabled' in data) {
+ /**
+ * Whether this guild has its premium (boost) progress bar enabled
+ * @type {boolean}
+ */
+ this.premiumProgressBarEnabled = data.premium_progress_bar_enabled;
+ }
+
+ if ('application_id' in data) {
+ /**
+ * The id of the application that created this guild (if applicable)
+ * @type {?Snowflake}
+ */
+ this.applicationId = data.application_id;
+ }
+
+ if ('afk_timeout' in data) {
+ /**
+ * The time in seconds before a user is counted as "away from keyboard"
+ * @type {?number}
+ */
+ this.afkTimeout = data.afk_timeout;
+ }
+
+ if ('afk_channel_id' in data) {
+ /**
+ * The id of the voice channel where AFK members are moved
+ * @type {?Snowflake}
+ */
+ this.afkChannelId = data.afk_channel_id;
+ }
+
+ if ('system_channel_id' in data) {
+ /**
+ * The system channel's id
+ * @type {?Snowflake}
+ */
+ this.systemChannelId = data.system_channel_id;
+ }
+
+ if ('premium_tier' in data) {
+ /**
+ * The premium tier of this guild
+ * @type {GuildPremiumTier}
+ */
+ this.premiumTier = data.premium_tier;
+ }
+
+ if ('widget_enabled' in data) {
+ /**
+ * Whether widget images are enabled on this guild
+ * @type {?boolean}
+ */
+ this.widgetEnabled = data.widget_enabled;
+ }
+
+ if ('widget_channel_id' in data) {
+ /**
+ * The widget channel's id, if enabled
+ * @type {?string}
+ */
+ this.widgetChannelId = data.widget_channel_id;
+ }
+
+ if ('explicit_content_filter' in data) {
+ /**
+ * The explicit content filter level of the guild
+ * @type {GuildExplicitContentFilter}
+ */
+ this.explicitContentFilter = data.explicit_content_filter;
+ }
+
+ if ('mfa_level' in data) {
+ /**
+ * The required MFA level for this guild
+ * @type {MFALevel}
+ */
+ this.mfaLevel = data.mfa_level;
+ }
+
+ if ('joined_at' in data) {
+ /**
+ * The timestamp the client user joined the guild at
+ * @type {number}
+ */
+ this.joinedTimestamp = Date.parse(data.joined_at);
+ }
+
+ if ('default_message_notifications' in data) {
+ /**
+ * The default message notification level of the guild
+ * @type {GuildDefaultMessageNotifications}
+ */
+ this.defaultMessageNotifications = data.default_message_notifications;
+ }
+
+ if ('system_channel_flags' in data) {
+ /**
+ * The value set for the guild's system channel flags
+ * @type {Readonly}
+ */
+ this.systemChannelFlags = new SystemChannelFlagsBitField(
+ data.system_channel_flags,
+ ).freeze();
+ }
+
+ if ('max_members' in data) {
+ /**
+ * The maximum amount of members the guild can have
+ * @type {?number}
+ */
+ this.maximumMembers = data.max_members;
+ } else {
+ this.maximumMembers ??= null;
+ }
+
+ if ('max_presences' in data) {
+ /**
+ * The maximum amount of presences the guild can have (this is `null` for all but the largest of guilds)
+ * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter
+ * @type {?number}
+ */
+ this.maximumPresences = data.max_presences;
+ } else {
+ this.maximumPresences ??= null;
+ }
+
+ if ('approximate_member_count' in data) {
+ /**
+ * The approximate amount of members the guild has
+ * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter
+ * @type {?number}
+ */
+ this.approximateMemberCount = data.approximate_member_count;
+ } else {
+ this.approximateMemberCount ??= null;
+ }
+
+ if ('approximate_presence_count' in data) {
+ /**
+ * The approximate amount of presences the guild has
+ * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter
+ * @type {?number}
+ */
+ this.approximatePresenceCount = data.approximate_presence_count;
+ } else {
+ this.approximatePresenceCount ??= null;
+ }
+
+ /**
+ * The use count of the vanity URL code of the guild, if any
+ * You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it
+ * @type {?number}
+ */
+ this.vanityURLUses ??= null;
+
+ if ('rules_channel_id' in data) {
+ /**
+ * The rules channel's id for the guild
+ * @type {?Snowflake}
+ */
+ this.rulesChannelId = data.rules_channel_id;
+ }
+
+ if ('public_updates_channel_id' in data) {
+ /**
+ * The community updates channel's id for the guild
+ * @type {?Snowflake}
+ */
+ this.publicUpdatesChannelId = data.public_updates_channel_id;
+ }
+
+ if ('preferred_locale' in data) {
+ /**
+ * The preferred locale of the guild, defaults to `en-US`
+ * @type {string}
+ * @see {@link https://discord.com/developers/docs/reference#locales}
+ */
+ this.preferredLocale = data.preferred_locale;
+ }
+
+ if (data.channels) {
+ this.channels.cache.clear();
+ for (const rawChannel of data.channels) {
+ this.client.channels._add(rawChannel, this);
+ }
+ }
+
+ if (data.threads) {
+ for (const rawThread of data.threads) {
+ this.client.channels._add(rawThread, this);
+ }
+ }
+
+ if (data.roles) {
+ this.roles.cache.clear();
+ for (const role of data.roles) this.roles._add(role);
+ }
+
+ if (data.members) {
+ this.members.cache.clear();
+ for (const guildUser of data.members) this.members._add(guildUser);
+ }
+
+ if ('owner_id' in data) {
+ /**
+ * The user id of this guild's owner
+ * @type {Snowflake}
+ */
+ this.ownerId = data.owner_id;
+ }
+
+ if (data.presences) {
+ for (const presence of data.presences) {
+ this.presences._add(Object.assign(presence, { guild: this }));
+ }
+ }
+
+ if (data.stage_instances) {
+ this.stageInstances.cache.clear();
+ for (const stageInstance of data.stage_instances) {
+ this.stageInstances._add(stageInstance);
+ }
+ }
+
+ if (data.guild_scheduled_events) {
+ this.scheduledEvents.cache.clear();
+ for (const scheduledEvent of data.guild_scheduled_events) {
+ this.scheduledEvents._add(scheduledEvent);
+ }
+ }
+
+ if (data.voice_states) {
+ this.voiceStates.cache.clear();
+ for (const voiceState of data.voice_states) {
+ this.voiceStates._add(voiceState);
+ }
+ }
+
+ if (!this.emojis) {
+ /**
+ * A manager of the emojis belonging to this guild
+ * @type {GuildEmojiManager}
+ */
+ this.emojis = new GuildEmojiManager(this);
+ if (data.emojis) for (const emoji of data.emojis) this.emojis._add(emoji);
+ } else if (data.emojis) {
+ this.client.actions.GuildEmojisUpdate.handle({
+ guild_id: this.id,
+ emojis: data.emojis,
+ });
+ }
+
+ if (!this.stickers) {
+ /**
+ * A manager of the stickers belonging to this guild
+ * @type {GuildStickerManager}
+ */
+ this.stickers = new GuildStickerManager(this);
+ if (data.stickers)
+ for (const sticker of data.stickers) this.stickers._add(sticker);
+ } else if (data.stickers) {
+ this.client.actions.GuildStickersUpdate.handle({
+ guild_id: this.id,
+ stickers: data.stickers,
+ });
+ }
+ }
+
+ /**
+ * The time the client user joined the guild
+ * @type {Date}
+ * @readonly
+ */
+ get joinedAt() {
+ return new Date(this.joinedTimestamp);
+ }
+
+ /**
+ * Positions of the guild [User Account]
+ * @type {number}
+ * @readonly
+ */
+ get position() {
+ return (
+ this.client.setting.guildMetadata.get(this.id.toString())?.guildIndex || null
+ );
+ }
+
+ /**
+ * Folder Guilds
+ * @type {object}
+ * @readonly
+ */
+ get folder() {
+ return (
+ this.client.setting.guildMetadata.get(this.id.toString()) || {}
+ );
+ }
+
+ /**
+ * The URL to this guild's discovery splash image.
+ * @param {ImageURLOptions} [options={}] Options for the image URL
+ * @returns {?string}
+ */
+ discoverySplashURL(options = {}) {
+ return (
+ this.discoverySplash &&
+ this.client.rest.cdn.discoverySplash(
+ this.id,
+ this.discoverySplash,
+ options,
+ )
+ );
+ }
+
+ /**
+ * Fetches the owner of the guild.
+ * If the member object isn't needed, use {@link Guild#ownerId} instead.
+ * @param {BaseFetchOptions} [options] The options for fetching the member
+ * @returns {Promise}
+ */
+ fetchOwner(options) {
+ return this.members.fetch({ ...options, user: this.ownerId });
+ }
+
+ /**
+ * AFK voice channel for this guild
+ * @type {?VoiceChannel}
+ * @readonly
+ */
+ get afkChannel() {
+ return this.client.channels.resolve(this.afkChannelId);
+ }
+
+ /**
+ * System channel for this guild
+ * @type {?TextChannel}
+ * @readonly
+ */
+ get systemChannel() {
+ return this.client.channels.resolve(this.systemChannelId);
+ }
+
+ /**
+ * Widget channel for this guild
+ * @type {?TextChannel}
+ * @readonly
+ */
+ get widgetChannel() {
+ return this.client.channels.resolve(this.widgetChannelId);
+ }
+
+ /**
+ * Rules channel for this guild
+ * @type {?TextChannel}
+ * @readonly
+ */
+ get rulesChannel() {
+ return this.client.channels.resolve(this.rulesChannelId);
+ }
+
+ /**
+ * Public updates channel for this guild
+ * @type {?TextChannel}
+ * @readonly
+ */
+ get publicUpdatesChannel() {
+ return this.client.channels.resolve(this.publicUpdatesChannelId);
+ }
+
+ /**
+ * The client user as a GuildMember of this guild
+ * @type {?GuildMember}
+ * @readonly
+ */
+ get me() {
+ return (
+ this.members.resolve(this.client.user.id) ??
+ (this.client.options.partials.includes(Partials.GuildMember)
+ ? this.members._add({ user: { id: this.client.user.id } }, true)
+ : null)
+ );
+ }
+
+ /**
+ * The maximum bitrate available for this guild
+ * @type {number}
+ * @readonly
+ */
+ get maximumBitrate() {
+ if (this.features.includes('VIP_REGIONS')) {
+ return 384_000;
+ }
+
+ switch (this.premiumTier) {
+ case GuildPremiumTier.Tier1:
+ return 128_000;
+ case GuildPremiumTier.Tier2:
+ return 256_000;
+ case GuildPremiumTier.Tier3:
+ return 384_000;
+ default:
+ return 96_000;
+ }
+ }
+
+ /**
+ * Fetches a collection of integrations to this guild.
+ * Resolves with a collection mapping integrations by their ids.
+ * @returns {Promise>}
+ * @example
+ * // Fetch integrations
+ * guild.fetchIntegrations()
+ * .then(integrations => console.log(`Fetched ${integrations.size} integrations`))
+ * .catch(console.error);
+ */
+ async fetchIntegrations() {
+ const data = await this.client.api.guilds(this.id).integrations.get();
+ return data.reduce(
+ (collection, integration) =>
+ collection.set(
+ integration.id,
+ new Integration(this.client, integration, this),
+ ),
+ new Collection(),
+ );
+ }
+
+ /**
+ * Fetches a collection of templates from this guild.
+ * Resolves with a collection mapping templates by their codes.
+ * @returns {Promise>}
+ */
+ async fetchTemplates() {
+ const templates = await this.client.api.guilds(this.id).templates.get();
+ return templates.reduce(
+ (col, data) => col.set(data.code, new GuildTemplate(this.client, data)),
+ new Collection(),
+ );
+ }
+
+ /**
+ * Fetches the welcome screen for this guild.
+ * @returns {Promise}
+ */
+ async fetchWelcomeScreen() {
+ const data = await this.client.api.guilds(this.id, 'welcome-screen').get();
+ return new WelcomeScreen(this, data);
+ }
+
+ /**
+ * Creates a template for the guild.
+ * @param {string} name The name for the template
+ * @param {string} [description] The description for the template
+ * @returns {Promise}
+ */
+ async createTemplate(name, description) {
+ const data = await this.client.api
+ .guilds(this.id)
+ .templates.post({ body: { name, description } });
+ return new GuildTemplate(this.client, data);
+ }
+
+ /**
+ * Obtains a guild preview for this guild from Discord.
+ * @returns {Promise}
+ */
+ async fetchPreview() {
+ const data = await this.client.api.guilds(this.id).preview.get();
+ return new GuildPreview(this.client, data);
+ }
+
+ /**
+ * An object containing information about a guild's vanity invite.
+ * @typedef {Object} Vanity
+ * @property {?string} code Vanity invite code
+ * @property {number} uses How many times this invite has been used
+ */
+
+ /**
+ * Fetches the vanity URL invite object to this guild.
+ * Resolves with an object containing the vanity URL invite code and the use count
+ * @returns {Promise}
+ * @example
+ * // Fetch invite data
+ * guild.fetchVanityData()
+ * .then(res => {
+ * console.log(`Vanity URL: https://discord.gg/${res.code} with ${res.uses} uses`);
+ * })
+ * .catch(console.error);
+ */
+ async fetchVanityData() {
+ if (!this.features.includes('VANITY_URL')) {
+ throw new Error('VANITY_URL');
+ }
+ const data = await this.client.api.guilds(this.id, 'vanity-url').get();
+ this.vanityURLCode = data.code;
+ this.vanityURLUses = data.uses;
+
+ return data;
+ }
+
+ /**
+ * Fetches all webhooks for the guild.
+ * @returns {Promise>}
+ * @example
+ * // Fetch webhooks
+ * guild.fetchWebhooks()
+ * .then(webhooks => console.log(`Fetched ${webhooks.size} webhooks`))
+ * .catch(console.error);
+ */
+ async fetchWebhooks() {
+ const apiHooks = await this.client.api.guilds(this.id).webhooks.get();
+ const hooks = new Collection();
+ for (const hook of apiHooks)
+ hooks.set(hook.id, new Webhook(this.client, hook));
+ return hooks;
+ }
+
+ /**
+ * Fetches the guild widget data, requires the widget to be enabled.
+ * @returns {Promise}
+ * @example
+ * // Fetches the guild widget data
+ * guild.fetchWidget()
+ * .then(widget => console.log(`The widget shows ${widget.channels.size} channels`))
+ * .catch(console.error);
+ */
+ fetchWidget() {
+ return this.client.fetchGuildWidget(this.id);
+ }
+
+ /**
+ * Data for the Guild Widget Settings object
+ * @typedef {Object} GuildWidgetSettings
+ * @property {boolean} enabled Whether the widget is enabled
+ * @property {?GuildChannel} channel The widget invite channel
+ */
+
+ /**
+ * The Guild Widget Settings object
+ * @typedef {Object} GuildWidgetSettingsData
+ * @property {boolean} enabled Whether the widget is enabled
+ * @property {?GuildChannelResolvable} channel The widget invite channel
+ */
+
+ /**
+ * Fetches the guild widget settings.
+ * @returns {Promise}
+ * @example
+ * // Fetches the guild widget settings
+ * guild.fetchWidgetSettings()
+ * .then(widget => console.log(`The widget is ${widget.enabled ? 'enabled' : 'disabled'}`))
+ * .catch(console.error);
+ */
+ async fetchWidgetSettings() {
+ const data = await this.client.api.guilds(this.id).widget.get();
+ this.widgetEnabled = data.enabled;
+ this.widgetChannelId = data.channel_id;
+ return {
+ enabled: data.enabled,
+ channel: data.channel_id
+ ? this.channels.cache.get(data.channel_id)
+ : null,
+ };
+ }
+
+ /**
+ * Options used to fetch audit logs.
+ * @typedef {Object} GuildAuditLogsFetchOptions
+ * @property {Snowflake|GuildAuditLogsEntry} [before] Only return entries before this entry
+ * @property {number} [limit] The number of entries to return
+ * @property {UserResolvable} [user] Only return entries for actions made by this user
+ * @property {?AuditLogEvent} [type] Only return entries for this action type
+ */
+
+ /**
+ * Fetches audit logs for this guild.
+ * @param {GuildAuditLogsFetchOptions} [options={}] Options for fetching audit logs
+ * @returns {Promise}
+ * @example
+ * // Output audit log entries
+ * guild.fetchAuditLogs()
+ * .then(audit => console.log(audit.entries.first()))
+ * .catch(console.error);
+ */
+ async fetchAuditLogs(options = {}) {
+ if (options.before && options.before instanceof GuildAuditLogs.Entry)
+ options.before = options.before.id;
+
+ const query = new URLSearchParams();
+
+ if (options.before) {
+ query.set('before', options.before);
+ }
+
+ if (options.limit) {
+ query.set('limit', options.limit);
+ }
+
+ if (options.user) {
+ const id = this.client.users.resolveId(options.user);
+ if (!id) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
+ query.set('user_id', id);
+ }
+
+ if (options.type) {
+ query.set('action_type', options.type);
+ }
+
+ const data = await this.client.api
+ .guilds(this.id)
+ ['audit-logs'].get({ query });
+ return GuildAuditLogs.build(this, data);
+ }
+
+ /**
+ * The data for editing a guild.
+ * @typedef {Object} GuildEditData
+ * @property {string} [name] The name of the guild
+ * @property {VerificationLevel|number} [verificationLevel] The verification level of the guild
+ * @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter
+ * @property {VoiceChannelResolvable} [afkChannel] The AFK channel of the guild
+ * @property {TextChannelResolvable} [systemChannel] The system channel of the guild
+ * @property {number} [afkTimeout] The AFK timeout of the guild
+ * @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild
+ * @property {GuildMemberResolvable} [owner] The owner of the guild
+ * @property {?(BufferResolvable|Base64Resolvable)} [splash] The invite splash image of the guild
+ * @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery splash image of the guild
+ * @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild
+ * @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notification
+ * level of the guild
+ * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild
+ * @property {TextChannelResolvable} [rulesChannel] The rules channel of the guild
+ * @property {TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild
+ * @property {string} [preferredLocale] The preferred locale of the guild
+ * @property {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled
+ * @property {string} [description] The discovery description of the guild
+ * @property {GuildFeature[]} [features] The features of the guild
+ */
+
+ /**
+ * Data that can be resolved to a Text Channel object. This can be:
+ * * A TextChannel
+ * * A Snowflake
+ * @typedef {TextChannel|Snowflake} TextChannelResolvable
+ */
+
+ /**
+ * Data that can be resolved to a Voice Channel object. This can be:
+ * * A VoiceChannel
+ * * A Snowflake
+ * @typedef {VoiceChannel|Snowflake} VoiceChannelResolvable
+ */
+
+ /**
+ * Updates the guild with new information - e.g. a new name.
+ * @param {GuildEditData} data The data to update the guild with
+ * @param {string} [reason] Reason for editing this guild
+ * @returns {Promise}
+ * @example
+ * // Set the guild name
+ * guild.edit({
+ * name: 'Discord Guild',
+ * })
+ * .then(updated => console.log(`New guild name ${updated}`))
+ * .catch(console.error);
+ */
+ async edit(data, reason) {
+ const _data = {};
+ if (data.name) _data.name = data.name;
+ if (typeof data.verificationLevel !== 'undefined') {
+ _data.verification_level = data.verificationLevel;
+ }
+ if (typeof data.afkChannel !== 'undefined') {
+ _data.afk_channel_id = this.client.channels.resolveId(data.afkChannel);
+ }
+ if (typeof data.systemChannel !== 'undefined') {
+ _data.system_channel_id = this.client.channels.resolveId(
+ data.systemChannel,
+ );
+ }
+ if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout);
+ if (typeof data.icon !== 'undefined')
+ _data.icon = await DataResolver.resolveImage(data.icon);
+ if (data.owner) _data.owner_id = this.client.users.resolveId(data.owner);
+ if (typeof data.splash !== 'undefined')
+ _data.splash = await DataResolver.resolveImage(data.splash);
+ if (typeof data.discoverySplash !== 'undefined') {
+ _data.discovery_splash = await DataResolver.resolveImage(
+ data.discoverySplash,
+ );
+ }
+ if (typeof data.banner !== 'undefined')
+ _data.banner = await DataResolver.resolveImage(data.banner);
+ if (typeof data.explicitContentFilter !== 'undefined') {
+ _data.explicit_content_filter = data.explicitContentFilter;
+ }
+ if (typeof data.defaultMessageNotifications !== 'undefined') {
+ _data.default_message_notifications = data.defaultMessageNotifications;
+ }
+ if (typeof data.systemChannelFlags !== 'undefined') {
+ _data.system_channel_flags = SystemChannelFlagsBitField.resolve(
+ data.systemChannelFlags,
+ );
+ }
+ if (typeof data.rulesChannel !== 'undefined') {
+ _data.rules_channel_id = this.client.channels.resolveId(
+ data.rulesChannel,
+ );
+ }
+ if (typeof data.publicUpdatesChannel !== 'undefined') {
+ _data.public_updates_channel_id = this.client.channels.resolveId(
+ data.publicUpdatesChannel,
+ );
+ }
+ if (typeof data.features !== 'undefined') {
+ _data.features = data.features;
+ }
+ if (typeof data.description !== 'undefined') {
+ _data.description = data.description;
+ }
+ if (data.preferredLocale) _data.preferred_locale = data.preferredLocale;
+ if ('premiumProgressBarEnabled' in data)
+ _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled;
+ const newData = await this.client.api
+ .guilds(this.id)
+ .patch({ body: _data, reason });
+ return this.client.actions.GuildUpdate.handle(newData).updated;
+ }
+
+ /**
+ * Welcome channel data
+ * @typedef {Object} WelcomeChannelData
+ * @property {string} description The description to show for this welcome channel
+ * @property {TextChannel|NewsChannel|StoreChannel|Snowflake} channel The channel to link for this welcome channel
+ * @property {EmojiIdentifierResolvable} [emoji] The emoji to display for this welcome channel
+ */
+
+ /**
+ * Welcome screen edit data
+ * @typedef {Object} WelcomeScreenEditData
+ * @property {boolean} [enabled] Whether the welcome screen is enabled
+ * @property {string} [description] The description for the welcome screen
+ * @property {WelcomeChannelData[]} [welcomeChannels] The welcome channel data for the welcome screen
+ */
+
+ /**
+ * Data that can be resolved to a GuildTextChannel object. This can be:
+ * * A TextChannel
+ * * A NewsChannel
+ * * A Snowflake
+ * @typedef {TextChannel|NewsChannel|Snowflake} GuildTextChannelResolvable
+ */
+
+ /**
+ * Data that can be resolved to a GuildVoiceChannel object. This can be:
+ * * A VoiceChannel
+ * * A StageChannel
+ * * A Snowflake
+ * @typedef {VoiceChannel|StageChannel|Snowflake} GuildVoiceChannelResolvable
+ */
+
+ /**
+ * Updates the guild's welcome screen
+ * @param {WelcomeScreenEditData} data Data to edit the welcome screen with
+ * @returns {Promise}
+ * @example
+ * guild.editWelcomeScreen({
+ * description: 'Hello World',
+ * enabled: true,
+ * welcomeChannels: [
+ * {
+ * description: 'foobar',
+ * channel: '222197033908436994',
+ * }
+ * ],
+ * })
+ */
+ async editWelcomeScreen(data) {
+ const { enabled, description, welcomeChannels } = data;
+ const welcome_channels = welcomeChannels?.map((welcomeChannelData) => {
+ const emoji = this.emojis.resolve(welcomeChannelData.emoji);
+ return {
+ emoji_id: emoji?.id,
+ emoji_name: emoji?.name ?? welcomeChannelData.emoji,
+ channel_id: this.channels.resolveId(welcomeChannelData.channel),
+ description: welcomeChannelData.description,
+ };
+ });
+
+ const patchData = await this.client.api
+ .guilds(this.id, 'welcome-screen')
+ .patch({
+ body: {
+ welcome_channels,
+ description,
+ enabled,
+ },
+ });
+ return new WelcomeScreen(this, patchData);
+ }
+
+ /**
+ * Edits the level of the explicit content filter.
+ * @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter
+ * @param {string} [reason] Reason for changing the level of the guild's explicit content filter
+ * @returns {Promise}
+ */
+ setExplicitContentFilter(explicitContentFilter, reason) {
+ return this.edit({ explicitContentFilter }, reason);
+ }
+
+ /* eslint-disable max-len */
+ /**
+ * Edits the setting of the default message notifications of the guild.
+ * @param {DefaultMessageNotificationLevel|number} defaultMessageNotifications The new default message notification level of the guild
+ * @param {string} [reason] Reason for changing the setting of the default message notifications
+ * @returns {Promise}
+ */
+ setDefaultMessageNotifications(defaultMessageNotifications, reason) {
+ return this.edit({ defaultMessageNotifications }, reason);
+ }
+ /* eslint-enable max-len */
+
+ /**
+ * Edits the flags of the default message notifications of the guild.
+ * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications
+ * @param {string} [reason] Reason for changing the flags of the default message notifications
+ * @returns {Promise}
+ */
+ setSystemChannelFlags(systemChannelFlags, reason) {
+ return this.edit({ systemChannelFlags }, reason);
+ }
+
+ /**
+ * Edits the name of the guild.
+ * @param {string} name The new name of the guild
+ * @param {string} [reason] Reason for changing the guild's name
+ * @returns {Promise}
+ * @example
+ * // Edit the guild name
+ * guild.setName('Discord Guild')
+ * .then(updated => console.log(`Updated guild name to ${updated.name}`))
+ * .catch(console.error);
+ */
+ setName(name, reason) {
+ return this.edit({ name }, reason);
+ }
+
+ /**
+ * Edits the verification level of the guild.
+ * @param {VerificationLevel} verificationLevel The new verification level of the guild
+ * @param {string} [reason] Reason for changing the guild's verification level
+ * @returns {Promise}
+ * @example
+ * // Edit the guild verification level
+ * guild.setVerificationLevel(1)
+ * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))
+ * .catch(console.error);
+ */
+ setVerificationLevel(verificationLevel, reason) {
+ return this.edit({ verificationLevel }, reason);
+ }
+
+ /**
+ * Edits the AFK channel of the guild.
+ * @param {VoiceChannelResolvable} afkChannel The new AFK channel
+ * @param {string} [reason] Reason for changing the guild's AFK channel
+ * @returns {Promise}
+ * @example
+ * // Edit the guild AFK channel
+ * guild.setAFKChannel(channel)
+ * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel.name}`))
+ * .catch(console.error);
+ */
+ setAFKChannel(afkChannel, reason) {
+ return this.edit({ afkChannel }, reason);
+ }
+
+ /**
+ * Edits the system channel of the guild.
+ * @param {TextChannelResolvable} systemChannel The new system channel
+ * @param {string} [reason] Reason for changing the guild's system channel
+ * @returns {Promise}
+ * @example
+ * // Edit the guild system channel
+ * guild.setSystemChannel(channel)
+ * .then(updated => console.log(`Updated guild system channel to ${guild.systemChannel.name}`))
+ * .catch(console.error);
+ */
+ setSystemChannel(systemChannel, reason) {
+ return this.edit({ systemChannel }, reason);
+ }
+
+ /**
+ * Edits the AFK timeout of the guild.
+ * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK
+ * @param {string} [reason] Reason for changing the guild's AFK timeout
+ * @returns {Promise}
+ * @example
+ * // Edit the guild AFK channel
+ * guild.setAFKTimeout(60)
+ * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))
+ * .catch(console.error);
+ */
+ setAFKTimeout(afkTimeout, reason) {
+ return this.edit({ afkTimeout }, reason);
+ }
+
+ /**
+ * Sets a new guild icon.
+ * @param {?(Base64Resolvable|BufferResolvable)} icon The new icon of the guild
+ * @param {string} [reason] Reason for changing the guild's icon
+ * @returns {Promise}
+ * @example
+ * // Edit the guild icon
+ * guild.setIcon('./icon.png')
+ * .then(updated => console.log('Updated the guild icon'))
+ * .catch(console.error);
+ */
+ setIcon(icon, reason) {
+ return this.edit({ icon }, reason);
+ }
+
+ /**
+ * Sets a new owner of the guild.
+ * @param {GuildMemberResolvable} owner The new owner of the guild
+ * @param {string} [reason] Reason for setting the new owner
+ * @returns {Promise}
+ * @example
+ * // Edit the guild owner
+ * guild.setOwner(guild.members.cache.first())
+ * .then(guild => guild.fetchOwner())
+ * .then(owner => console.log(`Updated the guild owner to ${owner.displayName}`))
+ * .catch(console.error);
+ */
+ setOwner(owner, reason) {
+ return this.edit({ owner }, reason);
+ }
+
+ /**
+ * Sets a new guild invite splash image.
+ * @param {?(Base64Resolvable|BufferResolvable)} splash The new invite splash image of the guild
+ * @param {string} [reason] Reason for changing the guild's invite splash image
+ * @returns {Promise}
+ * @example
+ * // Edit the guild splash
+ * guild.setSplash('./splash.png')
+ * .then(updated => console.log('Updated the guild splash'))
+ * .catch(console.error);
+ */
+ setSplash(splash, reason) {
+ return this.edit({ splash }, reason);
+ }
+
+ /**
+ * Sets a new guild discovery splash image.
+ * @param {?(Base64Resolvable|BufferResolvable)} discoverySplash The new discovery splash image of the guild
+ * @param {string} [reason] Reason for changing the guild's discovery splash image
+ * @returns {Promise}
+ * @example
+ * // Edit the guild discovery splash
+ * guild.setDiscoverySplash('./discoverysplash.png')
+ * .then(updated => console.log('Updated the guild discovery splash'))
+ * .catch(console.error);
+ */
+ setDiscoverySplash(discoverySplash, reason) {
+ return this.edit({ discoverySplash }, reason);
+ }
+
+ /**
+ * Sets a new guild banner.
+ * @param {?(Base64Resolvable|BufferResolvable)} banner The new banner of the guild
+ * @param {string} [reason] Reason for changing the guild's banner
+ * @returns {Promise}
+ * @example
+ * guild.setBanner('./banner.png')
+ * .then(updated => console.log('Updated the guild banner'))
+ * .catch(console.error);
+ */
+ setBanner(banner, reason) {
+ return this.edit({ banner }, reason);
+ }
+
+ /**
+ * Edits the rules channel of the guild.
+ * @param {TextChannelResolvable} rulesChannel The new rules channel
+ * @param {string} [reason] Reason for changing the guild's rules channel
+ * @returns {Promise}
+ * @example
+ * // Edit the guild rules channel
+ * guild.setRulesChannel(channel)
+ * .then(updated => console.log(`Updated guild rules channel to ${guild.rulesChannel.name}`))
+ * .catch(console.error);
+ */
+ setRulesChannel(rulesChannel, reason) {
+ return this.edit({ rulesChannel }, reason);
+ }
+
+ /**
+ * Edits the community updates channel of the guild.
+ * @param {TextChannelResolvable} publicUpdatesChannel The new community updates channel
+ * @param {string} [reason] Reason for changing the guild's community updates channel
+ * @returns {Promise}
+ * @example
+ * // Edit the guild community updates channel
+ * guild.setPublicUpdatesChannel(channel)
+ * .then(updated => console.log(`Updated guild community updates channel to ${guild.publicUpdatesChannel.name}`))
+ * .catch(console.error);
+ */
+ setPublicUpdatesChannel(publicUpdatesChannel, reason) {
+ return this.edit({ publicUpdatesChannel }, reason);
+ }
+
+ /**
+ * Edits the preferred locale of the guild.
+ * @param {string} preferredLocale The new preferred locale of the guild
+ * @param {string} [reason] Reason for changing the guild's preferred locale
+ * @returns {Promise}
+ * @example
+ * // Edit the guild preferred locale
+ * guild.setPreferredLocale('en-US')
+ * .then(updated => console.log(`Updated guild preferred locale to ${guild.preferredLocale}`))
+ * .catch(console.error);
+ */
+ setPreferredLocale(preferredLocale, reason) {
+ return this.edit({ preferredLocale }, reason);
+ }
+
+ /**
+ * Edits the enabled state of the guild's premium progress bar
+ * @param {boolean} [enabled=true] The new enabled state of the guild's premium progress bar
+ * @param {string} [reason] Reason for changing the state of the guild's premium progress bar
+ * @returns {Promise}
+ */
+ setPremiumProgressBarEnabled(enabled = true, reason) {
+ return this.edit({ premiumProgressBarEnabled: enabled }, reason);
+ }
+
+ /**
+ * Edits the guild's widget settings.
+ * @param {GuildWidgetSettingsData} settings The widget settings for the guild
+ * @param {string} [reason] Reason for changing the guild's widget settings
+ * @returns {Promise}
+ */
+ async setWidgetSettings(settings, reason) {
+ await this.client.api.guilds(this.id).widget.patch({
+ body: {
+ enabled: settings.enabled,
+ channel_id: this.channels.resolveId(settings.channel),
+ },
+ reason,
+ });
+ return this;
+ }
+
+ /**
+ * Leaves the guild.
+ * @returns {Promise}
+ * @example
+ * // Leave a guild
+ * guild.leave()
+ * .then(g => console.log(`Left the guild ${g}`))
+ * .catch(console.error);
+ */
+ async leave() {
+ if (this.ownerId === this.client.user.id) throw new Error('GUILD_OWNED');
+ await this.client.api.users('@me').guilds(this.id).delete();
+ return this;
+ }
+
+ /**
+ * Deletes the guild.
+ * @returns {Promise}
+ * @example
+ * // Delete a guild
+ * guild.delete()
+ * .then(g => console.log(`Deleted the guild ${g}`))
+ * .catch(console.error);
+ */
+ async delete() {
+ await this.client.api.guilds(this.id).delete();
+ return this;
+ }
+
+ /**
+ * Whether this guild equals another guild. It compares all properties, so for most operations
+ * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often
+ * what most users need.
+ * @param {Guild} guild The guild to compare with
+ * @returns {boolean}
+ */
+ equals(guild) {
+ return (
+ guild &&
+ guild instanceof this.constructor &&
+ this.id === guild.id &&
+ this.available === guild.available &&
+ this.splash === guild.splash &&
+ this.discoverySplash === guild.discoverySplash &&
+ this.name === guild.name &&
+ this.memberCount === guild.memberCount &&
+ this.large === guild.large &&
+ this.icon === guild.icon &&
+ this.ownerId === guild.ownerId &&
+ this.verificationLevel === guild.verificationLevel &&
+ (this.features === guild.features ||
+ (this.features.length === guild.features.length &&
+ this.features.every((feat, i) => feat === guild.features[i])))
+ );
+ }
+
+ toJSON() {
+ const json = super.toJSON({
+ available: false,
+ createdTimestamp: true,
+ nameAcronym: true,
+ presences: false,
+ voiceStates: false,
+ });
+ json.iconURL = this.iconURL();
+ json.splashURL = this.splashURL();
+ json.discoverySplashURL = this.discoverySplashURL();
+ json.bannerURL = this.bannerURL();
+ return json;
+ }
+
+ /**
+ * The voice state adapter for this guild that can be used with @discordjs/voice to play audio in voice
+ * and stage channels.
+ * @type {Function}
+ * @readonly
+ */
+ get voiceAdapterCreator() {
+ return (methods) => {
+ this.client.voice.adapters.set(this.id, methods);
+ return {
+ sendPayload: (data) => {
+ if (this.shard.status !== Status.Ready) return false;
+ this.shard.send(data);
+ return true;
+ },
+ destroy: () => {
+ this.client.voice.adapters.delete(this.id);
+ },
+ };
+ };
+ }
+
+ /**
+ * Creates a collection of this guild's roles, sorted by their position and ids.
+ * @returns {Collection}
+ * @private
+ */
+ _sortedRoles() {
+ return Util.discordSort(this.roles.cache);
+ }
+
+ /**
+ * Creates a collection of this guild's or a specific category's channels, sorted by their position and ids.
+ * @param {GuildChannel} [channel] Category to get the channels of
+ * @returns {Collection}
+ * @private
+ */
+ _sortedChannels(channel) {
+ const category = channel.type === ChannelType.GuildCategory;
+ const channelTypes = [
+ ChannelType.GuildText,
+ ChannelType.GuildNews,
+ ChannelType.GuildStore,
+ ];
+ return Util.discordSort(
+ this.channels.cache.filter(
+ (c) =>
+ (channelTypes.includes(channel.type)
+ ? channelTypes.includes(c.type)
+ : c.type === channel.type) &&
+ (category || c.parent === channel.parent),
+ ),
+ );
+ }
+}
+
+exports.Guild = Guild;
+
+/**
+ * @external APIGuild
+ * @see {@link https://discord.com/developers/docs/resources/guild#guild-object}
+ */
diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js
new file mode 100644
index 00000000..199e9d1
--- /dev/null
+++ b/src/structures/GuildAuditLogs.js
@@ -0,0 +1,528 @@
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const { OverwriteType, AuditLogEvent } = require('discord-api-types/v9');
+const { GuildScheduledEvent } = require('./GuildScheduledEvent');
+const Integration = require('./Integration');
+const Invite = require('./Invite');
+const { StageInstance } = require('./StageInstance');
+const { Sticker } = require('./Sticker');
+const Webhook = require('./Webhook');
+const Partials = require('../util/Partials');
+const Util = require('../util/Util');
+
+/**
+ * The target type of an entry. Here are the available types:
+ * * Guild
+ * * Channel
+ * * User
+ * * Role
+ * * Invite
+ * * Webhook
+ * * Emoji
+ * * Message
+ * * Integration
+ * * StageInstance
+ * * Sticker
+ * * Thread
+ * * GuildScheduledEvent
+ * @typedef {string} AuditLogTargetType
+ */
+
+/**
+ * Key mirror of all available audit log targets.
+ * @name GuildAuditLogs.Targets
+ * @type {Object}
+ */
+const Targets = {
+ All: 'All',
+ Guild: 'Guild',
+ GuildScheduledEvent: 'GuildScheduledEvent',
+ Channel: 'Channel',
+ User: 'User',
+ Role: 'Role',
+ Invite: 'Invite',
+ Webhook: 'Webhook',
+ Emoji: 'Emoji',
+ Message: 'Message',
+ Integration: 'Integration',
+ StageInstance: 'StageInstance',
+ Sticker: 'Sticker',
+ Thread: 'Thread',
+ Unknown: 'Unknown',
+};
+
+/**
+ * Audit logs entries are held in this class.
+ */
+class GuildAuditLogs {
+ constructor(guild, data) {
+ if (data.users) for (const user of data.users) guild.client.users._add(user);
+ if (data.threads) for (const thread of data.threads) guild.client.channels._add(thread, guild);
+ /**
+ * Cached webhooks
+ * @type {Collection}
+ * @private
+ */
+ this.webhooks = new Collection();
+ if (data.webhooks) {
+ for (const hook of data.webhooks) {
+ this.webhooks.set(hook.id, new Webhook(guild.client, hook));
+ }
+ }
+
+ /**
+ * Cached integrations
+ * @type {Collection}
+ * @private
+ */
+ this.integrations = new Collection();
+ if (data.integrations) {
+ for (const integration of data.integrations) {
+ this.integrations.set(integration.id, new Integration(guild.client, integration, guild));
+ }
+ }
+
+ /**
+ * The entries for this guild's audit logs
+ * @type {Collection}
+ */
+ this.entries = new Collection();
+ for (const item of data.audit_log_entries) {
+ const entry = new GuildAuditLogsEntry(this, guild, item);
+ this.entries.set(entry.id, entry);
+ }
+ }
+
+ /**
+ * Handles possible promises for entry targets.
+ * @returns {Promise}
+ */
+ static async build(...args) {
+ const logs = new GuildAuditLogs(...args);
+ await Promise.all(logs.entries.map(e => e.target));
+ return logs;
+ }
+
+ /**
+ * The target of an entry. It can be one of:
+ * * A guild
+ * * A channel
+ * * A user
+ * * A role
+ * * An invite
+ * * A webhook
+ * * An emoji
+ * * A message
+ * * An integration
+ * * A stage instance
+ * * A sticker
+ * * A guild scheduled event
+ * * A thread
+ * * An object with an id key if target was deleted
+ * * An object where the keys represent either the new value or the old value
+ * @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker|
+ * GuildScheduledEvent)} AuditLogEntryTarget
+ */
+
+ /**
+ * Finds the target type from the entry action.
+ * @param {AuditLogAction} target The action target
+ * @returns {AuditLogTargetType}
+ */
+ static targetType(target) {
+ if (target < 10) return Targets.Guild;
+ if (target < 20) return Targets.Channel;
+ if (target < 30) return Targets.User;
+ if (target < 40) return Targets.Role;
+ if (target < 50) return Targets.Invite;
+ if (target < 60) return Targets.Webhook;
+ if (target < 70) return Targets.Emoji;
+ if (target < 80) return Targets.Message;
+ if (target < 83) return Targets.Integration;
+ if (target < 86) return Targets.StageInstance;
+ if (target < 100) return Targets.Sticker;
+ if (target < 110) return Targets.GuildScheduledEvent;
+ if (target < 120) return Targets.Thread;
+ return Targets.Unknown;
+ }
+
+ /**
+ * The action type of an entry, e.g. `Create`. Here are the available types:
+ * * Create
+ * * Delete
+ * * Update
+ * * All
+ * @typedef {string} AuditLogActionType
+ */
+
+ /**
+ * Finds the action type from the entry action.
+ * @param {AuditLogAction} action The action target
+ * @returns {AuditLogActionType}
+ */
+ static actionType(action) {
+ if (
+ [
+ AuditLogEvent.ChannelCreate,
+ AuditLogEvent.ChannelOverwriteCreate,
+ AuditLogEvent.MemberBanRemove,
+ AuditLogEvent.BotAdd,
+ AuditLogEvent.RoleCreate,
+ AuditLogEvent.InviteCreate,
+ AuditLogEvent.WebhookCreate,
+ AuditLogEvent.EmojiCreate,
+ AuditLogEvent.MessagePin,
+ AuditLogEvent.IntegrationCreate,
+ AuditLogEvent.StageInstanceCreate,
+ AuditLogEvent.StickerCreate,
+ AuditLogEvent.GuildScheduledEventCreate,
+ AuditLogEvent.ThreadCreate,
+ ].includes(action)
+ ) {
+ return 'Create';
+ }
+
+ if (
+ [
+ AuditLogEvent.ChannelDelete,
+ AuditLogEvent.ChannelOverwriteDelete,
+ AuditLogEvent.MemberKick,
+ AuditLogEvent.MemberPrune,
+ AuditLogEvent.MemberBanAdd,
+ AuditLogEvent.MemberDisconnect,
+ AuditLogEvent.RoleDelete,
+ AuditLogEvent.InviteDelete,
+ AuditLogEvent.WebhookDelete,
+ AuditLogEvent.EmojiDelete,
+ AuditLogEvent.MessageDelete,
+ AuditLogEvent.MessageBulkDelete,
+ AuditLogEvent.MessageUnpin,
+ AuditLogEvent.IntegrationDelete,
+ AuditLogEvent.StageInstanceDelete,
+ AuditLogEvent.StickerDelete,
+ AuditLogEvent.GuildScheduledEventDelete,
+ AuditLogEvent.ThreadDelete,
+ ].includes(action)
+ ) {
+ return 'Delete';
+ }
+
+ if (
+ [
+ AuditLogEvent.GuildUpdate,
+ AuditLogEvent.ChannelUpdate,
+ AuditLogEvent.ChannelOverwriteUpdate,
+ AuditLogEvent.MemberUpdate,
+ AuditLogEvent.MemberRoleUpdate,
+ AuditLogEvent.MemberMove,
+ AuditLogEvent.RoleUpdate,
+ AuditLogEvent.InviteUpdate,
+ AuditLogEvent.WebhookUpdate,
+ AuditLogEvent.EmojiUpdate,
+ AuditLogEvent.IntegrationUpdate,
+ AuditLogEvent.StageInstanceUpdate,
+ AuditLogEvent.StickerUpdate,
+ AuditLogEvent.GuildScheduledEventUpdate,
+ AuditLogEvent.ThreadUpdate,
+ ].includes(action)
+ ) {
+ return 'Update';
+ }
+
+ return 'All';
+ }
+
+ toJSON() {
+ return Util.flatten(this);
+ }
+}
+
+/**
+ * Audit logs entry.
+ */
+class GuildAuditLogsEntry {
+ constructor(logs, guild, data) {
+ const targetType = GuildAuditLogs.targetType(data.action_type);
+ /**
+ * The target type of this entry
+ * @type {AuditLogTargetType}
+ */
+ this.targetType = targetType;
+
+ /**
+ * The action type of this entry
+ * @type {AuditLogActionType}
+ */
+ this.actionType = GuildAuditLogs.actionType(data.action_type);
+
+ /**
+ * Specific action type of this entry in its string presentation
+ * @type {AuditLogAction}
+ */
+ this.action = Object.keys(AuditLogEvent).find(k => AuditLogEvent[k] === data.action_type);
+
+ /**
+ * The reason of this entry
+ * @type {?string}
+ */
+ this.reason = data.reason ?? null;
+
+ /**
+ * The user that executed this entry
+ * @type {?User}
+ */
+ this.executor = data.user_id
+ ? guild.client.options.partials.includes(Partials.User)
+ ? guild.client.users._add({ id: data.user_id })
+ : guild.client.users.cache.get(data.user_id)
+ : null;
+
+ /**
+ * An entry in the audit log representing a specific change.
+ * @typedef {Object} AuditLogChange
+ * @property {string} key The property that was changed, e.g. `nick` for nickname changes
+ * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname
+ * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname
+ */
+
+ /**
+ * Specific property changes
+ * @type {?AuditLogChange[]}
+ */
+ this.changes = data.changes?.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) ?? null;
+
+ /**
+ * The entry's id
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+
+ /**
+ * Any extra data from the entry
+ * @type {?(Object|Role|GuildMember)}
+ */
+ this.extra = null;
+ switch (data.action_type) {
+ case AuditLogEvent.MemberPrune:
+ this.extra = {
+ removed: Number(data.options.members_removed),
+ days: Number(data.options.delete_member_days),
+ };
+ break;
+
+ case AuditLogEvent.MemberMove:
+ case AuditLogEvent.MessageDelete:
+ case AuditLogEvent.MessageBulkDelete:
+ this.extra = {
+ channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
+ count: Number(data.options.count),
+ };
+ break;
+
+ case AuditLogEvent.MessagePin:
+ case AuditLogEvent.MessageUnpin:
+ this.extra = {
+ channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
+ messageId: data.options.message_id,
+ };
+ break;
+
+ case AuditLogEvent.MemberDisconnect:
+ this.extra = {
+ count: Number(data.options.count),
+ };
+ break;
+
+ case AuditLogEvent.ChannelOverwriteCreate:
+ case AuditLogEvent.ChannelOverwriteUpdate:
+ case AuditLogEvent.ChannelOverwriteDelete:
+ switch (data.options.type) {
+ case OverwriteType.Role:
+ this.extra = guild.roles.cache.get(data.options.id) ?? {
+ id: data.options.id,
+ name: data.options.role_name,
+ type: OverwriteType.Role,
+ };
+ break;
+
+ case OverwriteType.Member:
+ this.extra = guild.members.cache.get(data.options.id) ?? {
+ id: data.options.id,
+ type: OverwriteType.Member,
+ };
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case AuditLogEvent.StageInstanceCreate:
+ case AuditLogEvent.StageInstanceDelete:
+ case AuditLogEvent.StageInstanceUpdate:
+ this.extra = {
+ channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
+ };
+ break;
+
+ default:
+ break;
+ }
+
+ /**
+ * The target of this entry
+ * @type {?AuditLogEntryTarget}
+ */
+ this.target = null;
+ if (targetType === Targets.Unknown) {
+ this.target = this.changes.reduce((o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ }, {});
+ this.target.id = data.target_id;
+ // MemberDisconnect and similar types do not provide a target_id.
+ } else if (targetType === Targets.User && data.target_id) {
+ this.target = guild.client.options.partials.includes(Partials.User)
+ ? guild.client.users._add({ id: data.target_id })
+ : guild.client.users.cache.get(data.target_id);
+ } else if (targetType === Targets.Guild) {
+ this.target = guild.client.guilds.cache.get(data.target_id);
+ } else if (targetType === Targets.Webhook) {
+ this.target =
+ logs.webhooks.get(data.target_id) ??
+ new Webhook(
+ guild.client,
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ {
+ id: data.target_id,
+ guild_id: guild.id,
+ },
+ ),
+ );
+ } else if (targetType === Targets.Invite) {
+ let change = this.changes.find(c => c.key === 'code');
+ change = change.new ?? change.old;
+
+ this.target =
+ guild.invites.cache.get(change) ??
+ new Invite(
+ guild.client,
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ { guild },
+ ),
+ );
+ } else if (targetType === Targets.Message) {
+ // Discord sends a channel id for the MessageBulkDelete action type.
+ this.target =
+ data.action_type === AuditLogEvent.MessageBulkDelete
+ ? guild.channels.cache.get(data.target_id) ?? { id: data.target_id }
+ : guild.client.users.cache.get(data.target_id);
+ } else if (targetType === Targets.Integration) {
+ this.target =
+ logs.integrations.get(data.target_id) ??
+ new Integration(
+ guild.client,
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ { id: data.target_id },
+ ),
+ guild,
+ );
+ } else if (targetType === Targets.Channel || targetType === Targets.Thread) {
+ this.target =
+ guild.channels.cache.get(data.target_id) ??
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ { id: data.target_id },
+ );
+ } else if (targetType === Targets.StageInstance) {
+ this.target =
+ guild.stageInstances.cache.get(data.target_id) ??
+ new StageInstance(
+ guild.client,
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ {
+ id: data.target_id,
+ channel_id: data.options?.channel_id,
+ guild_id: guild.id,
+ },
+ ),
+ );
+ } else if (targetType === Targets.Sticker) {
+ this.target =
+ guild.stickers.cache.get(data.target_id) ??
+ new Sticker(
+ guild.client,
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ { id: data.target_id },
+ ),
+ );
+ } else if (targetType === Targets.GuildScheduledEvent) {
+ this.target =
+ guild.scheduledEvents.cache.get(data.target_id) ??
+ new GuildScheduledEvent(
+ guild.client,
+ this.changes.reduce(
+ (o, c) => {
+ o[c.key] = c.new ?? c.old;
+ return o;
+ },
+ { id: data.target_id, guild_id: guild.id },
+ ),
+ );
+ } else if (data.target_id) {
+ this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) ?? { id: data.target_id };
+ }
+ }
+
+ /**
+ * The timestamp this entry was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return DiscordSnowflake.timestampFrom(this.id);
+ }
+
+ /**
+ * The time this entry was created at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ toJSON() {
+ return Util.flatten(this, { createdTimestamp: true });
+ }
+}
+
+GuildAuditLogs.Targets = Targets;
+GuildAuditLogs.Entry = GuildAuditLogsEntry;
+
+module.exports = GuildAuditLogs;
diff --git a/src/structures/GuildBan.js b/src/structures/GuildBan.js
new file mode 100644
index 00000000..9c5a10e
--- /dev/null
+++ b/src/structures/GuildBan.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const Base = require('./Base');
+
+/**
+ * Represents a ban in a guild on Discord.
+ * @extends {Base}
+ */
+class GuildBan extends Base {
+ constructor(client, data, guild) {
+ super(client);
+
+ /**
+ * The guild in which the ban is
+ * @type {Guild}
+ */
+ this.guild = guild;
+
+ this._patch(data);
+ }
+
+ _patch(data) {
+ if ('user' in data) {
+ /**
+ * The user this ban applies to
+ * @type {User}
+ */
+ this.user = this.client.users._add(data.user, true);
+ }
+
+ if ('reason' in data) {
+ /**
+ * The reason for the ban
+ * @type {?string}
+ */
+ this.reason = data.reason;
+ }
+ }
+
+ /**
+ * Whether this GuildBan is partial. If the reason is not provided the value is null
+ * @type {boolean}
+ * @readonly
+ */
+ get partial() {
+ return !('reason' in this);
+ }
+
+ /**
+ * Fetches this GuildBan.
+ * @param {boolean} [force=true] Whether to skip the cache check and request the API
+ * @returns {Promise}
+ */
+ fetch(force = true) {
+ return this.guild.bans.fetch({ user: this.user, cache: true, force });
+ }
+}
+
+module.exports = GuildBan;
diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js
new file mode 100644
index 00000000..67c2f9e
--- /dev/null
+++ b/src/structures/GuildChannel.js
@@ -0,0 +1,456 @@
+'use strict';
+
+const { PermissionFlagsBits } = require('discord-api-types/v9');
+const { Channel } = require('./Channel');
+const { Error } = require('../errors');
+const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager');
+const { VoiceBasedChannelTypes } = require('../util/Constants');
+const PermissionsBitField = require('../util/PermissionsBitField');
+
+/**
+ * Represents a guild channel from any of the following:
+ * - {@link TextChannel}
+ * - {@link VoiceChannel}
+ * - {@link CategoryChannel}
+ * - {@link NewsChannel}
+ * - {@link StoreChannel}
+ * - {@link StageChannel}
+ * @extends {Channel}
+ * @abstract
+ */
+class GuildChannel extends Channel {
+ constructor(guild, data, client, immediatePatch = true) {
+ super(guild?.client ?? client, data, false);
+
+ /**
+ * The guild the channel is in
+ * @type {Guild}
+ */
+ this.guild = guild;
+
+ /**
+ * The id of the guild the channel is in
+ * @type {Snowflake}
+ */
+ this.guildId = guild?.id ?? data.guild_id;
+
+ this.parentId = this.parentId ?? null;
+ /**
+ * A manager of permission overwrites that belong to this channel
+ * @type {PermissionOverwriteManager}
+ */
+ this.permissionOverwrites = new PermissionOverwriteManager(this);
+
+ if (data && immediatePatch) this._patch(data);
+ }
+
+ _patch(data) {
+ super._patch(data);
+
+ if ('name' in data) {
+ /**
+ * The name of the guild channel
+ * @type {string}
+ */
+ this.name = data.name;
+ }
+
+ if ('position' in data) {
+ /**
+ * The raw position of the channel from Discord
+ * @type {number}
+ */
+ this.rawPosition = data.position;
+ }
+
+ if ('guild_id' in data) {
+ this.guildId = data.guild_id;
+ }
+
+ if ('parent_id' in data) {
+ /**
+ * The id of the category parent of this channel
+ * @type {?Snowflake}
+ */
+ this.parentId = data.parent_id;
+ }
+
+ if ('permission_overwrites' in data) {
+ this.permissionOverwrites.cache.clear();
+ for (const overwrite of data.permission_overwrites) {
+ this.permissionOverwrites._add(overwrite);
+ }
+ }
+ }
+
+ _clone() {
+ const clone = super._clone();
+ clone.permissionOverwrites = new PermissionOverwriteManager(clone, this.permissionOverwrites.cache.values());
+ return clone;
+ }
+
+ /**
+ * The category parent of this channel
+ * @type {?CategoryChannel}
+ * @readonly
+ */
+ get parent() {
+ return this.guild.channels.resolve(this.parentId);
+ }
+
+ /**
+ * If the permissionOverwrites match the parent channel, null if no parent
+ * @type {?boolean}
+ * @readonly
+ */
+ get permissionsLocked() {
+ if (!this.parent) return null;
+
+ // Get all overwrites
+ const overwriteIds = new Set([
+ ...this.permissionOverwrites.cache.keys(),
+ ...this.parent.permissionOverwrites.cache.keys(),
+ ]);
+
+ // Compare all overwrites
+ return [...overwriteIds].every(key => {
+ const channelVal = this.permissionOverwrites.cache.get(key);
+ const parentVal = this.parent.permissionOverwrites.cache.get(key);
+
+ // Handle empty overwrite
+ if (
+ (!channelVal &&
+ parentVal.deny.bitfield === PermissionsBitField.defaultBit &&
+ parentVal.allow.bitfield === PermissionsBitField.defaultBit) ||
+ (!parentVal &&
+ channelVal.deny.bitfield === PermissionsBitField.defaultBit &&
+ channelVal.allow.bitfield === PermissionsBitField.defaultBit)
+ ) {
+ return true;
+ }
+
+ // Compare overwrites
+ return (
+ typeof channelVal !== 'undefined' &&
+ typeof parentVal !== 'undefined' &&
+ channelVal.deny.bitfield === parentVal.deny.bitfield &&
+ channelVal.allow.bitfield === parentVal.allow.bitfield
+ );
+ });
+ }
+
+ /**
+ * The position of the channel
+ * @type {number}
+ * @readonly
+ */
+ get position() {
+ const sorted = this.guild._sortedChannels(this);
+ return [...sorted.values()].indexOf(sorted.get(this.id));
+ }
+
+ /**
+ * Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites.
+ * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for
+ * @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions
+ * @returns {?Readonly}
+ */
+ permissionsFor(memberOrRole, checkAdmin = true) {
+ const member = this.guild.members.resolve(memberOrRole);
+ if (member) return this.memberPermissions(member, checkAdmin);
+ const role = this.guild.roles.resolve(memberOrRole);
+ return role && this.rolePermissions(role, checkAdmin);
+ }
+
+ overwritesFor(member, verified = false, roles = null) {
+ if (!verified) member = this.guild.members.resolve(member);
+ if (!member) return [];
+
+ roles ??= member.roles.cache;
+ const roleOverwrites = [];
+ let memberOverwrites;
+ let everyoneOverwrites;
+
+ for (const overwrite of this.permissionOverwrites.cache.values()) {
+ if (overwrite.id === this.guild.id) {
+ everyoneOverwrites = overwrite;
+ } else if (roles.has(overwrite.id)) {
+ roleOverwrites.push(overwrite);
+ } else if (overwrite.id === member.id) {
+ memberOverwrites = overwrite;
+ }
+ }
+
+ return {
+ everyone: everyoneOverwrites,
+ roles: roleOverwrites,
+ member: memberOverwrites,
+ };
+ }
+
+ /**
+ * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites.
+ * @param {GuildMember} member The member to obtain the overall permissions for
+ * @param {boolean} checkAdmin=true Whether having `ADMINISTRATOR` will return all permissions
+ * @returns {Readonly