diff --git a/eslint.config.js b/eslint.config.js index 009cef50..221d9451 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,26 @@ export default [ { rule: "no-await-in-loop", option: ["off"] + }, + { + rule: "unicorn/custom-error-definition", + option: ["off"] + }, + { + rule: "tsdoc/syntax", + option: ["off"] + }, + { + rule: "stylistic/max-len", + option: ["off"] + }, + { + rule: "promise/prefer-await-to-callbacks", + option: ["off"] + }, + { + rule: "unicorn/no-object-as-default-parameter", + option: ["off"] } ]), ...modules, @@ -42,6 +62,30 @@ export default [ { rule: "@typescript-eslint/no-unsafe-assignment", option: ["off"] + }, + { + rule: "@typescript-eslint/class-literal-property-style", + option: ["off"] + }, + { + rule: "@typescript-eslint/no-unsafe-return", + option: ["off"] + }, + { + rule: "@typescript-eslint/no-confusing-void-expression", + option: ["off"] + }, + { + rule: "@typescript-eslint/no-unsafe-call", + option: ["off"] + }, + { + rule: "@typescript-eslint/no-unsafe-member-access", + option: ["off"] + }, + { + rule: "@typescript-eslint/strict-boolean-expressions", + option: ["off"] } ]), ...ignores diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 00000000..6e91f913 --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,170 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.7.3](https://github.com/NezuChan/library/compare/@nezuchan/core@0.7.2...@nezuchan/core@0.7.3) (2024-01-01) + +**Note:** Version bump only for package @nezuchan/core + + + + + +## [0.7.2](https://github.com/NezuChan/library/compare/@nezuchan/core@0.7.1...@nezuchan/core@0.7.2) (2024-01-01) + + +### Bug Fixes + +* **deps:** update dependency @nezuchan/constants to ^0.7.0 ([#61](https://github.com/NezuChan/library/issues/61)) ([7da15c1](https://github.com/NezuChan/library/commit/7da15c1889b523576ce9ff8b09dd30957ac1acd5)) + + + + + +## [0.7.1](https://github.com/NezuChan/library/compare/@nezuchan/core@0.7.0...@nezuchan/core@0.7.1) (2024-01-01) + + +### Bug Fixes + +* **deps:** update dependency @nezuchan/utilities to ^0.6.2 ([#60](https://github.com/NezuChan/library/issues/60)) ([ca1513a](https://github.com/NezuChan/library/commit/ca1513aba21f544a237e0e76f39f79a9e178590b)) + + + + + +# [0.7.0](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.21...@nezuchan/core@0.7.0) (2023-12-31) + + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#50](https://github.com/NezuChan/library/issues/50)) ([ce1f360](https://github.com/NezuChan/library/commit/ce1f36082841e6cb2040d7f4d6f34a1a7cd9cf23)) +* **deps:** update dependency @sapphire/pieces to v4 ([#54](https://github.com/NezuChan/library/issues/54)) ([523bfde](https://github.com/NezuChan/library/commit/523bfdeb8ffdce7667bf7fd06a9466f201f71c50)) +* **deps:** update dependency @sapphire/utilities to ^3.15.1 ([8e259bc](https://github.com/NezuChan/library/commit/8e259bc985ec313796d2856062c1393ede1fb456)) + + + + + +## [0.6.21](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.20...@nezuchan/core@0.6.21) (2023-12-07) + + +### Bug Fixes + +* update option ([0d730cd](https://github.com/NezuChan/library/commit/0d730cdf801be8282b4f2eab26f67e3953dc478d)) + + + + + +## [0.6.20](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.19...@nezuchan/core@0.6.20) (2023-12-07) + + +### Bug Fixes + +* add option to set http proxy ([a94d1a8](https://github.com/NezuChan/library/commit/a94d1a8f51a2c72453d5b92aabcbdc411de1477a)) +* **deps:** update all non-major dependencies ([ed77e7f](https://github.com/NezuChan/library/commit/ed77e7f22fbc6d32a4c136fef4c4647a02725543)) +* **deps:** update sapphire dependencies ([3a34b73](https://github.com/NezuChan/library/commit/3a34b73e086a41be67e0c1b962bc7761033435f8)) + + + + + +## [0.6.19](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.18...@nezuchan/core@0.6.19) (2023-11-18) + + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#22](https://github.com/NezuChan/library/issues/22)) ([002d346](https://github.com/NezuChan/library/commit/002d3469048b0f2df180340b11fb76233f7deaaf)) +* **deps:** update all non-major dependencies ([#35](https://github.com/NezuChan/library/issues/35)) ([2817e59](https://github.com/NezuChan/library/commit/2817e59f298aab90662d40eea94e2d80a8736241)) +* **deps:** update dependency @sapphire/pieces to ^3.10.0 ([f3cde93](https://github.com/NezuChan/library/commit/f3cde93376026fd81465f915d3052e3721336efc)) +* **deps:** update dependency @sapphire/pieces to ^3.7.1 ([0f52f03](https://github.com/NezuChan/library/commit/0f52f03d3357f0cebe1c541df748184f53b8d2c9)) +* **deps:** update dependency @sapphire/pieces to ^3.9.0 ([4bd85e8](https://github.com/NezuChan/library/commit/4bd85e86b973bbdf1294e004c75836094ea85559)) +* wrong function name and return-data ([#42](https://github.com/NezuChan/library/issues/42)) ([1d8e7d9](https://github.com/NezuChan/library/commit/1d8e7d9067a844c599b47ead1095c492e49576cf)) + + +### Features + +* append `with_counts` to fetch guild query ([#33](https://github.com/NezuChan/library/issues/33)) ([14bf58c](https://github.com/NezuChan/library/commit/14bf58c4ad38031c7911f41fdbd4f15691865587)) + + + + + +## [0.6.18](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.17...@nezuchan/core@0.6.18) (2023-09-24) + + +### Bug Fixes + +* add option to fetch if not cached ([a2b39e2](https://github.com/NezuChan/library/commit/a2b39e22da4c824088d48cd5fb9f099516d8a065)) + + + + + +## [0.6.17](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.16...@nezuchan/core@0.6.17) (2023-09-16) + + +### Bug Fixes + +* properly return channel name ([12def06](https://github.com/NezuChan/library/commit/12def06706821b90165dc2f3de5e29e64b35cada)) + + + + + +## [0.6.16](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.15...@nezuchan/core@0.6.16) (2023-09-13) + + +### Bug Fixes + +* properly sort roles based on position ([8d1d8d2](https://github.com/NezuChan/library/commit/8d1d8d273885ac9d16510a7308563da7811f81da)) + + + + + +## [0.6.15](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.14...@nezuchan/core@0.6.15) (2023-09-09) + +**Note:** Version bump only for package @nezuchan/core + + + + + +## [0.6.14](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.13...@nezuchan/core@0.6.14) (2023-09-09) + +**Note:** Version bump only for package @nezuchan/core + + + + + +## [0.6.13](https://github.com/NezuChan/library/compare/@nezuchan/core@0.6.12...@nezuchan/core@0.6.13) (2023-09-07) + + +### Bug Fixes + +* ack to amqp message ([9a4c776](https://github.com/NezuChan/library/commit/9a4c776c430cc14d06d12b361ce8557ddf0d395f)) + + +### Features + +* add socket.io to fastify-plugin ([#6](https://github.com/NezuChan/library/issues/6)) ([d1acf54](https://github.com/NezuChan/library/commit/d1acf54389abf43d2f637667d4e593a1db0eff55)) + + + + + +## 0.6.12 (2023-08-27) + + +### Bug Fixes + +* tsconfig couldnt create declaration ([274bd93](https://github.com/NezuChan/library/commit/274bd937c48d2c9fe39d2eca11aad72c8a7a9879)) + + +### Features + +* add core libs ([7e2b126](https://github.com/NezuChan/library/commit/7e2b12634e8bd279ef120717965c72e8975a1bf9)) +* use pnpm workspace version ([0593064](https://github.com/NezuChan/library/commit/05930644af446f6d82511c1ce4d921e9f800f150)) diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 00000000..e62ec04c --- /dev/null +++ b/packages/core/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/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..a764de38 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,16 @@ +
+ +Logo + +# @nezuchan/core + +**A Core Low Level API for creating Discord bots using @nezuchan/nezu-gateway.** + +[![GitHub](https://img.shields.io/github/license/nezuchan/library)](https://github.com/nezuchan/library/blob/main/LICENSE) +[![Discord](https://discordapp.com/api/guilds/785715968608567297/embed.png)](https://nezu.my.id) + +
+ +# Information +- Everything is raw based. so you gonna implement structures with your own way with provided structures or create your own. this is not batteries included library. +- Listen for a gateway dispatch `client#amqp#receiver#` on "GATEWAY_EVENT_NAME" provided by Discord. \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..fc950ec5 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@nezuchan/core", + "version": "0.7.3", + "description": "A Core Low Level API for creating Discord bots using @nezuchan/nezu-gateway.", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "build": "rimraf dist && tsc" + }, + "repository": { + "type": "git", + "url": "https://github.com/NezuChan/library" + }, + "homepage": "https://nezu.my.id", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "type": "module", + "files": [ + "dist/**", + "LICENSE", + "README.md", + "package.json", + "pnpm-lock.yaml" + ], + "author": "KagChi", + "license": "GPL-3.0", + "devDependencies": { + "@types/amqplib": "^0.10.4" + }, + "dependencies": { + "@cordis/bitfield": "^1.2.0", + "@discordjs/rest": "^2.2.0", + "@nezuchan/constants": "^0.8.0", + "@nezuchan/decorators": "^0.2.0", + "@nezuchan/kanao-schema": "workspace:^", + "@nezuchan/utilities": "^0.6.2", + "@sapphire/pieces": "^4.2.2", + "@sapphire/result": "^2.6.6", + "@sapphire/snowflake": "^3.5.3", + "@sapphire/utilities": "^3.15.3", + "amqp-connection-manager": "^4.1.14", + "discord-api-types": "^0.37.69", + "drizzle-orm": "^0.29.3", + "postgres": "^3.4.3", + "tslib": "^2.6.2" + }, + "optionalDependencies": { + "ioredis": "^5.3.2" + } +} diff --git a/packages/core/src/Enums/Events.ts b/packages/core/src/Enums/Events.ts new file mode 100644 index 00000000..ebbd1ff4 --- /dev/null +++ b/packages/core/src/Enums/Events.ts @@ -0,0 +1,3 @@ +export enum Events { + RAW = "raw" +} diff --git a/packages/core/src/Structures/Base.ts b/packages/core/src/Structures/Base.ts new file mode 100644 index 00000000..b42ed8b1 --- /dev/null +++ b/packages/core/src/Structures/Base.ts @@ -0,0 +1,19 @@ +import type { Snowflake } from "discord-api-types/v10"; +import type { Client } from "./Client.js"; + +export class Base { + public constructor( + protected readonly data: RawType & { id?: Snowflake; }, + public client: Client + ) {} + + public get id(): string { + return this.data.id!; + } + + public toJSON(): unknown { + return { + id: this.id + }; + } +} diff --git a/packages/core/src/Structures/Channels/BaseChannel.ts b/packages/core/src/Structures/Channels/BaseChannel.ts new file mode 100644 index 00000000..43919d18 --- /dev/null +++ b/packages/core/src/Structures/Channels/BaseChannel.ts @@ -0,0 +1,122 @@ +import { channelsOverwrite } from "@nezuchan/kanao-schema"; +import type { channels } from "@nezuchan/kanao-schema"; +import type { ChannelFlags, RESTPostAPIChannelMessageJSONBody } from "discord-api-types/v10"; +import { ChannelType, OverwriteType, PermissionFlagsBits } from "discord-api-types/v10"; +import { eq } from "drizzle-orm"; +import type { InferSelectModel } from "drizzle-orm"; +import { Base } from "../Base.js"; +import type { Guild } from "../Guild.js"; +import type { GuildMember } from "../GuildMember.js"; +import type { Message } from "../Message.js"; +import { PermissionsBitField } from "../PermissionsBitField.js"; +import type { TextChannel } from "./TextChannel.js"; +import type { VoiceChannel } from "./VoiceChannel.js"; + +export class BaseChannel extends Base>> { + public get guildId(): string | null | undefined { + return this.data.guildId; + } + + public get name(): string | null | undefined { + return this.data.name; + } + + public get type(): ChannelType { + return this.data.type!; + } + + public get flags(): ChannelFlags | null | undefined { + return this.data.flags; + } + + public get position(): number | null | undefined { + return this.data.position; + } + + public get parentId(): string | null | undefined { + return this.data.parentId; + } + + public async send(options: RESTPostAPIChannelMessageJSONBody): Promise { + return this.client.sendMessage(options, this.id); + } + + public isVoice(): this is VoiceChannel { + return this.type === ChannelType.GuildVoice; + } + + public isText(): this is TextChannel { + return ![ + ChannelType.GuildStageVoice, ChannelType.GuildVoice + ].includes(this.type); + } + + public isSendable(): boolean { + return ![ChannelType.GuildCategory, ChannelType.GuildDirectory, ChannelType.GuildForum].includes(this.type); + } + + public async resolveOverwrites(): Promise[]> { + return this.client.drizzle.query.channelsOverwrite.findMany({ + where: () => eq(channelsOverwrite.id, this.id) + }); + } + + public async permissionsForMember(member: GuildMember): Promise { + if (!this.guildId) return new PermissionsBitField(PermissionFlagsBits, 0n); + const guild = await this.resolveGuild(); + if (!guild) return new PermissionsBitField(PermissionFlagsBits, 0n); + + if (member.id === guild.ownerId) { + return new PermissionsBitField(PermissionFlagsBits, Object.values(PermissionFlagsBits).reduce((a, b) => a | b, 0n)); + } + + const roles = await member.resolveRoles(); + const permissions = new PermissionsBitField(PermissionFlagsBits, roles.reduce((a, b) => a | b.permissions.bits, 0n)); + + if (permissions.bits & PermissionFlagsBits.Administrator) { + return new PermissionsBitField(PermissionFlagsBits, Object.values(PermissionFlagsBits).reduce((a, b) => a | b, 0n)); + } + + const overwrites = { + everyone: { allow: 0n, deny: 0n }, + roles: { allow: 0n, deny: 0n }, + member: { allow: 0n, deny: 0n } + }; + + for (const overwrite of await this.resolveOverwrites()) { + if (overwrite.type === OverwriteType.Role && overwrite.deny !== null && overwrite.allow !== null) { + if (overwrite.id === guild.id) { + overwrites.everyone.deny |= BigInt(overwrite.deny); + overwrites.everyone.allow |= BigInt(overwrite.allow); + } else if (roles.some(x => x.id === overwrite.id)) { + overwrites.roles.deny |= BigInt(overwrite.deny); + overwrites.roles.allow |= BigInt(overwrite.allow); + } + } else if (overwrite.id === member.id && overwrite.deny !== null && overwrite.allow !== null) { + overwrites.member.deny |= BigInt(overwrite.deny); + overwrites.member.allow |= BigInt(overwrite.allow); + } + } + + return permissions + .remove(overwrites.everyone.deny) + .add(overwrites.everyone.allow) + .remove(overwrites.roles.deny) + .add(overwrites.roles.allow) + .remove(overwrites.member.deny) + .add(overwrites.member.allow) + .freeze(); + } + + public async resolveGuild({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + if (this.guildId) { + return this.client.resolveGuild({ id: this.guildId, force, cache }); + } + + return undefined; + } + + public toString(): string { + return `<#${this.id}>`; + } +} diff --git a/packages/core/src/Structures/Channels/TextChannel.ts b/packages/core/src/Structures/Channels/TextChannel.ts new file mode 100644 index 00000000..b8a8d8fd --- /dev/null +++ b/packages/core/src/Structures/Channels/TextChannel.ts @@ -0,0 +1,11 @@ +import { BaseChannel } from "./BaseChannel.js"; + +export class TextChannel extends BaseChannel { + public get nsfw(): boolean { + return Boolean(this.data.nsfw); + } + + public get topic(): string | null | undefined { + return this.data.topic; + } +} diff --git a/packages/core/src/Structures/Channels/VoiceChannel.ts b/packages/core/src/Structures/Channels/VoiceChannel.ts new file mode 100644 index 00000000..596c218e --- /dev/null +++ b/packages/core/src/Structures/Channels/VoiceChannel.ts @@ -0,0 +1,11 @@ +import { BaseChannel } from "./BaseChannel.js"; + +export class VoiceChannel extends BaseChannel { + public get bitrate(): number | null | undefined { + return this.data.bitrate; + } + + public get userLimit(): number | null | undefined { + return this.data.userLimit; + } +} diff --git a/packages/core/src/Structures/Client.ts b/packages/core/src/Structures/Client.ts new file mode 100644 index 00000000..46364762 --- /dev/null +++ b/packages/core/src/Structures/Client.ts @@ -0,0 +1,518 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable promise/prefer-await-to-then */ +/* eslint-disable stylistic/max-len */ + +import { Buffer } from "node:buffer"; +import EventEmitter from "node:events"; +import process from "node:process"; +import { URLSearchParams } from "node:url"; +import { REST } from "@discordjs/rest"; +import { RabbitMQ } from "@nezuchan/constants"; +import * as schema from "@nezuchan/kanao-schema"; +import { RoutingKey, createAmqpChannel } from "@nezuchan/utilities"; +import { Result } from "@sapphire/result"; +import type { ChannelWrapper } from "amqp-connection-manager"; +import type { Channel } from "amqplib"; +import type { APIChannel, APIGuild, APIGuildMember, APIMessage, APIUser, RESTPostAPIChannelMessageJSONBody } from "discord-api-types/v10"; +import { ChannelType, Routes } from "discord-api-types/v10"; +import { and, eq } from "drizzle-orm"; +import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { Events } from "../Enums/Events.js"; +import type { ClientOptions } from "../Typings/index.js"; +import type { BaseChannel } from "./Channels/BaseChannel.js"; +import { TextChannel } from "./Channels/TextChannel.js"; +import { VoiceChannel } from "./Channels/VoiceChannel.js"; +import { Guild } from "./Guild.js"; +import { GuildMember } from "./GuildMember.js"; +import { Message } from "./Message.js"; +import { Role } from "./Role.js"; +import { User } from "./User.js"; +import { VoiceState } from "./VoiceState.js"; + +export class Client extends EventEmitter { + public drizzle: PostgresJsDatabase; + public clientId: string; + public rest = new REST({ + api: process.env.HTTP_PROXY ?? process.env.PROXY ?? process.env.NIRN_PROXY ?? "https://discord.com/api", + rejectOnRateLimit: (process.env.PROXY ?? process.env.NIRN_PROXY) === undefined ? null : () => false + }); + + public amqp!: ChannelWrapper; + + public constructor( + public options: ClientOptions + ) { + super(); + + if (options.rest) { + this.rest.options.api = options.rest; + } + + this.drizzle = drizzle(postgres(options.databaseUrl), { schema }); + + options.token ??= process.env.DISCORD_TOKEN; + this.clientId = Buffer.from(options.token!.split(".")[0], "base64").toString(); + } + + public connect(): void { + this.amqp = createAmqpChannel(this.options.amqpUrl, { + setup: async (channel: Channel) => this.setupAmqp(channel) + }); + + this.rest.setToken(this.options.token!); + } + + public async setupAmqp(channel: Channel): Promise { + await channel.assertExchange(RabbitMQ.GATEWAY_QUEUE_SEND, "direct", { durable: false }); + const { queue } = await channel.assertQueue("", { exclusive: true }); + + await this.bindQueue(channel, queue, RabbitMQ.GATEWAY_QUEUE_SEND); + + await channel.consume(queue, message => { + if (message) { + channel.ack(message); + this.emit(Events.RAW, JSON.parse(message.content.toString())); + } + }); + } + + public async resolveMember({ force = false, fetch = true, cache, id, guildId }: { force?: boolean | undefined; fetch?: boolean; cache?: boolean | undefined; id: string; guildId: string; }): Promise { + if (force) { + const result = await Result.fromAsync(async () => this.rest.get(Routes.guildMember(guildId, id)) as unknown as Promise); + if (result.isOk()) { + const member = result.unwrap(); + if (cache) { + await this.drizzle.insert(schema.users).values({ + id: member.user!.id, + username: member.user!.username, + discriminator: member.user!.discriminator ?? null, + globalName: member.user!.global_name ?? null, + avatar: member.user!.avatar ?? null, + bot: member.user!.bot ?? false, + flags: member.user!.flags, + accentColor: member.user!.accent_color, + avatarDecoration: member.user!.avatar_decoration, + banner: member.user!.banner, + locale: member.user!.locale, + mfaEnabled: member.user!.mfa_enabled, + premiumType: member.user!.premium_type, + publicFlags: member.user!.public_flags + }).onConflictDoNothing({ target: schema.users.id }); + + await this.drizzle.insert(schema.members).values({ + id, + guildId, + avatar: member.avatar, + flags: member.flags, + communicationDisabledUntil: member.communication_disabled_until, + deaf: member.deaf, + joinedAt: member.joined_at, + mute: member.mute, + nick: member.nick, + pending: member.pending, + premiumSince: member.premium_since + }).onConflictDoUpdate({ + target: schema.members.id, + set: { + avatar: member.avatar, + flags: member.flags, + communicationDisabledUntil: member.communication_disabled_until, + deaf: member.deaf, + joinedAt: member.joined_at, + mute: member.mute, + nick: member.nick, + pending: member.pending, + premiumSince: member.premium_since + } + }); + } + return new GuildMember({ + id, + guildId, + avatar: member.avatar ?? null, + flags: member.flags as unknown as number, + communicationDisabledUntil: member.communication_disabled_until ?? null, + deaf: member.deaf, + joinedAt: member.joined_at, + mute: member.mute, + nick: member.nick ?? null, + pending: member.pending ?? false, + premiumSince: member.premium_since ?? null + }, this); + } + } + + const member = await this.drizzle.query.members.findFirst({ + where: () => and(eq(schema.members.id, id), eq(schema.members.guildId, guildId)) + }); + + if (member) { + return new GuildMember(member, this); + } + + if (fetch) { + return this.resolveMember({ id, guildId, force: true, cache: true }); + } + + return undefined; + } + + public async resolveUser({ force = false, fetch = true, cache, id }: { force?: boolean | undefined; fetch?: boolean; cache?: boolean | undefined; id: string; }): Promise { + if (force) { + const result = await Result.fromAsync(async () => this.rest.get(Routes.user(id))); + if (result.isOk()) { + const user = result.unwrap() as APIUser; + if (cache) { + await this.drizzle.insert(schema.users).values({ + id: user.id, + username: user.username, + discriminator: user?.discriminator ?? null, + globalName: user?.global_name ?? null, + avatar: user?.avatar ?? null, + bot: user?.bot ?? false, + flags: user?.flags, + accentColor: user?.accent_color, + avatarDecoration: user?.avatar_decoration, + banner: user?.banner, + locale: user?.locale, + mfaEnabled: user?.mfa_enabled, + premiumType: user?.premium_type, + publicFlags: user?.public_flags + }).onConflictDoNothing({ target: schema.users.id }); + } + return new User({ + id: user.id, + discriminator: user.discriminator, + avatar: user.avatar, + banner: user.banner, + bot: user.bot, + accentColor: user.accent_color + }, this); + } + } + + const user = await this.drizzle.query.users.findFirst({ + where: () => eq(schema.users.id, id) + }); + + if (user) { + return new User(user, this); + } + + if (fetch) { + return this.resolveUser({ id, force: true, cache: true }); + } + + return undefined; + } + + public async resolveGuild({ force = false, fetch = true, withCounts = false, cache, id }: { force?: boolean | undefined; fetch?: boolean; cache?: boolean | undefined; id: string; withCounts?: boolean; }): Promise { + if (force) { + const result = await Result.fromAsync( + async () => this.rest.get( + Routes.guild(id), + withCounts ? { query: new URLSearchParams({ with_counts: "true" }) } : {} + ) + ); + if (result.isOk()) { + const guild = result.unwrap() as APIGuild; + if (cache) { + await this.drizzle.insert(schema.guilds).values({ + id: guild.id, + name: guild.name, + banner: guild.banner, + owner: guild.owner, + ownerId: guild.owner_id, + afkChannelId: guild.afk_channel_id, + afkTimeout: guild.afk_timeout, + defaultMessageNotifications: guild.default_message_notifications, + explicitContentFilter: guild.explicit_content_filter, + icon: guild.icon, + mfaLevel: guild.mfa_level, + region: guild.region, + systemChannelId: guild.system_channel_id, + verificationLevel: guild.verification_level, + widgetChannelId: guild.widget_channel_id, + widgetEnabled: guild.widget_enabled, + approximateMemberCount: guild.approximate_member_count, + approximatePresenceCount: guild.approximate_presence_count, + description: guild.description, + discoverySplash: guild.discovery_splash, + iconHash: guild.icon_hash, + maxMembers: guild.max_members, + maxPresences: guild.max_presences, + premiumSubscriptionCount: guild.premium_subscription_count, + premiumTier: guild.premium_tier, + vanityUrlCode: guild.vanity_url_code, + nsfwLevel: guild.nsfw_level, + rulesChannelId: guild.rules_channel_id, + publicUpdatesChannelId: guild.public_updates_channel_id, + preferredLocale: guild.preferred_locale, + maxVideoChannelUsers: guild.max_video_channel_users, + permissions: guild.permissions, + premiumProgressBarEnabled: guild.premium_progress_bar_enabled, + safetyAlertChannelId: guild.safety_alerts_channel_id, + splash: guild.splash, + systemChannelFlags: guild.system_channel_flags + }).onConflictDoUpdate({ + target: schema.guilds.id, + set: { + name: guild.name, + banner: guild.banner, + owner: guild.owner, + ownerId: guild.owner_id, + afkChannelId: guild.afk_channel_id, + afkTimeout: guild.afk_timeout, + defaultMessageNotifications: guild.default_message_notifications, + explicitContentFilter: guild.explicit_content_filter, + icon: guild.icon, + mfaLevel: guild.mfa_level, + region: guild.region, + systemChannelId: guild.system_channel_id, + verificationLevel: guild.verification_level, + widgetChannelId: guild.widget_channel_id, + widgetEnabled: guild.widget_enabled, + approximateMemberCount: guild.approximate_member_count, + approximatePresenceCount: guild.approximate_presence_count, + description: guild.description, + discoverySplash: guild.discovery_splash, + iconHash: guild.icon_hash, + maxMembers: guild.max_members, + maxPresences: guild.max_presences, + premiumSubscriptionCount: guild.premium_subscription_count, + premiumTier: guild.premium_tier, + vanityUrlCode: guild.vanity_url_code, + nsfwLevel: guild.nsfw_level, + rulesChannelId: guild.rules_channel_id, + publicUpdatesChannelId: guild.public_updates_channel_id, + preferredLocale: guild.preferred_locale, + maxVideoChannelUsers: guild.max_video_channel_users, + permissions: guild.permissions, + premiumProgressBarEnabled: guild.premium_progress_bar_enabled, + safetyAlertChannelId: guild.safety_alerts_channel_id, + splash: guild.splash, + systemChannelFlags: guild.system_channel_flags + } + }); + } + return new Guild({ + id: guild.id, + name: guild.name, + banner: guild.banner, + owner: guild.owner, + ownerId: guild.owner_id, + afkChannelId: guild.afk_channel_id, + afkTimeout: guild.afk_timeout, + defaultMessageNotifications: guild.default_message_notifications, + explicitContentFilter: guild.explicit_content_filter, + icon: guild.icon, + mfaLevel: guild.mfa_level, + region: guild.region, + systemChannelId: guild.system_channel_id, + verificationLevel: guild.verification_level, + widgetChannelId: guild.widget_channel_id, + widgetEnabled: guild.widget_enabled, + approximateMemberCount: guild.approximate_member_count, + approximatePresenceCount: guild.approximate_presence_count, + description: guild.description, + discoverySplash: guild.discovery_splash, + iconHash: guild.icon_hash, + maxMembers: guild.max_members, + maxPresences: guild.max_presences, + premiumSubscriptionCount: guild.premium_subscription_count, + premiumTier: guild.premium_tier, + vanityUrlCode: guild.vanity_url_code, + nsfwLevel: guild.nsfw_level, + rulesChannelId: guild.rules_channel_id, + publicUpdatesChannelId: guild.public_updates_channel_id, + preferredLocale: guild.preferred_locale, + maxVideoChannelUsers: guild.max_video_channel_users, + permissions: guild.permissions, + premiumProgressBarEnabled: guild.premium_progress_bar_enabled, + safetyAlertChannelId: guild.safety_alerts_channel_id, + splash: guild.splash, + systemChannelFlags: guild.system_channel_flags + }, this); + } + } + + const guild = await this.drizzle.query.guilds.findFirst({ + where: () => eq(schema.guilds.id, id) + }); + + if (guild) { + return new Guild(guild, this); + } + + if (fetch) { + return this.resolveGuild({ id, force: true, cache: true }); + } + + return undefined; + } + + public async resolveRole({ id, guildId }: { id: string; guildId: string; }): Promise { + const { role } = await this.drizzle.select({ role: schema.roles }).from(schema.memberRoles) + .where(and(eq(schema.memberRoles.id, id), eq(schema.memberRoles.guildId, guildId))) + .leftJoin(schema.roles, and(eq(schema.memberRoles.id, id), eq(schema.memberRoles.guildId, guildId))) + .then(x => x[0]); + + if (role) { + return new Role(role, this); + } + + return undefined; + } + + public async resolveVoiceState({ id, guildId }: { id: string; guildId: string; }): Promise { + const state = await this.drizzle.query.voiceStates.findFirst({ + where: () => and(eq(schema.voiceStates.memberId, id), eq(schema.voiceStates.guildId, guildId)) + }); + + if (state) return new VoiceState(state, this); + + return undefined; + } + + public async resolveChannel({ force = false, fetch = true, cache, id, guildId }: { force?: boolean | undefined; fetch?: boolean; cache?: boolean | undefined; id: string; guildId: string; }): Promise { + if (force) { + const result = await Result.fromAsync(async () => this.rest.get(Routes.channel(id)) as unknown as Promise); + if (result.isOk()) { + const channel = result.unwrap(); + if (cache) { + await this.drizzle.insert(schema.channels).values({ + id: channel.id, + guildId: "guild_id" in channel ? channel.guild_id : null, + name: channel.name, + type: channel.type, + position: "position" in channel ? channel.position : null, + topic: "topic" in channel ? channel.topic : null, + nsfw: "nsfw" in channel ? channel.nsfw : null, + lastMessageId: "last_message_id" in channel ? channel.last_message_id : undefined + }).onConflictDoUpdate({ + target: schema.channels.id, + set: { + name: channel.name, + type: channel.type, + position: "position" in channel ? channel.position : null, + topic: "topic" in channel ? channel.topic : null, + nsfw: "nsfw" in channel ? channel.nsfw : null, + lastMessageId: "last_message_id" in channel ? channel.last_message_id : undefined + } + }); + + if ("permission_overwrites" in channel && channel.permission_overwrites !== undefined) { + await this.drizzle.delete(schema.channelsOverwrite).where(eq(schema.channelsOverwrite.id, channel.id)); + for (const overwrite of channel.permission_overwrites) { + await this.drizzle.insert(schema.channelsOverwrite).values({ + id: channel.id, + type: overwrite.type, + allow: overwrite.allow, + deny: overwrite.deny + }).onConflictDoUpdate({ + target: schema.channelsOverwrite.id, + set: { + type: overwrite.type, + allow: overwrite.allow, + deny: overwrite.deny + } + }); + } + } + } + + switch (channel.type) { + case ChannelType.GuildStageVoice: + case ChannelType.GuildVoice: + return new VoiceChannel({ + id, + guildId: channel.guild_id, + name: channel.name, + type: channel.type as unknown as number, + position: "position" in channel ? channel.position : null, + nsfw: "nsfw" in channel ? channel.nsfw ?? false : null, + flags: channel.flags as unknown as number ?? null, + bitrate: channel.bitrate ?? null + }, this); + default: { + return new TextChannel({ + id, + guildId: "guild_id" in channel ? channel.guild_id ?? null : null, + name: channel.name, + type: channel.type, + position: "position" in channel ? channel.position : null, + nsfw: "nsfw" in channel ? channel.nsfw ?? false : null, + flags: channel.flags ?? null + }, this); + } + } + } + } + + const channel = await this.drizzle.query.channels.findFirst({ + where: () => and(eq(schema.channels.id, id), eq(schema.channels.guildId, guildId)) + }); + + if (channel) { + switch (channel.type) { + case ChannelType.GuildStageVoice: + case ChannelType.GuildVoice: + return new VoiceChannel(channel, this); + default: { + return new TextChannel(channel, this); + } + } + } + + if (fetch) { + return this.resolveChannel({ id, guildId, force: true, cache: true }); + } + + return undefined; + } + + public async sendMessage(options: RESTPostAPIChannelMessageJSONBody, channelId: string): Promise { + return this.rest.post(Routes.channelMessages(channelId), { + body: options + }).then(x => new Message(x as APIMessage, this)); + } + + public async bindQueue(channel: Channel, queue: string, exchange: string): Promise { + if (Array.isArray(this.options.shardIds)) { + for (const shard of this.options.shardIds) { + await channel.bindQueue(queue, exchange, RoutingKey(this.clientId, shard)); + } + } else if (this.options.shardIds && this.options.shardIds.start >= 0 && this.options.shardIds.end >= 1) { + for (let i = this.options.shardIds.start; i < this.options.shardIds.end; i++) { + await channel.bindQueue(queue, exchange, RoutingKey(this.clientId, i)); + } + } else { + const session = await this.drizzle.query.sessions.findFirst(); + if (session) { + for (let i = 0; i < Number(session.shardCount); i++) { + await channel.bindQueue(queue, exchange, RoutingKey(this.clientId, i)); + } + } + } + } + + public async fetchShardCount(): Promise { + const session = await this.drizzle.query.sessions.findFirst(); + return session ? Number(session.shardCount) : 1; + } + + public async publishExchange(guildId: string, exchange: string, data: unknown, waitReply?: () => Promise): Promise> { + const shardCount = await this.fetchShardCount(); + const currentShardId = Number(BigInt(guildId) >> 22n) % shardCount; + + const success = await this.amqp.publish(exchange, RoutingKey(this.clientId, currentShardId), Buffer.from(JSON.stringify(data))); + + if (waitReply) { + return Result.fromAsync(() => waitReply() as T); + } + + return success ? Result.ok({ success } as T) : Result.err(new Error("Failed to publish message")); + } +} diff --git a/packages/core/src/Structures/Guild.ts b/packages/core/src/Structures/Guild.ts new file mode 100644 index 00000000..b30a83ba --- /dev/null +++ b/packages/core/src/Structures/Guild.ts @@ -0,0 +1,142 @@ +import type { BaseImageURLOptions } from "@discordjs/rest"; +import type { guilds } from "@nezuchan/kanao-schema"; +import { DiscordSnowflake } from "@sapphire/snowflake"; +import type { APIGuild, GuildDefaultMessageNotifications, GuildExplicitContentFilter, GuildMFALevel } from "discord-api-types/v10"; +import { GuildPremiumTier } from "discord-api-types/v10"; +import type { InferSelectModel } from "drizzle-orm"; +import { Base } from "./Base.js"; + +export class Guild extends Base>> { + public get name(): string { + return this.data.name!; + } + + public get description(): string | null { + return this.data.description!; + } + + public get available(): boolean { + return Boolean("unavailable" in this.data ? this.data.unavailable : false); + } + + public get discoverySplash(): string | null { + return this.data.discoverySplash ?? null; + } + + public get memberCount(): number { + return this.memberCount; + } + + public get premiumProgressBarEnabled(): boolean { + return Boolean(this.data.premiumProgressBarEnabled); + } + + public get afkTimeout(): APIGuild["afk_timeout"] { + return this.data.afkTimeout as APIGuild["afk_timeout"]; + } + + public get afkChannelId(): string | null | undefined { + return this.data.afkChannelId; + } + + public get systemChannelId(): string | null | undefined { + return this.data.systemChannelId; + } + + public get premiumTier(): GuildPremiumTier { + return this.data.premiumTier as GuildPremiumTier; + } + + public get premiumSubscriptionCount(): number | null | undefined { + return this.data.premiumSubscriptionCount; + } + + public get widgetEnabled(): boolean { + return Boolean(this.data.widgetEnabled); + } + + public get widgetChannelId(): string | null | undefined { + return this.data.widgetChannelId; + } + + public get explicitContentFilter(): GuildExplicitContentFilter { + return this.data.explicitContentFilter as GuildExplicitContentFilter; + } + + public get mfaLevel(): GuildMFALevel { + return this.data.mfaLevel as GuildMFALevel; + } + + public get createdTimestamp(): number { + return Number(DiscordSnowflake.deconstruct(this.id).timestamp); + } + + public get createdAt(): Date { + return new Date(this.createdTimestamp); + } + + public get defaultMessageNotifications(): GuildDefaultMessageNotifications { + return this.data.defaultMessageNotifications as GuildDefaultMessageNotifications; + } + + public get maximumMembers(): number | null | undefined { + return this.data.maxMembers; + } + + public get maximumPresences(): number | null | undefined { + return this.data.maxMembers; + } + + public get maxVideoChannelUsers(): number | null | undefined { + return this.data.maxVideoChannelUsers; + } + + public get approximateMemberCount(): number | null | undefined { + return this.data.approximateMemberCount; + } + + public get rulesChannelId(): string | null | undefined { + return this.data.rulesChannelId; + } + + public get publicUpdatesChannelId(): string | null | undefined { + return this.data.publicUpdatesChannelId; + } + + public get ownerId(): string | null | undefined { + return this.data.ownerId; + } + + public get icon(): string | null | undefined { + return this.data.icon; + } + + public get banner(): string | null | undefined { + return this.data.banner; + } + + public bannerURL(options?: BaseImageURLOptions): string | null | undefined { + return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options); + } + + public iconURL(options?: BaseImageURLOptions): string | null | undefined { + return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options); + } + + public discoverySplashURL(options?: BaseImageURLOptions): string | null { + return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options); + } + + public get maximumBitrate(): 96_000 | 128_000 | 256_000 | 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; + } + } +} diff --git a/packages/core/src/Structures/GuildMember.ts b/packages/core/src/Structures/GuildMember.ts new file mode 100644 index 00000000..e9f0cdc5 --- /dev/null +++ b/packages/core/src/Structures/GuildMember.ts @@ -0,0 +1,94 @@ +import type { BaseImageURLOptions } from "@discordjs/rest"; +import type { members } from "@nezuchan/kanao-schema"; +import { memberRoles, roles } from "@nezuchan/kanao-schema"; +import { and, eq } from "drizzle-orm"; +import type { InferSelectModel } from "drizzle-orm"; +import { Base } from "./Base.js"; +import { Role } from "./Role.js"; +import type { User } from "./User.js"; +import type { VoiceState } from "./VoiceState.js"; + +export class GuildMember extends Base> { + public get id(): string { + return this.data.id; + } + + public get guildId(): string | null { + return this.data.guildId; + } + + public get nickname(): string | null { + return this.data.nick; + } + + public get joinedAt(): Date | null { + return this.data.joinedAt ? new Date(this.data.joinedAt) : null; + } + + public get premiumSince(): Date | null { + return this.data.premiumSince ? new Date(this.data.premiumSince) : null; + } + + public get communicationDisabledUntilTimestamp(): number | null { + return this.data.communicationDisabledUntil ? Date.parse(this.data.communicationDisabledUntil) : null; + } + + public get communicationDisabledUntil(): Date | null { + return this.communicationDisabledUntilTimestamp ? new Date(this.communicationDisabledUntilTimestamp) : null; + } + + public displayAvatarURL(options?: BaseImageURLOptions): string | null { + return this.data.avatar ? this.client.rest.cdn.guildMemberAvatar(this.guildId!, this.id, this.data.avatar, options) : null; + } + + public async manageable(): Promise { + const guild = await this.client.resolveGuild({ id: this.guildId! }); + if (this.id === guild?.ownerId) return false; + if (this.id === this.client.clientId) return false; + if (this.client.clientId === guild?.ownerId) return true; + + const clientMember = await this.client.resolveMember({ id: this.client.clientId, guildId: this.guildId! }); + if (!clientMember) return false; + + const clientRoles = await clientMember.resolveRoles(); + const mRoles = await this.resolveRoles(); + + if (clientRoles[0].position > mRoles[0].position) return true; + return false; + } + + public async resolveRoles(): Promise { + const mRoles = []; + if (this.guildId) { + const guildMemberRoles = await this.client.drizzle.select({ + role: roles + }).from(memberRoles) + .where(and(eq(memberRoles.id, this.id), eq(memberRoles.guildId, this.guildId))) + .leftJoin(roles, and(eq(memberRoles.id, this.id), eq(memberRoles.guildId, this.guildId))); + + for (const { role } of guildMemberRoles) { + if (role) mRoles.push(new Role(role, this.client)); + } + + const everyoneRole = await this.client.resolveRole({ id: this.guildId, guildId: this.guildId }); + if (everyoneRole) mRoles.push(everyoneRole); + } + return mRoles.sort((a, b) => b.position - a.position); + } + + public async resolveUser({ force = false, cache = true }: { force?: boolean; cache?: boolean; }): Promise { + return this.client.resolveUser({ id: this.id, force, cache }); + } + + public async resolveVoiceState(): Promise { + if (this.guildId) { + return this.client.resolveVoiceState({ id: this.id, guildId: this.guildId }); + } + + return undefined; + } + + public toString(): string { + return `<@!${this.id}>`; + } +} diff --git a/packages/core/src/Structures/Interactions/AutoCompleteInteraction.ts b/packages/core/src/Structures/Interactions/AutoCompleteInteraction.ts new file mode 100644 index 00000000..085d4773 --- /dev/null +++ b/packages/core/src/Structures/Interactions/AutoCompleteInteraction.ts @@ -0,0 +1,24 @@ +import { Routes, InteractionResponseType } from "discord-api-types/v10"; +import { BaseInteraction } from "./BaseInteraction.js"; + +export class AutoCompleteInteraction extends BaseInteraction { + public responded = false; + + public get commandName(): string | null { + return this.data.data && "name" in this.data.data ? this.data.data.name : null; + } + + public async respond(options: { name: string; value: string; }[]): Promise { + if (this.responded) new Error("This interaction has already been responded to."); + await this.client.rest.post(Routes.interactionCallback(this.id, this.data.token), { + body: { + type: InteractionResponseType.ApplicationCommandAutocompleteResult, + data: { + choices: options + } + }, + auth: false + }); + this.responded = true; + } +} diff --git a/packages/core/src/Structures/Interactions/BaseContextMenuInteraction.ts b/packages/core/src/Structures/Interactions/BaseContextMenuInteraction.ts new file mode 100644 index 00000000..3ced9f74 --- /dev/null +++ b/packages/core/src/Structures/Interactions/BaseContextMenuInteraction.ts @@ -0,0 +1,18 @@ +import { ApplicationCommandType } from "discord-api-types/v10"; +import { BaseInteraction } from "./BaseInteraction.js"; +import type { MessageContextMenuInteraction } from "./MessageContextMenuInteraction.js"; +import type { UserContextMenuInteraction } from "./UserContextMenuInteraction.js"; + +export class BaseContextMenuInteraction extends BaseInteraction { + public get commandName(): string | null { + return this.data.data && "name" in this.data.data ? this.data.data.name : null; + } + + public isUserContext(): this is UserContextMenuInteraction { + return this.commandType === ApplicationCommandType.User; + } + + public isMessageContext(): this is MessageContextMenuInteraction { + return this.commandType === ApplicationCommandType.Message; + } +} diff --git a/packages/core/src/Structures/Interactions/BaseInteraction.ts b/packages/core/src/Structures/Interactions/BaseInteraction.ts new file mode 100644 index 00000000..7833ea6e --- /dev/null +++ b/packages/core/src/Structures/Interactions/BaseInteraction.ts @@ -0,0 +1,182 @@ +import type { APIChannel, APIInteractionResponseCallbackData, APIMessage, GatewayInteractionCreateDispatchData, Snowflake } from "discord-api-types/v10"; +import { ApplicationCommandType, ComponentType, InteractionResponseType, InteractionType, MessageFlags, PermissionFlagsBits, Routes } from "discord-api-types/v10"; +import { Base } from "../Base.js"; +import type { Guild } from "../Guild.js"; +import { GuildMember } from "../GuildMember.js"; +import { Message } from "../Message.js"; +import { PermissionsBitField } from "../PermissionsBitField.js"; +import type { AutoCompleteInteraction } from "./AutoCompleteInteraction.js"; +import type { BaseContextMenuInteraction } from "./BaseContextMenuInteraction.js"; +import type { CommandInteraction } from "./CommandInteraction.js"; +import { CommandOptionsResolver } from "./CommandOptionsResolver.js"; +import type { MessageComponentInteraction } from "./MessageComponentInteraction.js"; +import type { ModalSubmitInteraction } from "./ModalSubmitInteraction.js"; + +export class BaseInteraction extends Base { + public deferred = false; + public replied = false; + + public get options(): CommandOptionsResolver { + return new CommandOptionsResolver(this.data.data); + } + + public get type(): InteractionType { + return this.data.type; + } + + public get commandType(): ApplicationCommandType | null { + return this.data.data && "type" in this.data.data ? this.data.data.type : null; + } + + public get applicationId(): Snowflake { + return this.data.application_id; + } + + public get channelId(): Snowflake | null { + return this.data.channel?.id ?? null; + } + + public get guildId(): Snowflake | null { + return this.data.guild_id ?? null; + } + + public get channel(): Partial & Pick | undefined { + return this.data.channel; + } + + public get applicationPermissions(): PermissionsBitField | null { + return this.data.app_permissions ? new PermissionsBitField(PermissionFlagsBits, BigInt(this.data.app_permissions)).freeze() : null; + } + + public get memberPermissions(): PermissionsBitField | null { + return this.data.member?.permissions ? new PermissionsBitField(PermissionFlagsBits, BigInt(this.data.member.permissions)).freeze() : null; + } + + public get member(): GuildMember | null { + return this.data.member + ? new GuildMember({ + id: this.data.user!.id, + guildId: this.guildId, + nick: this.data.member.nick ?? null, + avatar: this.data.member.avatar ?? null, + flags: this.data.member.flags, + joinedAt: this.data.member.joined_at, + premiumSince: this.data.member.premium_since ?? null, + deaf: this.data.member.deaf, + mute: this.data.member.mute, + pending: this.data.member.pending ?? null, + communicationDisabledUntil: this.data.member.communication_disabled_until ?? null + }, this.client) + : null; + } + + public async resolveClientMember({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + if (this.guildId) { + return this.client.resolveMember({ id: this.client.clientId, guildId: this.guildId, force, cache }); + } + + return undefined; + } + + public async resolveGuild({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + if (this.guildId) { + return this.client.resolveGuild({ id: this.guildId, force, cache }); + } + + return undefined; + } + + public async reply(options: APIInteractionResponseCallbackData): Promise { + if (this.deferred || this.replied) throw new Error("This interaction has already been deferred or replied."); + await this.client.rest.post(Routes.interactionCallback(this.id, this.data.token), { + body: { + type: InteractionResponseType.ChannelMessageWithSource, + data: options + }, + auth: false + }); + this.replied = true; + return this; + } + + public async editReply(options: APIInteractionResponseCallbackData): Promise { + if (!this.deferred && !this.replied) throw new Error("This interaction is not deferred or replied yet."); + const message = await this.client.rest.patch(Routes.webhookMessage(this.applicationId, this.data.token), { + body: options, + auth: false + }); + this.replied = true; + return new Message(message as APIMessage, this.client); + } + + public async deleteReply(): Promise { + await this.client.rest.delete(Routes.webhookMessage(this.applicationId, this.data.token), { + auth: false + }); + return this; + } + + public async deferReply(ephemeral?: boolean): Promise { + await this.client.rest.post(Routes.interactionCallback(this.id, this.data.token), { + body: { + type: InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: ephemeral ? MessageFlags.Ephemeral : undefined + } + }, + auth: false + }); + this.deferred = true; + return this; + } + + public async followUp(options: APIInteractionResponseCallbackData): Promise { + if (!this.deferred && !this.replied) throw new Error("This interaction is not deferred or replied yet."); + const message = await this.client.rest.post(Routes.webhook(this.applicationId, this.data.token), { + body: options, + auth: false + }); + return new Message(message as APIMessage, this.client); + } + + public async showModal(options: APIInteractionResponseCallbackData): Promise { + if (this.deferred || this.replied) throw new Error("This interaction is already deferred or replied."); + await this.client.rest.post(Routes.interactionCallback(this.id, this.data.token), { + body: { + type: InteractionResponseType.Modal, + data: options + }, + auth: false + }); + this.replied = true; + return this; + } + + public isCommandInteraction(): this is CommandInteraction { + return this.type === InteractionType.ApplicationCommand && this.commandType === ApplicationCommandType.ChatInput; + } + + public isContextMenuInteraction(): this is BaseContextMenuInteraction { + return this.type === InteractionType.ApplicationCommand && (this.commandType === ApplicationCommandType.User || this.commandType === ApplicationCommandType.Message); + } + + public isAutoCompleteInteraction(): this is AutoCompleteInteraction { + return this.data.type === InteractionType.ApplicationCommandAutocomplete; + } + + public isComponentInteraction(): this is MessageComponentInteraction { + return this.data.type === InteractionType.MessageComponent; + } + + public isModalSubmit(): this is ModalSubmitInteraction { + return this.data.type === InteractionType.ModalSubmit; + } + + public isButton(): this is MessageComponentInteraction { + return this.isComponentInteraction() && this.componentType === ComponentType.Button; + } + + public isSelectMenu(): this is MessageComponentInteraction { + return this.isComponentInteraction() && this.componentType === ComponentType.SelectMenu; + } +} diff --git a/packages/core/src/Structures/Interactions/CommandInteraction.ts b/packages/core/src/Structures/Interactions/CommandInteraction.ts new file mode 100644 index 00000000..00e61c36 --- /dev/null +++ b/packages/core/src/Structures/Interactions/CommandInteraction.ts @@ -0,0 +1,7 @@ +import { BaseInteraction } from "./BaseInteraction.js"; + +export class CommandInteraction extends BaseInteraction { + public get commandName(): string | null { + return this.data.data && "name" in this.data.data ? this.data.data.name : null; + } +} diff --git a/packages/core/src/Structures/Interactions/CommandOptionsResolver.ts b/packages/core/src/Structures/Interactions/CommandOptionsResolver.ts new file mode 100644 index 00000000..350dce6d --- /dev/null +++ b/packages/core/src/Structures/Interactions/CommandOptionsResolver.ts @@ -0,0 +1,111 @@ +import { cast } from "@sapphire/utilities"; +import type { APIApplicationCommandInteractionDataOption, APIInteractionDataResolved, APIMessageApplicationCommandInteractionDataResolved, APIUserInteractionDataResolved, APIInteraction, APIInteractionDataResolvedGuildMember, APIUser, APIRole, APIInteractionDataResolvedChannel } from "discord-api-types/v10"; +import { ApplicationCommandOptionType } from "discord-api-types/v10"; + +export class CommandOptionsResolver { + public subCommandName: string | null = null; + public subCommandGroupName: string | null = null; + public options!: APIApplicationCommandInteractionDataOption[]; + public resolvedOptions: APIInteractionDataResolved | APIMessageApplicationCommandInteractionDataResolved | APIUserInteractionDataResolved | undefined; + + public constructor(data: APIInteraction["data"]) { + if (data && "resolved" in data) { + this.resolvedOptions = data.resolved; + } + + this.options = data && "options" in data ? data.options ?? [] : []; + + if (this.options[0]?.type === ApplicationCommandOptionType.SubcommandGroup) { + this.subCommandGroupName = this.options[0].name; + this.options = this.options[0].options; + } + + if (this.options[0]?.type === ApplicationCommandOptionType.Subcommand) { + this.subCommandName = this.options[0].name; + this.options = this.options[0].options ?? []; + } + } + + public get(name: string, required?: boolean): T | null; + public get(name: string, required: true): T; + public get(name: string, required = false): T { + const option = this.options.find(o => o.name === name); + if (option && "value" in option) return cast(option.value); + if (required) throw new Error(`${name} is required, but it was missing.`); + return cast(null); + } + + public getString(name: string, required?: boolean): string | null; + public getString(name: string, required: true): string; + public getString(name: string, required = false): string { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.String); + if (option && "value" in option) return cast(option.value); + if (required) throw new Error(`${name} is required, but it was missing.`); + return cast(null); + } + + public getNumber(name: string, required?: boolean): number | null; + public getNumber(name: string, required: true): number; + public getNumber(name: string, required = false): number { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.Number); + if (option && "value" in option) return cast(option.value); + if (required) throw new Error(`${name} is required, but it was missing.`); + return cast(null); + } + + public getInteger(name: string, required?: boolean): number | null; + public getInteger(name: string, required: true): number; + public getInteger(name: string, required = false): number { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.Integer); + if (option && "value" in option) return cast(option.value); + if (required) throw new Error(`${name} is required, but it was missing.`); + return cast(null); + } + + public getMember(name: string, required?: boolean): APIInteractionDataResolvedGuildMember | null; + public getMember(name: string, required: true): APIInteractionDataResolvedGuildMember; + public getMember(name: string, required = false): APIInteractionDataResolvedGuildMember { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.User); + if (option && "value" in option && this.resolvedOptions && "members" in this.resolvedOptions && this.resolvedOptions.members) return this.resolvedOptions.members[cast(option.value)]; + if (required) throw new Error("member is required, but it was missing."); + return cast(null); + } + + public getUser(name: string, required?: boolean): APIUser | null; + public getUser(name: string, required: true): APIUser; + public getUser(name: string, required = false): APIUser { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.User); + if (option && "value" in option && this.resolvedOptions && "users" in this.resolvedOptions && this.resolvedOptions.users) return this.resolvedOptions.users[cast(option.value)]; + if (required) throw new Error("user is required, but it was missing."); + return cast(null); + } + + public getRole(name: string, required?: boolean): APIRole | null; + public getRole(name: string, required: true): APIRole; + public getRole(name: string, required = false): APIRole { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.Role); + if (option && "value" in option && this.resolvedOptions && "roles" in this.resolvedOptions && this.resolvedOptions.roles) return this.resolvedOptions.roles[cast(option.value)]; + if (required) throw new Error("role is required, but it was missing."); + return cast(null); + } + + public getChannel(name: string, required?: boolean): APIInteractionDataResolvedChannel | null; + public getChannel(name: string, required: true): APIInteractionDataResolvedChannel; + public getChannel(name: string, required = false): APIInteractionDataResolvedChannel { + const option = this.options.find(o => o.name === name && o.type === ApplicationCommandOptionType.Channel); + if (option && "value" in option && this.resolvedOptions && "channels" in this.resolvedOptions && this.resolvedOptions.channels) return this.resolvedOptions.channels[cast(option.value)]; + if (required) throw new Error("channel is required, but it was missing."); + return cast(null); + } + + public getFocused(name: string): Focused { + const option = this.options.find(o => "focused" in o && o.focused); + if (option && "focused" in option && option.focused && option.name === name) return { focused: true, value: String(option.value) }; + return { focused: false, value: null }; + } +} + +export type Focused = { + focused: boolean; + value: string | null; +}; diff --git a/packages/core/src/Structures/Interactions/MessageComponentInteraction.ts b/packages/core/src/Structures/Interactions/MessageComponentInteraction.ts new file mode 100644 index 00000000..7b10e642 --- /dev/null +++ b/packages/core/src/Structures/Interactions/MessageComponentInteraction.ts @@ -0,0 +1,45 @@ +import type { APIInteractionResponseCallbackData, APIMessage, ComponentType } from "discord-api-types/v10"; +import { InteractionResponseType, Routes } from "discord-api-types/v10"; +import { Message } from "../Message.js"; +import { BaseInteraction } from "./BaseInteraction.js"; + +export class MessageComponentInteraction extends BaseInteraction { + public getRawMessage(): Message | null { + return "message" in this.data && this.data.message ? new Message(this.data.message, this.client) : null; + } + + public get componentType(): ComponentType | null { + return this.data.data && "component_type" in this.data.data ? this.data.data.component_type : null; + } + + public get customId(): string | null { + return this.data.data && "custom_id" in this.data.data ? this.data.data.custom_id : null; + } + + public get values(): string[] { + return this.data.data && "values" in this.data.data ? this.data.data.values : []; + } + + public async deferUpdate(): Promise { + await this.client.rest.post(Routes.interactionCallback(this.id, this.data.token), { + body: { + type: InteractionResponseType.DeferredMessageUpdate + }, + auth: false + }); + this.deferred = true; + } + + public async update(options: APIInteractionResponseCallbackData): Promise { + if (this.deferred && this.replied) throw new Error("This interaction has already been deferred or replied."); + const message = await this.client.rest.patch(Routes.interactionCallback(this.applicationId, this.data.token), { + body: { + type: InteractionResponseType.UpdateMessage, + data: options + }, + auth: false + }); + this.replied = true; + return new Message(message as APIMessage, this.client); + } +} diff --git a/packages/core/src/Structures/Interactions/MessageContextMenuInteraction.ts b/packages/core/src/Structures/Interactions/MessageContextMenuInteraction.ts new file mode 100644 index 00000000..b3c6794c --- /dev/null +++ b/packages/core/src/Structures/Interactions/MessageContextMenuInteraction.ts @@ -0,0 +1,8 @@ +import { Message } from "../Message.js"; +import { BaseContextMenuInteraction } from "./BaseContextMenuInteraction.js"; + +export class MessageContextMenuInteraction extends BaseContextMenuInteraction { + public getMessage(): Message | null { + return this.data.data && "target_id" in this.data.data && "messages" in this.data.data.resolved ? new Message(this.data.data.resolved.messages[this.data.data.target_id], this.client) : null; + } +} diff --git a/packages/core/src/Structures/Interactions/ModalSubmitInteraction.ts b/packages/core/src/Structures/Interactions/ModalSubmitInteraction.ts new file mode 100644 index 00000000..952c5b1f --- /dev/null +++ b/packages/core/src/Structures/Interactions/ModalSubmitInteraction.ts @@ -0,0 +1,17 @@ +import type { ModalSubmitActionRowComponent } from "discord-api-types/v10"; +import { Message } from "../Message.js"; +import { BaseInteraction } from "./BaseInteraction.js"; + +export class ModalSubmitInteraction extends BaseInteraction { + public get customId(): string | null { + return this.data.data && "custom_id" in this.data.data ? this.data.data.custom_id : null; + } + + public get components(): ModalSubmitActionRowComponent[] { + return this.data.data && "components" in this.data.data ? this.data.data.components : []; + } + + public get message(): Message | null { + return "message" in this.data && this.data.message ? new Message(this.data.message, this.client) : null; + } +} diff --git a/packages/core/src/Structures/Interactions/UserContextMenuInteraction.ts b/packages/core/src/Structures/Interactions/UserContextMenuInteraction.ts new file mode 100644 index 00000000..55fa1ef0 --- /dev/null +++ b/packages/core/src/Structures/Interactions/UserContextMenuInteraction.ts @@ -0,0 +1,30 @@ +import { GuildMember } from "../GuildMember.js"; +import { User } from "../User.js"; +import { BaseContextMenuInteraction } from "./BaseContextMenuInteraction.js"; + +export class UserContextMenuInteraction extends BaseContextMenuInteraction { + public get getUser(): User | null { + return this.data.data && "target_id" in this.data.data && "users" in this.data.data.resolved ? new User(this.data.data.resolved.users[this.data.data.target_id], this.client) : null; + } + + public get getMember(): GuildMember | null { + const member = this.data.data && "target_id" in this.data.data && "members" in this.data.data.resolved && this.data.data.resolved.members ? { ...this.data.data.resolved.members[this.data.data.target_id], id: this.data.data.target_id } : null; + if (member) { + return new GuildMember({ + id: member.id, + guildId: this.data.guild_id!, + avatar: member.avatar ?? null, + flags: member.flags as unknown as number, + communicationDisabledUntil: member.communication_disabled_until ?? null, + deaf: null, + mute: null, + joinedAt: member.joined_at, + nick: member.nick ?? null, + pending: member.pending ?? false, + premiumSince: member.premium_since ?? null + }, this.client); + } + + return null; + } +} diff --git a/packages/core/src/Structures/Message.ts b/packages/core/src/Structures/Message.ts new file mode 100644 index 00000000..55042717 --- /dev/null +++ b/packages/core/src/Structures/Message.ts @@ -0,0 +1,95 @@ +/* eslint-disable promise/prefer-await-to-then */ +import type { APIAttachment, APIEmbed, APIMessage, APIMessageComponent, APIMessageReference, APIReaction, APIStickerItem, GatewayMessageCreateDispatchData, GatewayMessageDeleteDispatch, GatewayMessageUpdateDispatch, RESTPatchAPIChannelMessageJSONBody } from "discord-api-types/v10"; +import { MessageType, Routes } from "discord-api-types/v10"; +import { Base } from "./Base.js"; +import type { Guild } from "./Guild.js"; +import type { GuildMember } from "./GuildMember.js"; +import { User } from "./User.js"; + +export class Message extends Base { + public get type(): MessageType { + return "type" in this.data ? this.data.type : MessageType.Default; + } + + public get content(): string { + return "content" in this.data ? this.data.content : ""; + } + + public get guildId(): string | undefined { + return "guild_id" in this.data ? this.data.guild_id : undefined; + } + + public get channelId(): string | null { + return "channel_id" in this.data ? this.data.channel_id : null; + } + + public get webhookId(): string | undefined { + return "webhook_id" in this.data ? this.data.webhook_id : undefined; + } + + public get attachments(): APIAttachment[] { + return "attachments" in this.data ? this.data.attachments : []; + } + + public get embeds(): APIEmbed[] { + return "embeds" in this.data ? this.data.embeds : []; + } + + public get reactions(): APIReaction[] { + return "reactions" in this.data ? this.data.reactions ?? [] : []; + } + + public get stickerItems(): APIStickerItem[] { + return "sticker_items" in this.data ? this.data.sticker_items ?? [] : []; + } + + public get messageReference(): APIMessageReference | undefined { + return "message_reference" in this.data ? this.data.message_reference : undefined; + } + + public get components(): APIMessageComponent[] { + return "components" in this.data ? this.data.components ?? [] : []; + } + + public get mentionUsers(): APIMessage["mentions"] { + return "mentions" in this.data ? this.data.mentions : []; + } + + public get mentionChannels(): APIMessage["mention_channels"] { + return "mention_channels" in this.data ? this.data.mention_channels : []; + } + + public get mentionRoles(): APIMessage["mention_roles"] { + return "mention_roles" in this.data ? this.data.mention_roles : []; + } + + public get mentionEveryone(): boolean | undefined { + return "mention_everyone" in this.data ? this.data.mention_everyone : undefined; + } + + public async resolveGuild({ force = false, cache = true }: { force?: boolean; cache?: boolean; }): Promise { + if (this.guildId) { + return this.client.resolveGuild({ id: this.guildId, force, cache }); + } + + return undefined; + } + + public get author(): User | null { + return "author" in this.data ? new User(this.data.author, this.client) : null; + } + + public async resolveMember({ force = false, cache = true }: { force?: boolean; cache?: boolean; }): Promise { + if (this.guildId && this.author) { + return this.client.resolveMember({ id: this.author.id, guildId: this.guildId, force, cache }); + } + + return undefined; + } + + public async edit(options: RESTPatchAPIChannelMessageJSONBody): Promise { + return this.client.rest.patch(Routes.channelMessage(this.channelId!, this.id), { + body: options + }).then(x => new Message(x as APIMessage, this.client)); + } +} diff --git a/packages/core/src/Structures/PermissionsBitField.ts b/packages/core/src/Structures/PermissionsBitField.ts new file mode 100644 index 00000000..1008ef48 --- /dev/null +++ b/packages/core/src/Structures/PermissionsBitField.ts @@ -0,0 +1,17 @@ +import type { BitFieldResolvable } from "@cordis/bitfield"; +import { BitField } from "@cordis/bitfield"; +import { PermissionFlagsBits } from "discord-api-types/v10"; + +export class PermissionsBitField extends BitField { + public missing(bit: BitFieldResolvable, checkAdmin = true): (BitField | bigint | string)[] { + return checkAdmin && this.has(PermissionFlagsBits.Administrator) ? [] : super.missing(bit); + } + + public any(bit: BitFieldResolvable, checkAdmin = true): boolean { + return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.any(bit); + } + + public has(bit: BitFieldResolvable, checkAdmin = true): boolean { + return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.has(bit); + } +} diff --git a/packages/core/src/Structures/Role.ts b/packages/core/src/Structures/Role.ts new file mode 100644 index 00000000..1214619f --- /dev/null +++ b/packages/core/src/Structures/Role.ts @@ -0,0 +1,23 @@ +import type { roles } from "@nezuchan/kanao-schema"; +import { PermissionFlagsBits } from "discord-api-types/v10"; +import type { InferSelectModel } from "drizzle-orm"; +import { Base } from "./Base.js"; +import { PermissionsBitField } from "./PermissionsBitField.js"; + +export class Role extends Base> { + public get name(): string { + return this.data.name!; + } + + public get permissions(): PermissionsBitField { + return new PermissionsBitField(PermissionFlagsBits, BigInt(this.data.permissions ?? 0)); + } + + public get position(): number { + return this.data.position!; + } + + public toString(): string { + return `<@&${this.id}>`; + } +} diff --git a/packages/core/src/Structures/User.ts b/packages/core/src/Structures/User.ts new file mode 100644 index 00000000..8754fd7b --- /dev/null +++ b/packages/core/src/Structures/User.ts @@ -0,0 +1,63 @@ +import type { ImageURLOptions } from "@discordjs/rest"; +import type { users } from "@nezuchan/kanao-schema"; +import { DiscordSnowflake } from "@sapphire/snowflake"; +import type { InferSelectModel } from "drizzle-orm"; +import { Base } from "./Base.js"; + +export class User extends Base>> { + public get username(): string | undefined { + return this.data.username; + } + + public get discriminator(): string | null | undefined { + return this.data.discriminator; + } + + public get avatar(): string | null | undefined { + return this.data.avatar; + } + + public get bot(): boolean { + return Boolean(this.data.bot); + } + + public get accentColor(): number | null | undefined { + return this.data.accentColor; + } + + public get banner(): string | null | undefined { + return this.data.banner; + } + + public get tag(): string { + return `${this.username}${this.discriminator === "0" ? "" : `#${this.discriminator}`}`; + } + + public get createdTimestamp(): number { + return DiscordSnowflake.timestampFrom(this.id); + } + + public get createdAt(): Date { + return new Date(this.createdTimestamp); + } + + public get defaultAvatarURL(): string { + return this.client.rest.cdn.defaultAvatar(Number(this.discriminator) % 5); + } + + public avatarURL(options?: ImageURLOptions): string | null { + return this.avatar ? this.client.rest.cdn.avatar(this.id, this.avatar, options) : null; + } + + public displayAvatarURL(options?: ImageURLOptions): string { + return this.avatarURL(options) ?? this.defaultAvatarURL; + } + + public bannerURL(options?: ImageURLOptions): string | null { + return this.banner ? this.client.rest.cdn.banner(this.id, this.banner, options) : null; + } + + public toString(): string { + return `<@${this.id}>`; + } +} diff --git a/packages/core/src/Structures/VoiceState.ts b/packages/core/src/Structures/VoiceState.ts new file mode 100644 index 00000000..de25231c --- /dev/null +++ b/packages/core/src/Structures/VoiceState.ts @@ -0,0 +1,50 @@ +import type { voiceStates } from "@nezuchan/kanao-schema"; +import type { InferSelectModel } from "drizzle-orm"; +import { Base } from "./Base.js"; +import type { GuildMember } from "./GuildMember.js"; + +export class VoiceState extends Base> { + public get id(): string { + return this.data.memberId; + } + + public get guildId(): string { + return this.data.guildId!; + } + + public get channelId(): string | null { + return this.data.channelId; + } + + public get sessionId(): string { + return this.data.sessionId!; + } + + public get deaf(): boolean { + return this.data.deaf!; + } + + public get mute(): boolean { + return this.data.mute!; + } + + public get selfDeaf(): boolean { + return this.data.selfDeaf!; + } + + public get selfMute(): boolean { + return this.data.selfMute!; + } + + public get requestToSpeakTimestamp(): Date | null { + return this.data.requestToSpeakTimestamp ? new Date(this.data.requestToSpeakTimestamp) : null; + } + + public async resolveMember({ force = false, cache = true }: { force?: boolean; cache?: boolean; }): Promise { + if (this.guildId && this.id) { + return this.client.resolveMember({ id: this.id, guildId: this.guildId, force, cache }); + } + + return undefined; + } +} diff --git a/packages/core/src/Typings/index.ts b/packages/core/src/Typings/index.ts new file mode 100644 index 00000000..3f166e11 --- /dev/null +++ b/packages/core/src/Typings/index.ts @@ -0,0 +1,7 @@ +export type ClientOptions = { + token?: string; + amqpUrl: string; + shardIds?: number[] | { start: number; end: number; }; + rest?: string; + databaseUrl: string; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..fb18a608 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,23 @@ +export * from "./Structures/Client.js"; +export * from "./Structures/Base.js"; +export * from "./Structures/Channels/BaseChannel.js"; +export * from "./Structures/Guild.js"; +export * from "./Structures/GuildMember.js"; +export * from "./Structures/Message.js"; +export * from "./Structures/Role.js"; +export * from "./Structures/User.js"; +export * from "./Structures/Channels/TextChannel.js"; +export * from "./Structures/Channels/VoiceChannel.js"; +export * from "./Structures/VoiceState.js"; +export * from "./Structures/PermissionsBitField.js"; +export * from "./Typings/index.js"; +export * from "./Structures/Interactions/AutoCompleteInteraction.js"; +export * from "./Structures/Interactions/CommandInteraction.js"; +export * from "./Structures/Interactions/BaseInteraction.js"; +export * from "./Structures/Interactions/BaseContextMenuInteraction.js"; +export * from "./Structures/Interactions/CommandOptionsResolver.js"; +export * from "./Structures/Interactions/MessageComponentInteraction.js"; +export * from "./Structures/Interactions/MessageContextMenuInteraction.js"; +export * from "./Structures/Interactions/UserContextMenuInteraction.js"; +export * from "./Structures/Interactions/ModalSubmitInteraction.js"; +export * from "./Enums/Events.js"; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..d62780ed --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/framework/.swcrc b/packages/framework/.swcrc new file mode 100644 index 00000000..6b9a900a --- /dev/null +++ b/packages/framework/.swcrc @@ -0,0 +1,12 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true + }, + "target": "es2022" + }, + "module": { + "type": "es6" + } +} \ No newline at end of file diff --git a/packages/framework/CHANGELOG.md b/packages/framework/CHANGELOG.md new file mode 100644 index 00000000..49d98e5d --- /dev/null +++ b/packages/framework/CHANGELOG.md @@ -0,0 +1,147 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.7.3](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.7.2...@nezuchan/framework@0.7.3) (2024-01-01) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +## [0.7.2](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.7.1...@nezuchan/framework@0.7.2) (2024-01-01) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +## [0.7.1](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.7.0...@nezuchan/framework@0.7.1) (2024-01-01) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +# [0.7.0](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.23...@nezuchan/framework@0.7.0) (2023-12-31) + + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#50](https://github.com/NezuChan/library/issues/50)) ([ce1f360](https://github.com/NezuChan/library/commit/ce1f36082841e6cb2040d7f4d6f34a1a7cd9cf23)) +* **deps:** update dependency @sapphire/pieces to v4 ([#54](https://github.com/NezuChan/library/issues/54)) ([523bfde](https://github.com/NezuChan/library/commit/523bfdeb8ffdce7667bf7fd06a9466f201f71c50)) +* **deps:** update dependency @sapphire/utilities to ^3.15.1 ([8e259bc](https://github.com/NezuChan/library/commit/8e259bc985ec313796d2856062c1393ede1fb456)) + + + + + +## [0.6.23](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.22...@nezuchan/framework@0.6.23) (2023-12-07) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +## [0.6.22](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.21...@nezuchan/framework@0.6.22) (2023-12-07) + + +### Bug Fixes + +* **deps:** update sapphire dependencies ([3a34b73](https://github.com/NezuChan/library/commit/3a34b73e086a41be67e0c1b962bc7761033435f8)) + + + + + +## [0.6.21](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.20...@nezuchan/framework@0.6.21) (2023-11-18) + + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#22](https://github.com/NezuChan/library/issues/22)) ([002d346](https://github.com/NezuChan/library/commit/002d3469048b0f2df180340b11fb76233f7deaaf)) +* **deps:** update all non-major dependencies ([#35](https://github.com/NezuChan/library/issues/35)) ([2817e59](https://github.com/NezuChan/library/commit/2817e59f298aab90662d40eea94e2d80a8736241)) +* **deps:** update dependency @discordjs/collection to v2 ([#43](https://github.com/NezuChan/library/issues/43)) ([a87cb5b](https://github.com/NezuChan/library/commit/a87cb5bbcedebadb74862ae6f7958ecac8ee46dd)) +* **deps:** update dependency @sapphire/pieces to ^3.10.0 ([f3cde93](https://github.com/NezuChan/library/commit/f3cde93376026fd81465f915d3052e3721336efc)) +* **deps:** update dependency @sapphire/pieces to ^3.7.1 ([0f52f03](https://github.com/NezuChan/library/commit/0f52f03d3357f0cebe1c541df748184f53b8d2c9)) +* **deps:** update dependency @sapphire/pieces to ^3.9.0 ([4bd85e8](https://github.com/NezuChan/library/commit/4bd85e86b973bbdf1294e004c75836094ea85559)) + + + + + +## [0.6.20](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.19...@nezuchan/framework@0.6.20) (2023-09-24) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +## [0.6.19](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.18...@nezuchan/framework@0.6.19) (2023-09-16) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +## [0.6.18](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.17...@nezuchan/framework@0.6.18) (2023-09-13) + +**Note:** Version bump only for package @nezuchan/framework + + + + + +## [0.6.17](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.16...@nezuchan/framework@0.6.17) (2023-09-09) + + +### Features + +* allow to modify command strategy ([9edf472](https://github.com/NezuChan/library/commit/9edf472ab8acd7627b65376694ec5a54ed927533)) + + + + + +## [0.6.16](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.15...@nezuchan/framework@0.6.16) (2023-09-09) + + +### Bug Fixes + +* dont force fetch ([74b39aa](https://github.com/NezuChan/library/commit/74b39aaf9b9448307e3ef236806d25f7ace8ed33)) + + + + + +## [0.6.15](https://github.com/NezuChan/library/compare/@nezuchan/framework@0.6.14...@nezuchan/framework@0.6.15) (2023-09-07) + + +### Features + +* add socket.io to fastify-plugin ([#6](https://github.com/NezuChan/library/issues/6)) ([d1acf54](https://github.com/NezuChan/library/commit/d1acf54389abf43d2f637667d4e593a1db0eff55)) + + + + + +## 0.6.14 (2023-08-27) + + +### Bug Fixes + +* lint depends on build ([70b2170](https://github.com/NezuChan/library/commit/70b2170150cfe1ae9b41b4024f040e1f1a691e8a)) +* tsconfig couldnt create declaration ([274bd93](https://github.com/NezuChan/library/commit/274bd937c48d2c9fe39d2eca11aad72c8a7a9879)) + + +### Features + +* initial commit ([593b1bf](https://github.com/NezuChan/library/commit/593b1bf37243772275103deb237eb142610d149f)) +* use pnpm workspace version ([0593064](https://github.com/NezuChan/library/commit/05930644af446f6d82511c1ce4d921e9f800f150)) diff --git a/packages/framework/LICENSE b/packages/framework/LICENSE new file mode 100644 index 00000000..e62ec04c --- /dev/null +++ b/packages/framework/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/packages/framework/README.md b/packages/framework/README.md new file mode 100644 index 00000000..2146dc45 --- /dev/null +++ b/packages/framework/README.md @@ -0,0 +1,15 @@ +
+ +Logo + +# @nezuchan/framework + +**A Commands framework for @nezuchan/nezu-gateway.** + +[![GitHub](https://img.shields.io/github/license/nezuchan/library)](https://github.com/nezuchan/library/blob/main/LICENSE) +[![Discord](https://discordapp.com/api/guilds/785715968608567297/embed.png)](https://nezu.my.id) + +
+ +# Reference +- [Sapphire Framework](https://github.com/sapphiredev/framework) \ No newline at end of file diff --git a/packages/framework/package.json b/packages/framework/package.json new file mode 100644 index 00000000..c0b2ded6 --- /dev/null +++ b/packages/framework/package.json @@ -0,0 +1,48 @@ +{ + "name": "@nezuchan/framework", + "version": "0.7.3", + "description": "A Commands framework for @nezuchan/nezu-gateway.", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "build": "rimraf dist && tsc" + }, + "repository": { + "type": "git", + "url": "https://github.com/NezuChan/library" + }, + "homepage": "https://nezu.my.id", + "files": [ + "dist/**", + "LICENSE", + "README.md", + "package.json", + "pnpm-lock.yaml" + ], + "type": "module", + "author": "KagChi", + "license": "GPL-3.0", + "devDependencies": { + "@types/amqplib": "^0.10.4" + }, + "dependencies": { + "@discordjs/builders": "^1.7.0", + "@discordjs/collection": "^2.0.0", + "@nezuchan/core": "workspace:^", + "@sapphire/lexure": "^1.1.7", + "@sapphire/pieces": "^4.2.2", + "@sapphire/result": "^2.6.6", + "@sapphire/utilities": "^3.15.3", + "amqplib": "^0.10.3", + "discord-api-types": "^0.37.69", + "gen-esm-wrapper": "^1.1.3", + "tslib": "^2.6.2" + } +} diff --git a/packages/framework/src/Lib/CommandContext.ts b/packages/framework/src/Lib/CommandContext.ts new file mode 100644 index 00000000..a6da9ad7 --- /dev/null +++ b/packages/framework/src/Lib/CommandContext.ts @@ -0,0 +1,75 @@ +import type { BaseChannel, Guild, GuildMember, User } from "@nezuchan/core"; +import { BaseInteraction, Message } from "@nezuchan/core"; +import type { ArgumentStream } from "@sapphire/lexure"; +import type { APIInteractionResponseCallbackData, RESTPostAPIChannelMessageJSONBody } from "discord-api-types/v10"; + +export class CommandContext { + public constructor(private readonly context: BaseInteraction | Message, public messageArgs: ArgumentStream) {} + + public get message(): Message { + return this.context as Message; + } + + public get interaction(): BaseInteraction { + return this.context as BaseInteraction; + } + + public isInteraction(): boolean { + return this.context instanceof BaseInteraction; + } + + public isMessage(): boolean { + return this.context instanceof Message; + } + + public async send(options: APIInteractionResponseCallbackData | RESTPostAPIChannelMessageJSONBody): Promise { + if (this.isInteraction()) { + if (this.interaction.isCommandInteraction() && (this.interaction.isContextMenuInteraction() || this.interaction.isCommandInteraction())) { + if (this.interaction.deferred && !this.interaction.replied) { + return this.interaction.editReply(options); + } + + if (this.interaction.replied) { + return this.interaction.followUp(options); + } + } + + return this.interaction.reply(options); + } + return this.message.client.sendMessage(options, this.message.channelId!); + } + + public get guildId(): string | undefined { + if (this.isInteraction()) return this.interaction.guildId!; + return this.message.guildId; + } + + public get channelId(): string | null { + if (this.isInteraction()) return this.interaction.channelId; + return this.message.channelId!; + } + + public get userId(): string | undefined { + if (this.isInteraction()) return this.interaction.member?.id; + return this.message.author?.id; + } + + public async resolveMember({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + if (this.isInteraction()) return this.interaction.member; + return this.message.resolveMember({ force, cache }); + } + + public async resolveUser({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + if (this.isInteraction()) return this.interaction.member?.resolveUser({ force, cache }); + return this.message.author; + } + + public async resolveGuild({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + if (this.isInteraction()) return this.interaction.resolveGuild({ force, cache }); + return this.message.resolveGuild({ force, cache }); + } + + public async resolveChannel({ force, cache }: { force?: boolean; cache: boolean; } = { force: false, cache: true }): Promise { + return this.interaction.client.resolveChannel({ force, cache, guildId: this.guildId!, id: this.channelId! }); + } +} diff --git a/packages/framework/src/Lib/FlagUnorderedStrategy.ts b/packages/framework/src/Lib/FlagUnorderedStrategy.ts new file mode 100644 index 00000000..77aa9c73 --- /dev/null +++ b/packages/framework/src/Lib/FlagUnorderedStrategy.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import { PrefixedStrategy } from "@sapphire/lexure"; +import { Option } from "@sapphire/result"; + +export type FlagStrategyOptions = { + flags?: boolean | readonly string[]; + options?: boolean | readonly string[]; + prefixes?: string[]; + separators?: string[]; +}; + +const never = (): Option.None => Option.none; +const always = (): boolean => true; + +export class FlagUnorderedStrategy extends PrefixedStrategy { + public readonly flags: readonly string[] | true; + public readonly options: readonly string[] | true; + + public constructor({ flags, options, prefixes = ["--", "-", "—"], separators = ["=", ":"] }: FlagStrategyOptions = {}) { + super(prefixes, separators); + this.flags = flags || []; + this.options = options || []; + + if (this.flags === true) this.allowedFlag = always; + else if (this.flags.length === 0) this.matchFlag = never; + + if (this.options === true) { + this.allowedOption = always; + } else if (this.options.length === 0) { + this.matchOption = never; + } + } + + public override matchFlag(s: string): Option { + const result = super.matchFlag(s); + + if (result.isSomeAnd(value => this.allowedFlag(value))) return result; + + return Option.none; + } + + public override matchOption(s: string): Option { + const result = super.matchOption(s); + + if (result.isSomeAnd(option => this.allowedOption(option[0]))) return result; + + return Option.none; + } + + private allowedFlag(s: string): boolean { + return (this.flags as readonly string[]).includes(s); + } + + private allowedOption(s: string): boolean { + return (this.options as readonly string[]).includes(s); + } +} diff --git a/packages/framework/src/Lib/FrameworkClient.ts b/packages/framework/src/Lib/FrameworkClient.ts new file mode 100644 index 00000000..451e8987 --- /dev/null +++ b/packages/framework/src/Lib/FrameworkClient.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { ClientOptions as OClientOptions } from "@nezuchan/core"; +import { Client } from "@nezuchan/core"; +import type { Piece, Store } from "@sapphire/pieces"; +import { container } from "@sapphire/pieces"; + +import type { Channel } from "amqplib"; +import { PluginHook } from "../Plugins/Hook.js"; +import type { Plugin } from "../Plugins/Plugin.js"; +import { PluginManager } from "../Plugins/PluginManager.js"; +import { CommandStore } from "../Stores/CommandStore.js"; +import { InteractionHandlerStore } from "../Stores/InteractionHandlerStore.js"; +import { ListenerStore } from "../Stores/ListenerStore.js"; +import { PreconditionStore } from "../Stores/PreconditionStore.js"; +import { Events } from "../Utilities/EventEnums.js"; + +export class FrameworkClient extends Client { + public stores = container.stores; + public static plugins = new PluginManager(); + + public constructor( + public options: ClientOptions + ) { + super(options); + + container.client = this; + + for (const plugin of FrameworkClient.plugins.values(PluginHook.PreGenericsInitialization)) { + plugin.hook.call(this, options); + this.emit(Events.PluginLoaded, plugin.type, plugin.name); + } + + this.stores + .register(new ListenerStore() + .registerPath(resolve(dirname(fileURLToPath(import.meta.url)), "..", "Listeners"))) + .register(new CommandStore()) + .register(new PreconditionStore() + .registerPath(resolve(dirname(fileURLToPath(import.meta.url)), "..", "Preconditions"))) + .register(new InteractionHandlerStore()); + + this.stores.registerPath(this.options.baseUserDirectory); + + for (const plugin of FrameworkClient.plugins.values(PluginHook.PostInitialization)) { + plugin.hook.call(this, options); + this.emit(Events.PluginLoaded, plugin.type, plugin.name); + } + } + + public async connect(): Promise { + for (const plugin of FrameworkClient.plugins.values(PluginHook.PreLogin)) { + await plugin.hook.call(this, this.options); + this.emit(Events.PluginLoaded, plugin.type, plugin.name); + } + + super.connect(); + await Promise.all([...this.stores.values()].map(async (store: Store) => store.loadAll())); + if (this.options.registerCommands !== undefined) await this.stores.get("commands").postCommands(); + + for (const plugin of FrameworkClient.plugins.values(PluginHook.PostLogin)) { + await plugin.hook.call(this, this.options); + this.emit(Events.PluginLoaded, plugin.type, plugin.name); + } + } + + public async setupAmqp(channel: Channel): Promise { + for (const plugin of FrameworkClient.plugins.values(PluginHook.PreSetupAmqp)) { + await plugin.hook.call(this, this.options); + this.emit(Events.PluginLoaded, plugin.type, plugin.name); + } + + await super.setupAmqp(channel); + + for (const plugin of FrameworkClient.plugins.values(PluginHook.PostSetupAmqp)) { + await plugin.hook.call(this, this.options); + this.emit(Events.PluginLoaded, plugin.type, plugin.name); + } + } + + public static use(plugin: typeof Plugin): typeof FrameworkClient { + this.plugins.use(plugin); + return this; + } +} + +declare module "@sapphire/pieces" { + interface Container { + client: FrameworkClient; + } + + interface StoreRegistryEntries { + commands: CommandStore; + listeners: ListenerStore; + preconditions: PreconditionStore; + "interaction-handlers": InteractionHandlerStore; + } +} + +export type ClientOptions = OClientOptions & { + baseUserDirectory?: string; + fetchPrefix?(guildId?: string, authorId?: string, channelId?: string | null): Promise; + disableMentionPrefix?: boolean; + regexPrefix?: RegExp; + caseInsensitivePrefixes?: boolean; + caseInsensitiveCommands?: boolean; + registerCommands?: boolean; +}; diff --git a/packages/framework/src/Lib/Preconditions/Conditions/IPreconditionCondition.ts b/packages/framework/src/Lib/Preconditions/Conditions/IPreconditionCondition.ts new file mode 100644 index 00000000..bea52024 --- /dev/null +++ b/packages/framework/src/Lib/Preconditions/Conditions/IPreconditionCondition.ts @@ -0,0 +1,78 @@ +import type { BaseContextMenuInteraction, BaseInteraction, CommandInteraction, Message } from "@nezuchan/core"; +import type { Command } from "../../../Stores/Command.js"; +import type { InteractionHandler } from "../../../Stores/InteractionHandler.js"; +import type { PreconditionContext } from "../../../Stores/Precondition.js"; +import type { CommandContext } from "../../CommandContext.js"; +import type { IPreconditionContainer, PreconditionContainerReturn } from "../IPreconditionContainer.js"; + +export type IPreconditionCondition = { + messageSequential( + message: Message, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + messageParallel( + message: Message, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + chatInputSequential( + interaction: CommandInteraction, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + chatInputParallel( + interaction: CommandInteraction, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + contextMenuSequential( + interaction: BaseContextMenuInteraction, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + contextMenuParallel( + interaction: BaseContextMenuInteraction, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + contextCommandSequential( + ctx: CommandContext, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + contextCommandParallel( + ctx: CommandContext, + command: Command, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + interactionHandlerSequential( + interaction: BaseInteraction, + handler: InteractionHandler, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; + + interactionHandlerParallel( + interaction: BaseInteraction, + handler: InteractionHandler, + entries: readonly IPreconditionContainer[], + context?: PreconditionContext | undefined + ): PreconditionContainerReturn; +}; diff --git a/packages/framework/src/Lib/Preconditions/Conditions/PreconditionConditionAnd.ts b/packages/framework/src/Lib/Preconditions/Conditions/PreconditionConditionAnd.ts new file mode 100644 index 00000000..9bde4a80 --- /dev/null +++ b/packages/framework/src/Lib/Preconditions/Conditions/PreconditionConditionAnd.ts @@ -0,0 +1,65 @@ +import { Result } from "@sapphire/result"; +import type { IPreconditionCondition } from "./IPreconditionCondition.js"; + +export const PreconditionConditionAnd: IPreconditionCondition = { + async messageSequential(message, command, entries, context) { + for (const child of entries) { + const result = await child.messageRun(message, command, context); + if (result.isErr()) return result; + } + + return Result.ok(); + }, + async messageParallel(message, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.messageRun(message, command, context))); + return results.find(res => res.isErr()) ?? Result.ok(); + }, + async chatInputSequential(interaction, command, entries, context) { + for (const child of entries) { + const result = await child.chatInputRun(interaction, command, context); + if (result.isErr()) return result; + } + + return Result.ok(); + }, + async chatInputParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.chatInputRun(interaction, command, context))); + return results.find(res => res.isErr()) ?? Result.ok(); + }, + async contextMenuSequential(interaction, command, entries, context) { + for (const child of entries) { + const result = await child.contextMenuRun(interaction, command, context); + if (result.isErr()) return result; + } + + return Result.ok(); + }, + async contextMenuParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.contextMenuRun(interaction, command, context))); + return results.find(res => res.isErr()) ?? Result.ok(); + }, + async contextCommandSequential(ctx, command, entries, context) { + for (const child of entries) { + const result = await child.contextRun(ctx, command, context); + if (result.isErr()) return result; + } + + return Result.ok(); + }, + async contextCommandParallel(ctx, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.contextRun(ctx, command, context))); + return results.find(res => res.isErr()) ?? Result.ok(); + }, + async interactionHandlerParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.interactionHandlerRun(interaction, command, context))); + return results.find(res => res.isErr()) ?? Result.ok(); + }, + async interactionHandlerSequential(interaction, command, entries, context) { + for (const child of entries) { + const result = await child.interactionHandlerRun(interaction, command, context); + if (result.isErr()) return result; + } + + return Result.ok(); + } +}; diff --git a/packages/framework/src/Lib/Preconditions/Conditions/PreconditionConditionOr.ts b/packages/framework/src/Lib/Preconditions/Conditions/PreconditionConditionOr.ts new file mode 100644 index 00000000..97239fb5 --- /dev/null +++ b/packages/framework/src/Lib/Preconditions/Conditions/PreconditionConditionOr.ts @@ -0,0 +1,111 @@ +import { Result } from "@sapphire/result"; +import type { PreconditionContainerResult } from "../IPreconditionContainer.js"; +import type { IPreconditionCondition } from "./IPreconditionCondition.js"; + +export const PreconditionConditionOr: IPreconditionCondition = { + async messageSequential(message, command, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.messageRun(message, command, context); + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async messageParallel(message, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.messageRun(message, command, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async chatInputSequential(interaction, command, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.chatInputRun(interaction, command, context); + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async chatInputParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.chatInputRun(interaction, command, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async contextMenuSequential(interaction, command, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.contextMenuRun(interaction, command, context); + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async contextMenuParallel(interaction, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.contextMenuRun(interaction, command, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async contextCommandSequential(ctx, command, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.contextRun(ctx, command, context); + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async contextCommandParallel(ctx, command, entries, context) { + const results = await Promise.all(entries.map(entry => entry.contextRun(ctx, command, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async interactionHandlerSequential(interaction, handler, entries, context) { + let error: PreconditionContainerResult | null = null; + for (const child of entries) { + const result = await child.interactionHandlerRun(interaction, handler, context); + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + }, + async interactionHandlerParallel(interaction, handler, entries, context) { + const results = await Promise.all(entries.map(entry => entry.interactionHandlerRun(interaction, handler, context))); + + let error: PreconditionContainerResult | null = null; + for (const result of results) { + if (result.isOk()) return result; + error = result; + } + + return error ?? Result.ok(); + } +}; diff --git a/packages/framework/src/Lib/Preconditions/IPreconditionContainer.ts b/packages/framework/src/Lib/Preconditions/IPreconditionContainer.ts new file mode 100644 index 00000000..3113799e --- /dev/null +++ b/packages/framework/src/Lib/Preconditions/IPreconditionContainer.ts @@ -0,0 +1,23 @@ +import type { BaseContextMenuInteraction, BaseInteraction, CommandInteraction, Message } from "@nezuchan/core"; +import type { Result } from "@sapphire/result"; +import type { Awaitable } from "@sapphire/utilities"; +import type { Command } from "../../Stores/Command.js"; +import type { InteractionHandler } from "../../Stores/InteractionHandler.js"; +import type { PreconditionContext } from "../../Stores/Precondition.js"; +import type { UserError } from "../../Utilities/Errors/UserError.js"; +import type { CommandContext } from "../CommandContext.js"; + +export type PreconditionContainerResult = Result; + +export type PreconditionContainerReturn = Awaitable; + +export type AsyncPreconditionContainerReturn = Promise; + +export type IPreconditionContainer = { + messageRun(message: Message, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + chatInputRun(interaction: CommandInteraction, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + contextMenuRun(interaction: BaseContextMenuInteraction, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + contextRun(ctx: CommandContext, command: Command, context?: PreconditionContext): PreconditionContainerReturn; + + interactionHandlerRun(interaction: BaseInteraction, handler: InteractionHandler, context?: PreconditionContext): PreconditionContainerReturn; +}; diff --git a/packages/framework/src/Lib/Preconditions/PreconditionContainerArray.ts b/packages/framework/src/Lib/Preconditions/PreconditionContainerArray.ts new file mode 100644 index 00000000..b5f3d490 --- /dev/null +++ b/packages/framework/src/Lib/Preconditions/PreconditionContainerArray.ts @@ -0,0 +1,131 @@ +import { Collection } from "@discordjs/collection"; +import type { BaseContextMenuInteraction, BaseInteraction, CommandInteraction, Message } from "@nezuchan/core"; +import type { Command } from "../../Stores/Command.js"; +import type { InteractionHandler } from "../../Stores/InteractionHandler.js"; +import type { SimplePreconditionKeys, PreconditionKeys, PreconditionContext } from "../../Stores/Precondition.js"; +import type { CommandContext } from "../CommandContext.js"; +import type { IPreconditionCondition } from "./Conditions/IPreconditionCondition.js"; +import { PreconditionConditionAnd } from "./Conditions/PreconditionConditionAnd.js"; +import { PreconditionConditionOr } from "./Conditions/PreconditionConditionOr.js"; +import type { IPreconditionContainer, PreconditionContainerReturn } from "./IPreconditionContainer.js"; +import type { PreconditionSingleResolvable, PreconditionSingleResolvableDetails, SimplePreconditionSingleResolvableDetails } from "./PreconditionContainerSingle.js"; +import { PreconditionContainerSingle } from "./PreconditionContainerSingle.js"; + +export enum PreconditionRunMode { + Sequential = 0, + Parallel = 1 +} + +export enum PreconditionRunCondition { + And = 0, + Or = 1 +} + +export type PreconditionArrayResolvableDetails = { + entries: readonly PreconditionEntryResolvable[]; + mode: PreconditionRunMode; +}; + +export type PreconditionArrayResolvable = PreconditionArrayResolvableDetails | readonly PreconditionEntryResolvable[]; + +export type PreconditionEntryResolvable = PreconditionArrayResolvable | PreconditionSingleResolvable; + +function isSingle(entry: PreconditionEntryResolvable): entry is PreconditionSingleResolvable { + return typeof entry === "string" || Reflect.has(entry, "name"); +} + +export class PreconditionContainerArray implements IPreconditionContainer { + public readonly mode: PreconditionRunMode; + + public readonly entries: IPreconditionContainer[]; + + public readonly runCondition: PreconditionRunCondition; + + public static readonly conditions = new Collection([ + [PreconditionRunCondition.And, PreconditionConditionAnd], + [PreconditionRunCondition.Or, PreconditionConditionOr] + ]); + + public constructor(data: PreconditionArrayResolvable = [], parent: PreconditionContainerArray | null = null) { + this.entries = []; + this.runCondition = parent?.runCondition === PreconditionRunCondition.And ? PreconditionRunCondition.Or : PreconditionRunCondition.And; + + if (Array.isArray(data)) { + const casted = data as readonly PreconditionEntryResolvable[]; + + this.mode = parent?.mode ?? PreconditionRunMode.Sequential; + this.parse(casted); + } else { + const casted = data as PreconditionArrayResolvableDetails; + + this.mode = casted.mode; + this.parse(casted.entries); + } + } + + public add(entry: IPreconditionContainer): this { + this.entries.push(entry); + return this; + } + + public append(keyOrEntries: PreconditionContainerArray | SimplePreconditionKeys | SimplePreconditionSingleResolvableDetails): this; + public append(entry: PreconditionSingleResolvableDetails): this; + public append(entry: PreconditionContainerArray | PreconditionSingleResolvable): this { + this.entries.push(entry instanceof PreconditionContainerArray ? entry : new PreconditionContainerSingle(entry)); + return this; + } + + public messageRun(message: Message, command: Command, context?: PreconditionContext | undefined): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.messageSequential(message, command, this.entries, context) + : this.condition.messageParallel(message, command, this.entries, context); + } + + public chatInputRun( + interaction: CommandInteraction, + command: Command, + context?: PreconditionContext | undefined + ): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.chatInputSequential(interaction, command, this.entries, context) + : this.condition.chatInputParallel(interaction, command, this.entries, context); + } + + public contextMenuRun( + interaction: BaseContextMenuInteraction, + command: Command, + context?: PreconditionContext | undefined + ): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.contextMenuSequential(interaction, command, this.entries, context) + : this.condition.contextMenuParallel(interaction, command, this.entries, context); + } + + public contextRun(ctx: CommandContext, command: Command, context?: PreconditionContext): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.contextCommandSequential(ctx, command, this.entries, context) + : this.condition.contextCommandSequential(ctx, command, this.entries, context); + } + + public interactionHandlerRun(interaction: BaseInteraction, handler: InteractionHandler, context?: PreconditionContext): PreconditionContainerReturn { + return this.mode === PreconditionRunMode.Sequential + ? this.condition.interactionHandlerParallel(interaction, handler, this.entries, context) + : this.condition.interactionHandlerSequential(interaction, handler, this.entries, context); + } + + protected parse(entries: Iterable): this { + for (const entry of entries) { + this.add( + isSingle(entry) + ? new PreconditionContainerSingle(entry) + : new PreconditionContainerArray(entry, this) + ); + } + + return this; + } + + protected get condition(): IPreconditionCondition { + return PreconditionContainerArray.conditions.get(this.runCondition)!; + } +} diff --git a/packages/framework/src/Lib/Preconditions/PreconditionContainerSingle.ts b/packages/framework/src/Lib/Preconditions/PreconditionContainerSingle.ts new file mode 100644 index 00000000..a469c2e7 --- /dev/null +++ b/packages/framework/src/Lib/Preconditions/PreconditionContainerSingle.ts @@ -0,0 +1,103 @@ +import type { BaseContextMenuInteraction, BaseInteraction, CommandInteraction, Message } from "@nezuchan/core"; +import { container } from "@sapphire/pieces"; +import { err } from "@sapphire/result"; +import type { Result } from "@sapphire/result"; +import type { Awaitable } from "@sapphire/utilities"; +import type { Command } from "../../Stores/Command.js"; +import type { InteractionHandler } from "../../Stores/InteractionHandler.js"; +import type { PreconditionContext, PreconditionKeys, Preconditions, SimplePreconditionKeys } from "../../Stores/Precondition.js"; +import { Identifiers } from "../../Utilities/Errors/Identifiers.js"; +import { UserError } from "../../Utilities/Errors/UserError.js"; +import type { CommandContext } from "../CommandContext.js"; +import type { IPreconditionContainer } from "./IPreconditionContainer.js"; + +export type SimplePreconditionSingleResolvableDetails = { + name: SimplePreconditionKeys; +}; + +export type PreconditionSingleResolvableDetails = { + name: K; + context: Preconditions[K]; +}; + +export type PreconditionSingleResolvable = PreconditionSingleResolvableDetails | SimplePreconditionKeys | SimplePreconditionSingleResolvableDetails; + +export class PreconditionContainerSingle implements IPreconditionContainer { + public readonly context: Record; + public readonly name: string; + + public constructor(data: PreconditionSingleResolvable) { + if (typeof data === "string") { + this.name = data; + this.context = {}; + } else { + this.context = Reflect.get(data, "context") ?? {}; + this.name = data.name; + } + } + + public messageRun(message: Message, command: Command, context?: PreconditionContext | undefined): Awaitable> { + const precondition = container.stores.get("preconditions").get(this.name); + if (precondition) { + return precondition.messageRun + ? precondition.messageRun(message, command, { ...context, ...this.context }) + : precondition.error({ + identifier: Identifiers.PreconditionMissingMessageHandler, + message: `The precondition "${precondition.name}" is missing a "messageRun" handler, but it was requested for the "${command.name}" command.` + }); + } + return err(new UserError({ identifier: Identifiers.PreconditionUnavailable, message: `The precondition "${this.name}" is not available.` })); + } + + public chatInputRun(interaction: CommandInteraction, command: Command, context?: PreconditionContext | undefined): Awaitable> { + const precondition = container.stores.get("preconditions").get(this.name); + if (precondition) { + return precondition.chatInputRun + ? precondition.chatInputRun(interaction, command, { ...context, ...this.context }) + : precondition.error({ + identifier: Identifiers.PreconditionMissingChatInputHandler, + message: `The precondition "${precondition.name}" is missing a "chatInputRun" handler, but it was requested for the "${command.name}" command.` + }); + } + return err(new UserError({ identifier: Identifiers.PreconditionUnavailable, message: `The precondition "${this.name}" is not available.` })); + } + + public contextMenuRun(interaction: BaseContextMenuInteraction, command: Command, context?: PreconditionContext | undefined): Awaitable> { + const precondition = container.stores.get("preconditions").get(this.name); + if (precondition) { + return precondition.contextMenuRun + ? precondition.contextMenuRun(interaction, command, { ...context, ...this.context }) + : precondition.error({ + identifier: Identifiers.PreconditionMissingContextMenuHandler, + message: `The precondition "${precondition.name}" is missing a "contextMenuRun" handler, but it was requested for the "${command.name}" command.` + }); + } + return err(new UserError({ identifier: Identifiers.PreconditionUnavailable, message: `The precondition "${this.name}" is not available.` })); + } + + public contextRun(ctx: CommandContext, command: Command, context?: PreconditionContext | undefined): Awaitable> { + const precondition = container.stores.get("preconditions").get(this.name); + if (precondition) { + return precondition.contextRun + ? precondition.contextRun(ctx, command, { ...context, ...this.context }) + : precondition.error({ + identifier: Identifiers.PreconditionMissingContextHandler, + message: `The precondition "${precondition.name}" is missing a "contextRun" handler, but it was requested for the "${command.name}" command.` + }); + } + return err(new UserError({ identifier: Identifiers.PreconditionUnavailable, message: `The precondition "${this.name}" is not available.` })); + } + + public interactionHandlerRun(interaction: BaseInteraction, handler: InteractionHandler, context?: PreconditionContext | undefined): Awaitable> { + const precondition = container.stores.get("preconditions").get(this.name); + if (precondition) { + return precondition.interactionHandlerRun + ? precondition.interactionHandlerRun(interaction, handler, { ...context, ...this.context }) + : precondition.error({ + identifier: Identifiers.PreconditionMissingInteractionHandler, + message: `The precondition "${precondition.name}" is missing a "PreconditionMissingInteractionHandler" handler, but it was requested for the "${handler.name}" handler.` + }); + } + return err(new UserError({ identifier: Identifiers.PreconditionUnavailable, message: `The precondition "${this.name}" is not available.` })); + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/ChatInputCommandAccepted.ts b/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/ChatInputCommandAccepted.ts new file mode 100644 index 00000000..498d5223 --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/ChatInputCommandAccepted.ts @@ -0,0 +1,19 @@ +import type { CommandInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import type { Command } from "../../../Stores/Command.js"; +import { Listener } from "../../../Stores/Listener.js"; +import { Events } from "../../../Utilities/EventEnums.js"; + +export class ChatInputCommandAccepted extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.ChatInputCommandAccepted + }); + } + + public async run(payload: { command: Command; interaction: CommandInteraction; }): Promise { + const result = await Result.fromAsync(() => payload.command.chatInputRun!(payload.interaction)); + result.inspectErr(error => this.container.client.emit(Events.ChatInputCommandError, error, { ...payload })); + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/PossibleChatInputCommand.ts b/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/PossibleChatInputCommand.ts new file mode 100644 index 00000000..b5214621 --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/PossibleChatInputCommand.ts @@ -0,0 +1,49 @@ +import type { CommandInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import { Listener } from "../../../Stores/Listener.js"; +import { Events } from "../../../Utilities/EventEnums.js"; + +export class PossibleChatInputCommand extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.PossibleChatInputCommand + }); + } + + public run(interaction: CommandInteraction): void { + const commandStore = this.container.stores.get("commands"); + if (interaction.commandName === null) return; + + const command = commandStore.get(interaction.commandName); + + if (command?.chatInputRun !== undefined) { + this.container.client.emit( + Events.PreChatInputCommandRun, { + command, + interaction, + context: { commandId: interaction.id, commandName: interaction.commandName } + } + ); + return; + } + + if (command?.options.enableChatInputCommand === false) { + this.container.client.emit(Events.ChatInputCommandDisabled, { + command, + interaction, + context: { commandId: interaction.id, commandName: interaction.commandName } + }); + return; + } + + if (command?.contextRun !== undefined) { + this.container.client.emit( + Events.PreContextCommandRun, { + command, + interaction, + context: interaction + } + ); + } + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/PreChatInputCommandRun.ts b/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/PreChatInputCommandRun.ts new file mode 100644 index 00000000..f807993e --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/ChatInputCommand/PreChatInputCommandRun.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { CommandInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import type { Command } from "../../../Stores/Command.js"; +import { Listener } from "../../../Stores/Listener.js"; +import { Events } from "../../../Utilities/EventEnums.js"; + +export class PreChatInputCommandRun extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.PreChatInputCommandRun + }); + } + + public async run(payload: { command: Command; interaction: CommandInteraction; }): Promise { + const globalResult = await this.container.stores.get("preconditions").chatInputRun(payload.interaction, payload.command, payload as any); + if (globalResult.isErr()) { + this.container.client.emit(Events.ChatInputCommandDenied, globalResult.unwrapErr(), payload); + return; + } + + const localResult = await payload.command.preconditions.chatInputRun(payload.interaction, payload.command, payload as any); + if (localResult.isErr()) { + this.container.client.emit(Events.ChatInputCommandDenied, localResult.unwrapErr(), payload); + return; + } + this.container.client.emit(Events.ChatInputCommandAccepted, payload); + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/ContextMenuCommandAccepted.ts b/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/ContextMenuCommandAccepted.ts new file mode 100644 index 00000000..1a3237dd --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/ContextMenuCommandAccepted.ts @@ -0,0 +1,19 @@ +import type { BaseContextMenuInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import type { Command } from "../../../Stores/Command.js"; +import { Listener } from "../../../Stores/Listener.js"; +import { Events } from "../../../Utilities/EventEnums.js"; + +export class ContextMenuCommandAccepted extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.ContextMenuCommandAccepted + }); + } + + public async run(payload: { command: Command; interaction: BaseContextMenuInteraction; }): Promise { + const result = await Result.fromAsync(() => payload.command.contextMenuRun!(payload.interaction)); + result.inspectErr(error => this.container.client.emit(Events.ChatInputCommandError, error, { ...payload })); + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/PossibleContextMenuCommand.ts b/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/PossibleContextMenuCommand.ts new file mode 100644 index 00000000..49f7108c --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/PossibleContextMenuCommand.ts @@ -0,0 +1,49 @@ +import type { BaseContextMenuInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import { Listener } from "../../../Stores/Listener.js"; +import { Events } from "../../../Utilities/EventEnums.js"; + +export class PossibleContextMenuCommand extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.PossibleContextMenuCommand + }); + } + + public run(interaction: BaseContextMenuInteraction): void { + const commandStore = this.container.stores.get("commands"); + if (!interaction.commandName) return; + + const command = commandStore.get(interaction.commandName); + + if (command?.contextMenuRun) { + this.container.client.emit( + Events.PreContextCommandRun, { + command, + interaction, + context: { commandId: interaction.id, commandName: interaction.commandName } + } + ); + return; + } + + if (command?.options.enableContextMenuCommand === false) { + this.container.client.emit(Events.ContextMenuCommandDisabled, { + command, + interaction, + context: { commandId: interaction.id, commandName: interaction.commandName } + }); + return; + } + + if (command?.contextRun) { + this.container.client.emit( + Events.PreContextCommandRun, { + command, + interaction, + context: interaction + } + ); + } + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/PreContextMenuCommandRun.ts b/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/PreContextMenuCommandRun.ts new file mode 100644 index 00000000..4e4c4ab4 --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/ContextMenuCommand/PreContextMenuCommandRun.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { BaseContextMenuInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import type { Command } from "../../../Stores/Command.js"; +import { Listener } from "../../../Stores/Listener.js"; +import { Events } from "../../../Utilities/EventEnums.js"; + +export class PreContextMenuCommandRun extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.PreContextMenuCommandRun + }); + } + + public async run(payload: { command: Command; interaction: BaseContextMenuInteraction; }): Promise { + const globalResult = await this.container.stores.get("preconditions").contextMenuRun(payload.interaction, payload.command, payload as any); + if (globalResult.isErr()) { + this.container.client.emit(Events.ContextMenuCommandDenied, globalResult.unwrapErr(), payload); + return; + } + + const localResult = await payload.command.preconditions.contextMenuRun(payload.interaction, payload.command, payload as any); + if (localResult.isErr()) { + this.container.client.emit(Events.ContextMenuCommandDenied, localResult.unwrapErr(), payload); + return; + } + + this.container.client.emit(Events.ContextMenuCommandAccepted, payload); + } +} diff --git a/packages/framework/src/Listeners/ApplicationCommands/PossibleAutoCompleteInteraction.ts b/packages/framework/src/Listeners/ApplicationCommands/PossibleAutoCompleteInteraction.ts new file mode 100644 index 00000000..93e6286a --- /dev/null +++ b/packages/framework/src/Listeners/ApplicationCommands/PossibleAutoCompleteInteraction.ts @@ -0,0 +1,36 @@ +import type { AutoCompleteInteraction } from "@nezuchan/core"; +import type { Piece } from "@sapphire/pieces"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class PossibleAutoCompleteInteraction extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.PossibleAutocompleteInteraction + }); + } + + public async run(interaction: AutoCompleteInteraction): Promise { + const commandStore = this.container.stores.get("commands"); + if (!interaction.commandName) return; + + const command = commandStore.get(interaction.commandName); + + if (command?.autoCompleteRun) { + try { + await command.autoCompleteRun(interaction); + this.container.client.emit(Events.CommandAutocompleteInteractionSuccess, { + command, + interaction + }); + } catch (error) { + this.container.client.emit(Events.CommandAutocompleteInteractionError, error, { + command, + interaction + }); + } + } + + await this.container.client.stores.get("interaction-handlers").run(interaction); + } +} diff --git a/packages/framework/src/Listeners/ContextCommand/ContextCommandAccepted.ts b/packages/framework/src/Listeners/ContextCommand/ContextCommandAccepted.ts new file mode 100644 index 00000000..8df281c8 --- /dev/null +++ b/packages/framework/src/Listeners/ContextCommand/ContextCommandAccepted.ts @@ -0,0 +1,19 @@ +import type { Piece } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import type { CommandContext } from "../../Lib/CommandContext.js"; +import type { Command } from "../../Stores/Command.js"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class ContextCommandAccepted extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.ContextCommandAccepted + }); + } + + public async run(payload: { command: Command; context: CommandContext; }): Promise { + const result = await Result.fromAsync(() => payload.command.contextRun!(payload.context)); + result.inspectErr(error => this.container.client.emit(Events.ContextCommandError, error, { ...payload })); + } +} diff --git a/packages/framework/src/Listeners/ContextCommand/PreContextCommandRun.ts b/packages/framework/src/Listeners/ContextCommand/PreContextCommandRun.ts new file mode 100644 index 00000000..3de92b81 --- /dev/null +++ b/packages/framework/src/Listeners/ContextCommand/PreContextCommandRun.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { BaseInteraction, Message } from "@nezuchan/core"; +import { Parser, ArgumentStream } from "@sapphire/lexure"; +import type { Piece } from "@sapphire/pieces"; +import { CommandContext } from "../../Lib/CommandContext.js"; +import type { Command } from "../../Stores/Command.js"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class PreContextCommandRun extends Listener { + public constructor(context: Piece.LoaderContext) { + super(context, { + name: Events.PreContextCommandRun + }); + } + + public async run(payload: { command: Command; context: BaseInteraction | Message; parameters?: string; }): Promise { + const parser = new Parser(payload.command.strategy); + const stream = new ArgumentStream(parser.run(payload.command.lexer.run(payload.parameters ?? ""))); + const context = new CommandContext(payload.context, stream); + + const globalResult = await this.container.stores.get("preconditions").contextRun(context, payload.command, payload as any); + if (globalResult.isErr()) { + this.container.client.emit(Events.ContextCommandDenied, context, globalResult.unwrapErr(), payload); + return; + } + + const localResult = await payload.command.preconditions.contextRun(context, payload.command, payload as any); + if (localResult.isErr()) { + this.container.client.emit(Events.ContextCommandDenied, context, localResult.unwrapErr(), payload); + return; + } + + this.container.client.emit(Events.ContextCommandAccepted, { + ...payload, + context + }); + } +} diff --git a/packages/framework/src/Listeners/InteractionCreate.ts b/packages/framework/src/Listeners/InteractionCreate.ts new file mode 100644 index 00000000..8cae2dc3 --- /dev/null +++ b/packages/framework/src/Listeners/InteractionCreate.ts @@ -0,0 +1,24 @@ +import type { BaseInteraction } from "@nezuchan/core"; +import type { LoaderPieceContext } from "@sapphire/pieces"; +import { Listener } from "../Stores/Listener.js"; +import { Events } from "../Utilities/EventEnums.js"; + +export class InteractionCreate extends Listener { + public constructor(context: LoaderPieceContext) { + super(context, { + name: Events.InteractionCreate + }); + } + + public async run(interaction: BaseInteraction): Promise { + if (interaction.isCommandInteraction()) { + this.container.client.emit(Events.PossibleChatInputCommand, interaction); + } else if (interaction.isContextMenuInteraction()) { + this.container.client.emit(Events.PossibleContextMenuCommand, interaction); + } else if (interaction.isAutoCompleteInteraction()) { + this.container.client.emit(Events.PossibleAutocompleteInteraction, interaction); + } else if (interaction.isComponentInteraction() || interaction.isModalSubmit()) { + await this.container.client.stores.get("interaction-handlers").run(interaction); + } + } +} diff --git a/packages/framework/src/Listeners/MessageCommands/PreMessageCommandRun.ts b/packages/framework/src/Listeners/MessageCommands/PreMessageCommandRun.ts new file mode 100644 index 00000000..aa7786ea --- /dev/null +++ b/packages/framework/src/Listeners/MessageCommands/PreMessageCommandRun.ts @@ -0,0 +1,35 @@ +import type { Message } from "@nezuchan/core"; +import type { PieceContext } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import type { Command } from "../../Stores/Command.js"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class PreMessageCommandRun extends Listener { + public constructor(context: PieceContext) { + super(context, { + name: Events.PreMessageCommandRun + }); + } + + public async run(payload: { command: Command; message: Message; }): Promise { + const globalResult = await this.container.stores.get("preconditions").messageRun(payload.message, payload.command, payload); + if (globalResult.isErr()) { + this.container.client.emit(Events.MessageCommandDenied, globalResult.unwrapErr(), payload); + return; + } + + const localResult = await payload.command.preconditions.messageRun(payload.message, payload.command, payload); + if (localResult.isErr()) { + this.container.client.emit(Events.MessageCommandDenied, localResult.unwrapErr(), payload); + return; + } + + const result = await Result.fromAsync(() => payload.command.messageRun!(payload.message)); + if (result.isOk()) { + this.container.client.emit(Events.MessageCommandAccepted, payload); + } else { + this.container.client.emit(Events.MessageCommandError, result.unwrapErr(), payload); + } + } +} diff --git a/packages/framework/src/Listeners/MessageCommands/PreMessageParsed.ts b/packages/framework/src/Listeners/MessageCommands/PreMessageParsed.ts new file mode 100644 index 00000000..6a24b556 --- /dev/null +++ b/packages/framework/src/Listeners/MessageCommands/PreMessageParsed.ts @@ -0,0 +1,65 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { Message } from "@nezuchan/core"; +import type { PieceContext } from "@sapphire/pieces"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class PreMessageParsed extends Listener { + public constructor(context: PieceContext) { + super(context, { + name: Events.PreMessageParsed + }); + } + + public async run(message: Message): Promise { + if (message.author?.bot ?? message.webhookId) return; + + const mentionPrefix = await this.getMentionPrefix(message); + let prefix: RegExp | string | null = null; + + if (mentionPrefix) { + if (message.content.length === mentionPrefix.length) { + this.container.client.emit(Events.MentionPrefixOnly, message); + return; + } + + prefix = mentionPrefix; + } else if (this.container.client.options.regexPrefix?.test(message.content)) { + prefix = this.container.client.options.regexPrefix; + } else if (this.container.client.options.fetchPrefix) { + const prefixes = await this.container.client.options.fetchPrefix(message.guildId, message.author?.id, message.channelId); + const parsed = this.getPrefix(message.content, prefixes); + if (parsed !== null) prefix = parsed; + } + + if (prefix === null) this.container.client.emit(Events.NonPrefixedMessage, message); + else this.container.client.emit(Events.PrefixedMessage, message, prefix); + } + + private async getMentionPrefix(message: Message): Promise { + if (this.container.client.options.disableMentionPrefix) return null; + if (message.content.length < 20 || !message.content.startsWith("<@")) return null; + const me = await this.container.client.resolveUser({ cache: true, force: true, id: this.container.client.clientId }); + + if (me) { + if (message.content.startsWith(`<@!${me.id}>`)) return message.content.slice(0, Math.max(0, me.id.length + 4)); + if (message.content.startsWith(`<@${me.id}>`)) return message.content.slice(0, Math.max(0, me.id.length + 3)); + } + + return null; + } + + private getPrefix(content: string, prefixes: string | readonly string[] | null): string | null { + if (prefixes === null) return null; + const { caseInsensitivePrefixes } = this.container.client.options; + + if (caseInsensitivePrefixes) content = content.toLowerCase(); + + if (typeof prefixes === "string") { + return content.startsWith(caseInsensitivePrefixes ? prefixes.toLowerCase() : prefixes) ? prefixes : null; + } + + return prefixes.find(prefix => content.startsWith(caseInsensitivePrefixes ? prefix.toLowerCase() : prefix)) ?? null; + } +} diff --git a/packages/framework/src/Listeners/MessageCommands/PrefixedMessage.ts b/packages/framework/src/Listeners/MessageCommands/PrefixedMessage.ts new file mode 100644 index 00000000..322f0a13 --- /dev/null +++ b/packages/framework/src/Listeners/MessageCommands/PrefixedMessage.ts @@ -0,0 +1,66 @@ +import type { Message } from "@nezuchan/core"; +import type { PieceContext } from "@sapphire/pieces"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class PrefixedMessage extends Listener { + public constructor(context: PieceContext) { + super(context, { + name: Events.PrefixedMessage + }); + } + + public run(message: Message, prefix: RegExp | string): any { + const commandPrefix = this.getCommandPrefix(message.content, prefix); + const prefixLess = message.content.slice(commandPrefix.length).trim(); + + const spaceIndex = prefixLess.indexOf(" "); + const commandName = spaceIndex === -1 ? prefixLess : prefixLess.slice(0, spaceIndex); + if (commandName.length === 0) { + this.container.client.emit(Events.UnknownMessageCommandName, { message, prefix, commandPrefix, prefixLess }); + return; + } + + const command = this.container.stores.get("commands").get(this.container.client.options.caseInsensitiveCommands ? commandName.toLowerCase() : commandName); + if (!command) { + this.container.client.emit(Events.UnknownMessageCommand, { message, prefix, commandName, commandPrefix, prefixLess }); + return; + } + + const parameters = spaceIndex === -1 ? "" : prefixLess.slice(Math.max(0, spaceIndex + 1)).trim(); + + if (command.messageRun) { + this.container.client.emit(Events.PreMessageCommandRun, { + message, + command, + parameters, + context: { commandName, commandPrefix, prefix, prefixLess } + }); + return; + } + + if (command.options.enableMessageCommand === false) { + this.container.client.emit(Events.MessageCommandDisabled, { + message, + command, + parameters, + context: { commandName, commandPrefix, prefix, prefixLess } + }); + return; + } + + if (command.contextRun) { + this.container.client.emit(Events.PreContextCommandRun, { + message, + command, + parameters, + context: message, + prefixLess + }); + } + } + + private getCommandPrefix(content: string, prefix: RegExp | string): string { + return typeof prefix === "string" ? prefix : prefix.exec(content)![0]; + } +} diff --git a/packages/framework/src/Listeners/MessageCreate.ts b/packages/framework/src/Listeners/MessageCreate.ts new file mode 100644 index 00000000..0841b66a --- /dev/null +++ b/packages/framework/src/Listeners/MessageCreate.ts @@ -0,0 +1,18 @@ +import type { Message } from "@nezuchan/core"; +import type { LoaderPieceContext } from "@sapphire/pieces"; +import { Listener } from "../Stores/Listener.js"; +import { Events } from "../Utilities/EventEnums.js"; + +export class MessageCreate extends Listener { + public constructor(context: LoaderPieceContext) { + super(context, { + name: Events.MessageCreate + }); + } + + public run(message: Message): void { + if (message.author?.bot ?? message.webhookId) return; + + this.container.client.emit(Events.PreMessageParsed, message); + } +} diff --git a/packages/framework/src/Listeners/Raw/InteractionCreate.ts b/packages/framework/src/Listeners/Raw/InteractionCreate.ts new file mode 100644 index 00000000..7f715a73 --- /dev/null +++ b/packages/framework/src/Listeners/Raw/InteractionCreate.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { CommandInteraction, MessageContextMenuInteraction, UserContextMenuInteraction, AutoCompleteInteraction, MessageComponentInteraction, BaseInteraction, ModalSubmitInteraction } from "@nezuchan/core"; +import type { LoaderPieceContext } from "@sapphire/pieces"; +import type { GatewayInteractionCreateDispatch } from "discord-api-types/v10"; +import { ApplicationCommandType, GatewayDispatchEvents, InteractionType } from "discord-api-types/v10"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class InteractionCreate extends Listener { + public constructor(context: LoaderPieceContext) { + super(context, { + name: GatewayDispatchEvents.InteractionCreate + }); + } + + public run(payload: GatewayInteractionCreateDispatch["d"]): void { + switch (payload.type) { + case InteractionType.ApplicationCommand: + switch (payload.data.type) { + case ApplicationCommandType.ChatInput: { + this.container.client.emit(Events.InteractionCreate, new CommandInteraction(payload, this.container.client)); + break; + } + case ApplicationCommandType.Message: { + this.container.client.emit(Events.InteractionCreate, new MessageContextMenuInteraction(payload, this.container.client)); + break; + } + case ApplicationCommandType.User: { + this.container.client.emit(Events.InteractionCreate, new UserContextMenuInteraction(payload, this.container.client)); + break; + } + + default: + break; + } + break; + case InteractionType.ApplicationCommandAutocomplete: + this.container.client.emit(Events.InteractionCreate, new AutoCompleteInteraction(payload, this.container.client)); + break; + case InteractionType.MessageComponent: + this.container.client.emit(Events.InteractionCreate, new MessageComponentInteraction(payload, this.container.client)); + break; + case InteractionType.ModalSubmit: + this.container.client.emit(Events.InteractionCreate, new ModalSubmitInteraction(payload, this.container.client)); + break; + default: + this.container.client.emit(Events.InteractionCreate, new BaseInteraction(payload, this.container.client)); + break; + } + } +} diff --git a/packages/framework/src/Listeners/Raw/MessageCreate.ts b/packages/framework/src/Listeners/Raw/MessageCreate.ts new file mode 100644 index 00000000..a77b646a --- /dev/null +++ b/packages/framework/src/Listeners/Raw/MessageCreate.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { Message } from "@nezuchan/core"; +import type { LoaderPieceContext } from "@sapphire/pieces"; +import type { GatewayMessageCreateDispatch } from "discord-api-types/v10"; +import { GatewayDispatchEvents } from "discord-api-types/v10"; +import { Listener } from "../../Stores/Listener.js"; +import { Events } from "../../Utilities/EventEnums.js"; + +export class InteractionCreate extends Listener { + public constructor(context: LoaderPieceContext) { + super(context, { + name: GatewayDispatchEvents.MessageCreate + }); + } + + public run(payload: GatewayMessageCreateDispatch["d"]): void { + const message = new Message(payload, this.container.client); + this.container.client.emit(Events.MessageCreate, message); + } +} diff --git a/packages/framework/src/Listeners/Raw/Raw.ts b/packages/framework/src/Listeners/Raw/Raw.ts new file mode 100644 index 00000000..bd240c91 --- /dev/null +++ b/packages/framework/src/Listeners/Raw/Raw.ts @@ -0,0 +1,16 @@ +import { Events } from "@nezuchan/core"; +import type { LoaderPieceContext } from "@sapphire/pieces"; +import type { GatewayDispatchPayload } from "discord-api-types/v10"; +import { Listener } from "../../Stores/Listener.js"; + +export class InteractionCreate extends Listener { + public constructor(context: LoaderPieceContext) { + super(context, { + name: Events.RAW + }); + } + + public run(payload: GatewayDispatchPayload): void { + this.container.client.emit(payload.t, payload.d); + } +} diff --git a/packages/framework/src/Plugins/Hook.ts b/packages/framework/src/Plugins/Hook.ts new file mode 100644 index 00000000..e4fdd2cf --- /dev/null +++ b/packages/framework/src/Plugins/Hook.ts @@ -0,0 +1,9 @@ +export enum PluginHook { + PreGenericsInitialization = "preGenericsInitialization", + PreInitialization = "preInitialization", + PostInitialization = "postInitialization", + PreLogin = "preLogin", + PostLogin = "postLogin", + PreSetupAmqp = "preSetupAmqp", + PostSetupAmqp = "postSetupAmqp" +} diff --git a/packages/framework/src/Plugins/Plugin.ts b/packages/framework/src/Plugins/Plugin.ts new file mode 100644 index 00000000..3ca6c73e --- /dev/null +++ b/packages/framework/src/Plugins/Plugin.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +import type { ClientOptions } from "@nezuchan/core"; +import type { Awaitable } from "@sapphire/utilities"; +import type { FrameworkClient } from "../Lib/FrameworkClient.js"; +import { preGenericsInitialization, preInitialization, postInitialization, preLogin, postLogin, postSetupAmqp, preSetupAmqp } from "./Symbols.js"; + +export abstract class Plugin { + public static [preGenericsInitialization]?: (this: FrameworkClient, options: ClientOptions) => void; + public static [preInitialization]?: (this: FrameworkClient, options: ClientOptions) => void; + public static [postInitialization]?: (this: FrameworkClient, options: ClientOptions) => void; + public static [preLogin]?: (this: FrameworkClient, options: ClientOptions) => Awaitable; + public static [postLogin]?: (this: FrameworkClient, options: ClientOptions) => Awaitable; + public static [preSetupAmqp]?: (this: FrameworkClient, options: ClientOptions) => Awaitable; + public static [postSetupAmqp]?: (this: FrameworkClient, options: ClientOptions) => Awaitable; +} diff --git a/packages/framework/src/Plugins/PluginManager.ts b/packages/framework/src/Plugins/PluginManager.ts new file mode 100644 index 00000000..2fd86d0b --- /dev/null +++ b/packages/framework/src/Plugins/PluginManager.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import type { Awaitable } from "@sapphire/utilities"; +import type { ClientOptions, FrameworkClient } from "../Lib/FrameworkClient.js"; +import { PluginHook } from "./Hook.js"; +import type { Plugin } from "./Plugin.js"; +import { preGenericsInitialization, preInitialization, postInitialization, preLogin, postLogin } from "./Symbols.js"; + +export type AsyncPluginHooks = PluginHook.PostLogin | PluginHook.PreLogin; +export type FrameworkPluginAsyncHook = (this: FrameworkClient, options: ClientOptions) => Awaitable; + +export type SyncPluginHooks = Exclude; +export type FrameworkPluginHook = (this: FrameworkClient, options: ClientOptions) => unknown; + +export type FrameworkPluginHookEntry = { + hook: T; + type: PluginHook; + name?: string; +}; + +export class PluginManager { + public readonly registry = new Set(); + + public registerHook(hook: FrameworkPluginHook, type: SyncPluginHooks, name?: string): this; + public registerHook(hook: FrameworkPluginAsyncHook, type: AsyncPluginHooks, name?: string): this; + public registerHook(hook: FrameworkPluginAsyncHook | FrameworkPluginHook, type: PluginHook, name?: string): this { + if (typeof hook !== "function") throw new TypeError(`The provided hook ${name === undefined ? "" : `(${name}) `}is not a function`); + this.registry.add({ hook, type, name }); + return this; + } + + public registerPreGenericsInitializationHook(hook: FrameworkPluginHook, name?: string): this { + return this.registerHook(hook, PluginHook.PreGenericsInitialization, name); + } + + public registerPreInitializationHook(hook: FrameworkPluginHook, name?: string): this { + return this.registerHook(hook, PluginHook.PreInitialization, name); + } + + public registerPostInitializationHook(hook: FrameworkPluginHook, name?: string): this { + return this.registerHook(hook, PluginHook.PostInitialization, name); + } + + public registerPreLoginHook(hook: FrameworkPluginAsyncHook, name?: string): this { + return this.registerHook(hook, PluginHook.PreLogin, name); + } + + public registerPostLoginHook(hook: FrameworkPluginAsyncHook, name?: string): this { + return this.registerHook(hook, PluginHook.PostLogin, name); + } + + public use(plugin: typeof Plugin): this { + const possibleSymbolHooks: [symbol, PluginHook][] = [ + [preGenericsInitialization, PluginHook.PreGenericsInitialization], + [preInitialization, PluginHook.PreInitialization], + [postInitialization, PluginHook.PostInitialization], + [preLogin, PluginHook.PreLogin], + [postLogin, PluginHook.PostLogin] + ]; + for (const [hookSymbol, hookType] of possibleSymbolHooks) { + const hook = Reflect.get(plugin, hookSymbol) as FrameworkPluginAsyncHook | FrameworkPluginHook; + if (typeof hook !== "function") continue; + this.registerHook(hook, hookType as any); + } + return this; + } + + public values(): Generator; + public values(hook: SyncPluginHooks): Generator, void>; + public values(hook: AsyncPluginHooks): Generator, void>; + public * values(hook?: PluginHook): Generator { + for (const plugin of this.registry) { + if ((hook !== undefined) && plugin.type !== hook) continue; + yield plugin; + } + } +} diff --git a/packages/framework/src/Plugins/Symbols.ts b/packages/framework/src/Plugins/Symbols.ts new file mode 100644 index 00000000..03491988 --- /dev/null +++ b/packages/framework/src/Plugins/Symbols.ts @@ -0,0 +1,9 @@ +export const preGenericsInitialization: unique symbol = Symbol("FrameworkPluginsPreGenericsInitialization"); +export const preInitialization: unique symbol = Symbol("FrameworkPluginsPreInitialization"); +export const postInitialization: unique symbol = Symbol("FrameworkPluginsPostInitialization"); + +export const preLogin: unique symbol = Symbol("FrameworkPluginsPreLogin"); +export const postLogin: unique symbol = Symbol("FrameworkPluginsPostLogin"); + +export const preSetupAmqp: unique symbol = Symbol("FrameworkPluginsPreSetupAmqp"); +export const postSetupAmqp: unique symbol = Symbol("FrameworkPluginsPostSetupAmqp"); diff --git a/packages/framework/src/Preconditions/ClientTextPermissions.ts b/packages/framework/src/Preconditions/ClientTextPermissions.ts new file mode 100644 index 00000000..ed8f3e55 --- /dev/null +++ b/packages/framework/src/Preconditions/ClientTextPermissions.ts @@ -0,0 +1,49 @@ +/* eslint-disable stylistic/max-len */ +import { inlineCode } from "@discordjs/builders"; +import type { BaseInteraction, CommandInteraction, Message, PermissionsBitField, User } from "@nezuchan/core"; +import type { Result } from "@sapphire/result"; +import type { CommandContext } from "../Lib/CommandContext.js"; +import type { Command } from "../Stores/Command.js"; +import type { InteractionHandler } from "../Stores/InteractionHandler.js"; +import { Precondition } from "../Stores/Precondition.js"; +import type { UserError } from "../Utilities/Errors/UserError.js"; + +export class ClientTextPermissions extends Precondition { + public async contextRun(ctx: CommandContext, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + const guildId = ctx.isMessage() ? ctx.message.guildId! : ctx.interaction.guildId; + const user = ctx.isMessage() ? ctx.message.author : await ctx.interaction.member?.resolveUser({ cache: true }); + const client = await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }); + const channelId = ctx.isMessage() ? ctx.message.channelId : ctx.interaction.channelId; + return this.parseConditions(guildId, channelId, user, client, context); + } + + public async interactionHandlerRun(interaction: BaseInteraction, handler: InteractionHandler, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(interaction.guildId, interaction.channelId, await interaction.member?.resolveUser({ force: true }), await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }), context); + } + + public async chatInputRun(interaction: CommandInteraction, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(interaction.guildId, interaction.channelId, await interaction.member?.resolveUser({ force: true }), await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }), context); + } + + public async messageRun(message: Message, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(message.guildId, message.channelId, message.author, await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }), context); + } + + public async parseConditions(guildId: string | null | undefined, channelId: string | null, user: User | null | undefined, client: User | null | undefined, context: { permissions: PermissionsBitField; }): Promise> { + if (guildId !== null && guildId !== undefined && client && channelId !== null) { + const channel = await this.container.client.resolveChannel({ id: channelId, guildId, cache: true }); + const member = await this.container.client.resolveMember({ id: client.id, guildId, cache: true }); + if (channel && member) { + const permissions = await channel.permissionsForMember(member); + const missing = permissions.missing(context.permissions); + if (missing.length > 0) { + return this.error({ message: `I dont have permissions: ${missing.map(x => inlineCode(String(x))).join(", ")}` }); + } + + return this.ok(); + } + } + + return this.error({ message: `I dont have permissions: ${context.permissions.toArray().map(x => inlineCode(String(x))).join(", ")}` }); + } +} diff --git a/packages/framework/src/Preconditions/ClientVoicePermissions.ts b/packages/framework/src/Preconditions/ClientVoicePermissions.ts new file mode 100644 index 00000000..dff5167b --- /dev/null +++ b/packages/framework/src/Preconditions/ClientVoicePermissions.ts @@ -0,0 +1,53 @@ +/* eslint-disable stylistic/max-len */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { inlineCode } from "@discordjs/builders"; +import type { BaseInteraction, CommandInteraction, Message, PermissionsBitField } from "@nezuchan/core"; +import type { Result } from "@sapphire/result"; +import type { CommandContext } from "../Lib/CommandContext.js"; +import type { Command } from "../Stores/Command.js"; +import type { InteractionHandler } from "../Stores/InteractionHandler.js"; +import { Precondition } from "../Stores/Precondition.js"; +import type { UserError } from "../Utilities/Errors/UserError.js"; + +export class ClientVoicePermissions extends Precondition { + public async contextRun(ctx: CommandContext, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + const guildId = ctx.isMessage() ? ctx.message.guildId! : ctx.interaction.guildId; + const user = ctx.isMessage() ? ctx.message.author : await ctx.interaction.member?.resolveUser({ cache: true }); + const client = await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }); + const channelId = ctx.isMessage() ? ctx.message.channelId : ctx.interaction.channelId; + return this.parseConditions(guildId, channelId, user, client, context); + } + + public async interactionHandlerRun(interaction: BaseInteraction, handler: InteractionHandler, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(interaction.guildId, interaction.channelId, await interaction.member?.resolveUser({ cache: true }), await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }), context); + } + + public async chatInputRun(interaction: CommandInteraction, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(interaction.guildId, interaction.channelId, await interaction.member?.resolveUser({ cache: true }), await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }), context); + } + + public async messageRun(message: Message, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(message.guildId, message.channelId, message.author, await this.container.client.resolveUser({ cache: true, id: this.container.client.clientId }), context); + } + + public async parseConditions(guildId: string | null | undefined, channelId: string | null, user: { id: string; } | null | undefined, client: { id: string; } | null | undefined, context: { permissions: PermissionsBitField; }): Promise> { + if (guildId && user?.id) { + const voiceState = await this.container.client.resolveVoiceState({ guildId, id: user.id }); + if (client && voiceState?.channelId) { + const channel = await this.container.client.resolveChannel({ id: voiceState.channelId, guildId, cache: true }); + const member = await this.container.client.resolveMember({ id: client.id, guildId, cache: true }); + if (channel && member) { + const permissions = await channel.permissionsForMember(member); + const missing = permissions.missing(context.permissions); + if (missing.length > 0) { + return this.error({ message: `I dont have permissions: ${missing.map(x => inlineCode(String(x))).join(", ")}` }); + } + + return this.ok(); + } + } + } + + return this.error({ message: `I dont have permissions: ${context.permissions.toArray().map(x => inlineCode(String(x))).join(", ")}` }); + } +} diff --git a/packages/framework/src/Preconditions/UserPermissions.ts b/packages/framework/src/Preconditions/UserPermissions.ts new file mode 100644 index 00000000..7bde5008 --- /dev/null +++ b/packages/framework/src/Preconditions/UserPermissions.ts @@ -0,0 +1,48 @@ +/* eslint-disable stylistic/max-len */ +import { inlineCode } from "@discordjs/builders"; +import type { BaseInteraction, Message, PermissionsBitField } from "@nezuchan/core"; +import type { Result } from "@sapphire/result"; +import type { CommandContext } from "../Lib/CommandContext.js"; +import type { Command } from "../Stores/Command.js"; +import type { InteractionHandler } from "../Stores/InteractionHandler.js"; +import { Precondition } from "../Stores/Precondition.js"; +import type { UserError } from "../Utilities/Errors/UserError.js"; + +export class UserPermissions extends Precondition { + public async contextRun(ctx: CommandContext, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + const guildId = ctx.isMessage() ? ctx.message.guildId : ctx.interaction.guildId; + const user = ctx.isMessage() ? ctx.message.author : await ctx.interaction.member?.resolveUser({ cache: true }); + const channelId = ctx.isMessage() ? ctx.message.channelId : ctx.interaction.channelId; + return this.parseConditions(guildId, channelId, user, context); + } + + public async interactionHandlerRun(interaction: BaseInteraction, handler: InteractionHandler, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(interaction.guildId, interaction.channelId, await interaction.member?.resolveUser({ cache: true }), context); + } + + public async chatInputRun(interaction: BaseInteraction, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(interaction.guildId, interaction.channelId, await interaction.member?.resolveUser({ cache: true }), context); + } + + public async messageRun(message: Message, command: Command, context: { permissions: PermissionsBitField; }): Promise> { + return this.parseConditions(message.guildId, message.channelId, message.author, context); + } + + public async parseConditions(guildId: string | null | undefined, channelId: string | null, user: { id: string; } | null | undefined, context: { permissions: PermissionsBitField; }): Promise> { + if (guildId && user && channelId) { + const channel = await this.container.client.resolveChannel({ id: channelId, guildId }); + const member = await this.container.client.resolveMember({ id: user.id, guildId }); + if (channel && member) { + const permissions = await channel.permissionsForMember(member); + const missing = permissions.missing(context.permissions); + if (missing.length > 0) { + return this.error({ message: `You dont have permissions: ${missing.map(x => inlineCode(String(x))).join(", ")}` }); + } + + return this.ok(); + } + } + + return this.error({ message: `You dont have permissions: ${context.permissions.toArray().map(x => inlineCode(String(x))).join(", ")}` }); + } +} diff --git a/packages/framework/src/Stores/Command.ts b/packages/framework/src/Stores/Command.ts new file mode 100644 index 00000000..33129601 --- /dev/null +++ b/packages/framework/src/Stores/Command.ts @@ -0,0 +1,124 @@ +/* eslint-disable tsdoc/syntax */ +import type { AutoCompleteInteraction, BaseContextMenuInteraction, CommandInteraction, Message } from "@nezuchan/core"; +import { PermissionsBitField } from "@nezuchan/core"; +import type { IUnorderedStrategy } from "@sapphire/lexure"; +import { Lexer, PrefixedStrategy } from "@sapphire/lexure"; +import type { AliasPieceOptions, LoaderPieceContext } from "@sapphire/pieces"; +import { AliasPiece } from "@sapphire/pieces"; +import type { Awaitable } from "@sapphire/utilities"; +import type { RESTPostAPIApplicationCommandsJSONBody } from "discord-api-types/v10"; +import { PermissionFlagsBits } from "discord-api-types/v10"; +import type { CommandContext } from "../Lib/CommandContext.js"; +import type { FlagStrategyOptions } from "../Lib/FlagUnorderedStrategy.js"; +import type { PreconditionEntryResolvable } from "../Lib/Preconditions/PreconditionContainerArray.js"; +import { PreconditionContainerArray } from "../Lib/Preconditions/PreconditionContainerArray.js"; + +export class Command extends AliasPiece { + public lexer: Lexer; + public fullCategory = this.location.directories; + public strategy: IUnorderedStrategy; + public preconditions: PreconditionContainerArray; + public meta: CommandMeta; + + public get category(): string | null { + return this.fullCategory.length > 0 ? this.fullCategory[0] : null; + } + + public get subCategory(): string | null { + return this.fullCategory.length > 1 ? this.fullCategory[1] : null; + } + + public get parentCategory(): string | null { + return this.fullCategory.length > 1 ? this.fullCategory.at(-1)! : null; + } + + public constructor(context: LoaderPieceContext, options: CommandOptions) { + super(context, options); + + this.lexer = new Lexer({ + quotes: options.quotes ?? [ + ['"', '"'], // Double quotes + ["“", "”"], // Fancy quotes (on iOS) + ["「", "」"], // Corner brackets (CJK) + ["«", "»"] // French quotes (guillemets) + ] + }); + + this.meta = options.meta ?? {}; + this.strategy = options.strategy ?? new PrefixedStrategy(["--", "/"], ["=", ":"]); + this.preconditions = new PreconditionContainerArray(options.preconditions); + + const clientTextPermissions = new PermissionsBitField(PermissionFlagsBits, options.clientPermissions?.text ?? 0n); + + if (clientTextPermissions.bits !== 0n) { + this.preconditions.append({ + name: "ClientTextPermissions", + context: { + permissions: clientTextPermissions + } + }); + } + + const clientVoicePermissions = new PermissionsBitField(PermissionFlagsBits, options.clientPermissions?.voice ?? 0n); + + if (clientVoicePermissions.bits !== 0n) { + this.preconditions.append({ + name: "ClientVoicePermissions", + context: { + permissions: clientVoicePermissions + } + }); + } + + const UserPermissions = new PermissionsBitField(PermissionFlagsBits, options.userPermissions ?? 0n); + + if (UserPermissions.bits !== 0n) { + this.preconditions.append({ + name: "UserPermissions", + context: { + permissions: UserPermissions + } + }); + } + } + + public chatInputRun?(interaction: CommandInteraction): Awaitable; + public contextMenuRun?(interaction: BaseContextMenuInteraction): Awaitable; + public messageRun?(message: Message): Awaitable; + public autoCompleteRun?(interaction: AutoCompleteInteraction): Awaitable; + + public contextRun?(ctx: CommandContext): Awaitable; +} + +export type CommandOptions = AliasPieceOptions & FlagStrategyOptions & { + quotes?: [string, string][]; + strategy?: IUnorderedStrategy; + preconditions?: PreconditionEntryResolvable[]; + chatInput?: RESTPostAPIApplicationCommandsJSONBody; + contextMenu?: RESTPostAPIApplicationCommandsJSONBody; + meta?: CommandMeta; + clientPermissions?: { + voice?: bigint[]; + text?: bigint[]; + }; + userPermissions?: bigint[]; + + /** + * @description If chat input command is enabled on command context. + */ + enableChatInputCommand?: boolean; + + /** + * @description If context menu command is enabled on command context. + */ + enableContextMenuCommand?: boolean; + + /** + * @description If message command is enabled on command context. + */ + enableMessageCommand?: boolean; +}; + +export type CommandMeta = { + description?: string; +}; diff --git a/packages/framework/src/Stores/CommandStore.ts b/packages/framework/src/Stores/CommandStore.ts new file mode 100644 index 00000000..95148f36 --- /dev/null +++ b/packages/framework/src/Stores/CommandStore.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { AliasStore } from "@sapphire/pieces"; +import type { RESTPostAPIApplicationCommandsJSONBody } from "discord-api-types/v10"; +import { Routes } from "discord-api-types/v10"; +import { Events } from "../Utilities/EventEnums.js"; +import { Command } from "./Command.js"; + +export class CommandStore extends AliasStore { + public constructor() { + super(Command, { name: "commands" }); + } + + public async postCommands(): Promise { + const commands = [...this.values()]; + const registerAbleCommands: RESTPostAPIApplicationCommandsJSONBody[] = []; + + for (const command of commands) { + if (command.options.chatInput) { + this.container.client.emit(Events.RegisteringCommand, command); + registerAbleCommands.push(command.options.chatInput); + } + + if (command.options.contextMenu) { + this.container.client.emit(Events.RegisteringCommand, command); + registerAbleCommands.push(command.options.contextMenu); + } + } + + if (registerAbleCommands.length > 0) { + await this.container.client.rest.put(Routes.applicationCommands(this.container.client.clientId), { + body: registerAbleCommands + }); + this.container.client.emit(Events.CommandRegistered, registerAbleCommands); + } + } +} diff --git a/packages/framework/src/Stores/InteractionHandler.ts b/packages/framework/src/Stores/InteractionHandler.ts new file mode 100644 index 00000000..710279fa --- /dev/null +++ b/packages/framework/src/Stores/InteractionHandler.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable unicorn/no-array-callback-reference */ +import type { BaseInteraction } from "@nezuchan/core"; +import { PermissionsBitField } from "@nezuchan/core"; +import { Piece } from "@sapphire/pieces"; +import { Option } from "@sapphire/result"; +import type { Awaitable } from "@sapphire/utilities"; +import { PermissionFlagsBits } from "discord-api-types/v10"; +import type { PreconditionEntryResolvable } from "../Lib/Preconditions/PreconditionContainerArray.js"; +import { PreconditionContainerArray } from "../Lib/Preconditions/PreconditionContainerArray.js"; + +export abstract class InteractionHandler extends Piece { + public preconditions: PreconditionContainerArray; + public constructor(context: Piece.Context, options: InteractionHandlerOptions) { + super(context, options); + this.preconditions = new PreconditionContainerArray(options.preconditions); + + const clientTextPermissions = new PermissionsBitField(PermissionFlagsBits, options.clientPermissions?.text ?? 0n); + + if (clientTextPermissions.bits !== 0n) { + this.preconditions.append({ + name: "ClientTextPermissions", + context: { + permissions: clientTextPermissions + } + }); + } + + const clientVoicePermissions = new PermissionsBitField(PermissionFlagsBits, options.clientPermissions?.voice ?? 0n); + + if (clientVoicePermissions.bits !== 0n) { + this.preconditions.append({ + name: "ClientVoicePermissions", + context: { + permissions: clientVoicePermissions + } + }); + } + + const userPermissions = new PermissionsBitField(PermissionFlagsBits, options.userPermissions ?? 0n); + + if (userPermissions.bits !== 0n) { + this.preconditions.append({ + name: "UserPermissions", + context: { + permissions: userPermissions + } + }); + } + } + + public parse(interaction: BaseInteraction): Awaitable> { + return this.some(); + } + + public some(): Option.Some; + public some(data: T): Option.Some; + public some(data?: T): Option.Some { + return Option.some(data); + } + + public none(): Option.None { + return Option.none; + } + + public abstract run(interaction: BaseInteraction, parsedData?: unknown): unknown; +} + +export type InteractionHandlerOptions = Piece.Options & { + preconditions?: PreconditionEntryResolvable[]; + clientPermissions?: { + voice?: bigint[]; + text?: bigint[]; + }; + userPermissions?: bigint[]; + readonly interactionHandlerType: InteractionHandlerTypes; +}; + +export enum InteractionHandlerTypes { + Button = 0, + SelectMenu = 1, + ModalSubmit = 2, + MessageComponent = 3, + Autocomplete = 4 +} diff --git a/packages/framework/src/Stores/InteractionHandlerStore.ts b/packages/framework/src/Stores/InteractionHandlerStore.ts new file mode 100644 index 00000000..e80e7289 --- /dev/null +++ b/packages/framework/src/Stores/InteractionHandlerStore.ts @@ -0,0 +1,76 @@ +/* eslint-disable promise/prefer-await-to-then */ +import type { BaseInteraction } from "@nezuchan/core"; +import { Store } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import { Events } from "../Utilities/EventEnums.js"; +import { InteractionHandler, InteractionHandlerTypes } from "./InteractionHandler.js"; + +export const InteractionHandlerFilters = new Map boolean>([ + [InteractionHandlerTypes.Button, interaction => interaction.isButton()], + [InteractionHandlerTypes.SelectMenu, interaction => interaction.isSelectMenu()], + [InteractionHandlerTypes.ModalSubmit, interaction => interaction.isModalSubmit()], + + [InteractionHandlerTypes.MessageComponent, interaction => interaction.isComponentInteraction()], + [InteractionHandlerTypes.Autocomplete, Interaction => Interaction.isAutoCompleteInteraction()] +]); + +export class InteractionHandlerStore extends Store { + public constructor() { + super(InteractionHandler, { name: "interaction-handlers" }); + } + + public async run(interaction: BaseInteraction): Promise { + if (this.size === 0) return false; + + const promises: Promise>[] = []; + + for (const handler of this.values()) { + const filter = InteractionHandlerFilters.get(handler.options.interactionHandlerType); + + if (!filter?.(interaction)) continue; + + const result = await Result.fromAsync(() => handler.parse(interaction)); + + result.match({ + ok: option => { + option.inspect(value => { + const promise = Result.fromAsync(async () => { + const globalResult = await this.container.stores.get("preconditions").interactionHandlerRun(interaction, handler); + if (globalResult.isErr()) { + this.container.client.emit(Events.InteractionHandlerDenied, globalResult.unwrapErr(), { interaction, handler }); + return; + } + + const localResult = await handler.preconditions.interactionHandlerRun(interaction, handler); + if (localResult.isErr()) { + this.container.client.emit(Events.InteractionHandlerDenied, localResult.unwrapErr(), { interaction, handler }); + return; + } + handler.run(interaction, value); + }) + .then(res => res.mapErr(error => ({ handler, error }))); + + promises.push(promise); + }); + }, + err: error => { + this.container.client.emit(Events.InteractionHandlerParseError, error, { interaction, handler }); + } + }); + } + + if (promises.length === 0) return false; + + const results = await Promise.allSettled(promises); + + for (const result of results) { + const res = ( + result as PromiseFulfilledResult> + ).value; + + res.inspectErr(value => this.container.client.emit(Events.InteractionHandlerError, value.error, { interaction, handler: value.handler })); + } + + return true; + } +} diff --git a/packages/framework/src/Stores/Listener.ts b/packages/framework/src/Stores/Listener.ts new file mode 100644 index 00000000..f499657c --- /dev/null +++ b/packages/framework/src/Stores/Listener.ts @@ -0,0 +1,76 @@ +/* eslint-disable unicorn/no-nested-ternary */ +import type { EventEmitter } from "node:events"; +import type { PieceContext, PieceOptions } from "@sapphire/pieces"; +import { Piece } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import { ListenerEvents } from "../Utilities/EventEnums.js"; + +export abstract class Listener extends Piece { + public emitter: EventEmitter | null; + public event: string | symbol; + public once: boolean; + private _listener: ((...args: any[]) => void) | null; + + public constructor(context: PieceContext, options: ListenerOptions = {}) { + super(context, options); + + this.emitter = + options.emitter === undefined + ? this.container.client + : (typeof options.emitter === "string" ? (Reflect.get(this.container.client, options.emitter) as EventEmitter) : options.emitter) ?? + null; + + this.event = options.event ?? this.name; + this.once = options.once ?? false; + + this._listener = this.emitter && this.event ? this.once ? this._runOnce.bind(this) : this._run.bind(this) : null; + + if (this.emitter === null || this._listener === null) this.enabled = false; + } + + public override onLoad(): unknown { + if (this._listener) { + const emitter = this.emitter!; + + const maxListeners = emitter.getMaxListeners(); + if (maxListeners !== 0) emitter.setMaxListeners(maxListeners + 1); + + emitter[this.once ? "once" : "on"](this.event, this._listener); + } + return super.onLoad(); + } + + public override onUnload(): unknown { + if (!this.once && this._listener) { + const emitter = this.emitter!; + + const maxListeners = emitter.getMaxListeners(); + if (maxListeners !== 0) emitter.setMaxListeners(maxListeners - 1); + + emitter.off(this.event, this._listener); + this._listener = null; + } + + return super.onUnload(); + } + + private async _run(...args: unknown[]): Promise { + const result = await Result.fromAsync(() => this.run(...args)); + if (result.isErr()) { + this.container.client.emit(ListenerEvents.ListenerError, { listener: this, error: result.err().unwrap() }); + } + } + + private async _runOnce(...args: unknown[]): Promise { + await this._run(...args); + await this.unload(); + } + + public abstract run(...args: unknown[]): unknown; +} + +export type ListenerOptions = PieceOptions & { + emitter?: EventEmitter | string; + event?: string | symbol; + once?: boolean; +}; diff --git a/packages/framework/src/Stores/ListenerStore.ts b/packages/framework/src/Stores/ListenerStore.ts new file mode 100644 index 00000000..c53bf73b --- /dev/null +++ b/packages/framework/src/Stores/ListenerStore.ts @@ -0,0 +1,8 @@ +import { Store } from "@sapphire/pieces"; +import { Listener } from "./Listener.js"; + +export class ListenerStore extends Store { + public constructor() { + super(Listener, { name: "listeners" }); + } +} diff --git a/packages/framework/src/Stores/Precondition.ts b/packages/framework/src/Stores/Precondition.ts new file mode 100644 index 00000000..215bffab --- /dev/null +++ b/packages/framework/src/Stores/Precondition.ts @@ -0,0 +1,60 @@ +import type { BaseContextMenuInteraction, BaseInteraction, CommandInteraction, Message, PermissionsBitField } from "@nezuchan/core"; +import type { LoaderPieceContext, PieceOptions } from "@sapphire/pieces"; +import { Piece } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import type { Awaitable } from "@sapphire/utilities"; +import type { CommandContext } from "../Lib/CommandContext.js"; +import { PreconditionError } from "../Utilities/Errors/PreconditionError.js"; +import type { UserError } from "../Utilities/Errors/UserError.js"; +import type { Command } from "./Command.js"; +import type { InteractionHandler } from "./InteractionHandler.js"; + +export class Precondition extends Piece { + public readonly position: number | null; + + public constructor(context: LoaderPieceContext, options: PreconditionOptions) { + super(context, options); + this.position = options.position ?? null; + } + + public chatInputRun?(interaction: CommandInteraction, command: Command, context?: PreconditionContext): Awaitable>; + public contextMenuRun?(interaction: BaseContextMenuInteraction, command: Command, context?: PreconditionContext): Awaitable>; + public messageRun?(message: Message, command: Command, context?: PreconditionContext): Awaitable>; + + public contextRun?(ctx: CommandContext, command: Command, context?: PreconditionContext): Awaitable>; + public interactionHandlerRun?(interaction: BaseInteraction, handler: InteractionHandler, context?: PreconditionContext): Awaitable>; + + public error(options: Omit = {}): Awaitable> { + return Result.err(new PreconditionError({ precondition: this, ...options })); + } + + public ok(): Awaitable> { + return Result.ok(); + } +} + +export type PreconditionOptions = PieceOptions & { + position?: number; +}; + +export type Preconditions = { + Enabled: never; + ClientVoicePermissions: { + permissions: PermissionsBitField; + }; + ClientTextPermissions: { + permissions: PermissionsBitField; + }; + UserPermissions: { + permissions: PermissionsBitField; + }; +}; + +export type PreconditionContext = Record & { + external?: boolean; +}; + +export type PreconditionKeys = keyof Preconditions; +export type SimplePreconditionKeys = { + [K in PreconditionKeys]: Preconditions[K] extends never ? K : never; +}[PreconditionKeys]; diff --git a/packages/framework/src/Stores/PreconditionStore.ts b/packages/framework/src/Stores/PreconditionStore.ts new file mode 100644 index 00000000..ae20858c --- /dev/null +++ b/packages/framework/src/Stores/PreconditionStore.ts @@ -0,0 +1,143 @@ +import type { BaseContextMenuInteraction, BaseInteraction, CommandInteraction, Message } from "@nezuchan/core"; +import { Store } from "@sapphire/pieces"; +import { Result } from "@sapphire/result"; +import type { CommandContext } from "../Lib/CommandContext.js"; +import { Identifiers } from "../Utilities/Errors/Identifiers.js"; +import type { UserError } from "../Utilities/Errors/UserError.js"; +import type { Command } from "./Command.js"; +import type { InteractionHandler } from "./InteractionHandler.js"; +import { Precondition } from "./Precondition.js"; +import type { PreconditionContext } from "./Precondition.js"; + +export class PreconditionStore extends Store { + private readonly globalPreconditions: Precondition[] = []; + + public constructor() { + super(Precondition, { name: "preconditions" }); + } + + public async messageRun(message: Message, command: Command, context: PreconditionContext = {}): Promise> { + for (const precondition of this.globalPreconditions) { + const result = precondition.messageRun + ? await precondition.messageRun(message, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingMessageHandler, + message: `The precondition "${precondition.name}" is missing a "messageRun" handler, but it was requested for the "${command.name}" command.` + }); + + if (result.isErr()) { + return result; + } + } + + return Result.ok(); + } + + public async chatInputRun( + interaction: CommandInteraction, + command: Command, + context: PreconditionContext = {} + ): Promise> { + for (const precondition of this.globalPreconditions) { + const result = precondition.chatInputRun + ? await precondition.chatInputRun(interaction, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingChatInputHandler, + message: `The precondition "${precondition.name}" is missing a "chatInputRun" handler, but it was requested for the "${command.name}" command.` + }); + + if (result.isErr()) { + return result; + } + } + + return Result.ok(); + } + + public async contextMenuRun( + interaction: BaseContextMenuInteraction, + command: Command, + context: PreconditionContext = {} + ): Promise> { + for (const precondition of this.globalPreconditions) { + const result = precondition.contextMenuRun + ? await precondition.contextMenuRun(interaction, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingContextMenuHandler, + message: `The precondition "${precondition.name}" is missing a "contextMenuRun" handler, but it was requested for the "${command.name}" command.` + }); + + if (result.isErr()) { + return result; + } + } + + return Result.ok(); + } + + public async contextRun( + ctx: CommandContext, + command: Command, + context: PreconditionContext = {} + ): Promise> { + for (const precondition of this.globalPreconditions) { + const result = precondition.contextRun + ? await precondition.contextRun(ctx, command, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingContextMenuHandler, + message: `The precondition "${precondition.name}" is missing a "contextRun" handler, but it was requested for the "${command.name}" command.` + }); + + if (result.isErr()) { + return result; + } + } + + return Result.ok(); + } + + public async interactionHandlerRun( + interaction: BaseInteraction, + handler: InteractionHandler, + context: PreconditionContext = {} + ): Promise> { + for (const precondition of this.globalPreconditions) { + const result = precondition.interactionHandlerRun + ? await precondition.interactionHandlerRun(interaction, handler, context) + : await precondition.error({ + identifier: Identifiers.PreconditionMissingInteractionHandler, + message: `The precondition "${precondition.name}" is missing a "interactionHandlerRun" handler, but it was requested for the "${handler.name}" handler.` + }); + + if (result.isErr()) { + return result; + } + } + + return Result.ok(); + } + + public override set(key: string, value: Precondition): this { + if (value.position !== null) { + const index = this.globalPreconditions.findIndex(precondition => precondition.position! >= value.position!); + + if (index === -1) this.globalPreconditions.push(value); + else this.globalPreconditions.splice(index, 0, value); + } + + return super.set(key, value); + } + + public override delete(key: string): boolean { + const index = this.globalPreconditions.findIndex(precondition => precondition.name === key); + + if (index !== -1) this.globalPreconditions.splice(index, 1); + + return super.delete(key); + } + + public override clear(): void { + this.globalPreconditions.length = 0; + super.clear(); + } +} diff --git a/packages/framework/src/Utilities/Errors/Identifiers.ts b/packages/framework/src/Utilities/Errors/Identifiers.ts new file mode 100644 index 00000000..bd050ce1 --- /dev/null +++ b/packages/framework/src/Utilities/Errors/Identifiers.ts @@ -0,0 +1,9 @@ +export enum Identifiers { + PreconditionUnavailable = "preconditionUnavailable", + + PreconditionMissingMessageHandler = "preconditionMissingMessageHandler", + PreconditionMissingChatInputHandler = "preconditionMissingChatInputHandler", + PreconditionMissingContextMenuHandler = "preconditionMissingContextMenuHandler", + PreconditionMissingContextHandler = "preconditionMissingContextHandler", + PreconditionMissingInteractionHandler = "preconditionMissingInteractionHandler" +} diff --git a/packages/framework/src/Utilities/Errors/PreconditionError.ts b/packages/framework/src/Utilities/Errors/PreconditionError.ts new file mode 100644 index 00000000..04ce2f78 --- /dev/null +++ b/packages/framework/src/Utilities/Errors/PreconditionError.ts @@ -0,0 +1,29 @@ +/* eslint-disable tsdoc/syntax */ +/* eslint-disable @typescript-eslint/no-namespace */ +import type { Precondition } from "../../Stores/Precondition.js"; +import { UserError } from "./UserError.js"; + +/** + * Errors thrown by preconditions + * + * @property name This will be `'PreconditionError'` and can be used to distinguish the type of error when any error gets thrown + */ +export class PreconditionError extends UserError { + public readonly precondition: Precondition; + + public constructor(options: PreconditionError.Options) { + super({ ...options, identifier: options.identifier ?? options.precondition.name }); + this.precondition = options.precondition; + } + + public override get name(): string { + return "PreconditionError"; + } +} + +export namespace PreconditionError { + export type Options = Omit & { + precondition: Precondition; + identifier?: string; + }; +} diff --git a/packages/framework/src/Utilities/Errors/UserError.ts b/packages/framework/src/Utilities/Errors/UserError.ts new file mode 100644 index 00000000..d1511f81 --- /dev/null +++ b/packages/framework/src/Utilities/Errors/UserError.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +export class UserError extends Error { + public readonly identifier: string; + public readonly context: unknown; + public constructor(options: UserError.Options) { + super(options.message); + this.identifier = options.identifier; + this.context = options.context ?? null; + } + + public override get name(): string { + return "UserError"; + } +} + +export namespace UserError { + export type Options = { + identifier: string; + message?: string; + context?: unknown; + }; +} diff --git a/packages/framework/src/Utilities/EventEnums.ts b/packages/framework/src/Utilities/EventEnums.ts new file mode 100644 index 00000000..6adefd93 --- /dev/null +++ b/packages/framework/src/Utilities/EventEnums.ts @@ -0,0 +1,54 @@ +export enum ListenerEvents { + ListenerError = "listenerError" +} + +export enum Events { + InteractionCreate = "interactionCreate", + MessageCreate = "messageCreate", + + PreMessageParsed = "preMessageParsed", + MentionPrefixOnly = "mentionPrefixOnly", + NonPrefixedMessage = "nonPrefixedMessage", + PrefixedMessage = "prefixedMessage", + UnknownMessageCommandName = "unknownMessageCommandName", + CommandDoesNotHaveMessageCommandHandler = "commandDoesNotHaveMessageCommandHandler", + UnknownMessageCommand = "unknownMessageCommand", + PreMessageCommandRun = "preMessageCommandRun", + MessageCommandDisabled = "messageCommandDisabled", + MessageCommandDenied = "messageCommandDenied", + MessageCommandAccepted = "messageCommandAccepted", + MessageCommandError = "messageCommandError", + + PossibleChatInputCommand = "possibleChatInputCommand", + PossibleContextMenuCommand = "possibleContextMenuCommand", + PossibleAutocompleteInteraction = "possibleAutoCompleteInteraction", + + PreChatInputCommandRun = "preChatInputCommandRun", + ChatInputCommandAccepted = "chatInputCommandAccepted", + ChatInputCommandError = "chatInputCommandError", + ChatInputCommandDenied = "chatInputCommandDenied", + ChatInputCommandDisabled = "chatInputCommandDisabled", + + PreContextMenuCommandRun = "preContextMenuCommandRun", + ContextMenuCommandAccepted = "contextMenuCommandAccepted", + ContextMenuCommandError = "contextMenuCommandError", + ContextMenuCommandDenied = "contextMenuCommandDenied", + ContextMenuCommandDisabled = "contextMenuCommandDisabled", + + PreContextCommandRun = "preContextCommandRun", + ContextCommandAccepted = "contextCommandAccepted", + ContextCommandError = "contextCommandError", + ContextCommandDenied = "contextCommandDenied", + + CommandAutocompleteInteractionSuccess = "commandAutocompleteInteractionSuccess", + CommandAutocompleteInteractionError = "commandAutocompleteInteractionError", + + InteractionHandlerError = "interactionHandlerError", + InteractionHandlerParseError = "interactionHandlerParseError", + InteractionHandlerDenied = "interactionHandlerDenied", + + RegisteringCommand = "registeringCommand", + CommandRegistered = "commandRegistered", + + PluginLoaded = "pluginLoaded" +} diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts new file mode 100644 index 00000000..105799e7 --- /dev/null +++ b/packages/framework/src/index.ts @@ -0,0 +1,25 @@ +export * from "./Lib/FrameworkClient.js"; +export * from "./Stores/Listener.js"; +export * from "./Stores/ListenerStore.js"; +export * from "./Utilities/EventEnums.js"; +export * from "./Stores/Command.js"; +export * from "./Stores/CommandStore.js"; +export * from "./Lib/CommandContext.js"; +export * from "./Lib/FlagUnorderedStrategy.js"; +export * from "./Stores/Precondition.js"; +export * from "./Stores/PreconditionStore.js"; +export * from "./Utilities/Errors/Identifiers.js"; +export * from "./Utilities/Errors/PreconditionError.js"; +export * from "./Utilities/Errors/UserError.js"; +export * from "./Lib/Preconditions/PreconditionContainerArray.js"; +export * from "./Lib/Preconditions/PreconditionContainerSingle.js"; +export * from "./Lib/Preconditions/IPreconditionContainer.js"; +export * from "./Lib/Preconditions/Conditions/IPreconditionCondition.js"; +export * from "./Lib/Preconditions/Conditions/PreconditionConditionAnd.js"; +export * from "./Lib/Preconditions/Conditions/PreconditionConditionOr.js"; +export * from "./Stores/InteractionHandler.js"; +export * from "./Stores/InteractionHandlerStore.js"; +export * from "./Plugins/Hook.js"; +export * from "./Plugins/Plugin.js"; +export * from "./Plugins/PluginManager.js"; +export * from "./Plugins/Symbols.js"; diff --git a/packages/framework/tsconfig.json b/packages/framework/tsconfig.json new file mode 100644 index 00000000..d62780ed --- /dev/null +++ b/packages/framework/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/kanao-schema/src/Schema/channel.ts b/packages/kanao-schema/src/Schema/channel.ts index cd2e2c37..8d8c49fa 100644 --- a/packages/kanao-schema/src/Schema/channel.ts +++ b/packages/kanao-schema/src/Schema/channel.ts @@ -20,7 +20,6 @@ export const channels = pgTable("channels", { videoQualityMode: integer("video_quality_mode"), messageCount: integer("message_count"), defaultAutoArchiveDuration: integer("default_auto_archive_duration"), - permissions: text("permissions"), flags: integer("flags"), guildId: text("guild_id").references(() => guilds.id, { onDelete: "cascade" }) diff --git a/packages/kanao-schema/src/Schema/member.ts b/packages/kanao-schema/src/Schema/member.ts index 808bc5f0..e21013ee 100644 --- a/packages/kanao-schema/src/Schema/member.ts +++ b/packages/kanao-schema/src/Schema/member.ts @@ -14,6 +14,5 @@ export const members = pgTable("members", { deaf: boolean("deaf"), mute: boolean("mute"), pending: boolean("pending"), - permissions: integer("permissions"), communicationDisabledUntil: text("communication_disabled_until") }); diff --git a/packages/kanao-schema/src/Schema/roles.ts b/packages/kanao-schema/src/Schema/roles.ts index 7152fb83..8eae7415 100644 --- a/packages/kanao-schema/src/Schema/roles.ts +++ b/packages/kanao-schema/src/Schema/roles.ts @@ -1,3 +1,4 @@ +import { relations } from "drizzle-orm"; import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core"; import { guilds } from "./guild.js"; import { members } from "./member.js"; @@ -13,10 +14,18 @@ export const roles = pgTable("roles", { export const memberRoles = pgTable("member_roles", { id: text("id").primaryKey().references(() => members.id, { onDelete: "cascade" }), - roleId: text("role_id").references(() => roles.id, { onDelete: "cascade" }) + roleId: text("role_id").references(() => roles.id, { onDelete: "cascade" }), + guildId: text("guild_id").references(() => guilds.id, { onDelete: "cascade" }) }); export const guildsRoles = pgTable("guild_roles", { id: text("id").primaryKey().references(() => guilds.id, { onDelete: "cascade" }), roleId: text("role_id").references(() => roles.id, { onDelete: "cascade" }) }); + +export const memberRolesRelations = relations(memberRoles, ({ one }) => ({ + role: one(roles, { + fields: [memberRoles.roleId], + references: [roles.id] + }) +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49e03b9a..937bea15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,102 @@ importers: specifier: ^5.3.3 version: 5.3.3 + packages/core: + dependencies: + '@cordis/bitfield': + specifier: ^1.2.0 + version: 1.2.0 + '@discordjs/rest': + specifier: ^2.2.0 + version: 2.2.0 + '@nezuchan/constants': + specifier: ^0.8.0 + version: 0.8.0 + '@nezuchan/decorators': + specifier: ^0.2.0 + version: 0.2.0 + '@nezuchan/kanao-schema': + specifier: workspace:^ + version: link:../kanao-schema + '@nezuchan/utilities': + specifier: ^0.6.2 + version: 0.6.2(amqplib@0.10.3) + '@sapphire/pieces': + specifier: ^4.2.2 + version: 4.2.2 + '@sapphire/result': + specifier: ^2.6.6 + version: 2.6.6 + '@sapphire/snowflake': + specifier: ^3.5.3 + version: 3.5.3 + '@sapphire/utilities': + specifier: ^3.15.3 + version: 3.15.3 + amqp-connection-manager: + specifier: ^4.1.14 + version: 4.1.14(amqplib@0.10.3) + discord-api-types: + specifier: ^0.37.69 + version: 0.37.69 + drizzle-orm: + specifier: ^0.29.3 + version: 0.29.3(postgres@3.4.3) + postgres: + specifier: ^3.4.3 + version: 3.4.3 + tslib: + specifier: ^2.6.2 + version: 2.6.2 + optionalDependencies: + ioredis: + specifier: ^5.3.2 + version: 5.3.2 + devDependencies: + '@types/amqplib': + specifier: ^0.10.4 + version: 0.10.4 + + packages/framework: + dependencies: + '@discordjs/builders': + specifier: ^1.7.0 + version: 1.7.0 + '@discordjs/collection': + specifier: ^2.0.0 + version: 2.0.0 + '@nezuchan/core': + specifier: workspace:^ + version: link:../core + '@sapphire/lexure': + specifier: ^1.1.7 + version: 1.1.7 + '@sapphire/pieces': + specifier: ^4.2.2 + version: 4.2.2 + '@sapphire/result': + specifier: ^2.6.6 + version: 2.6.6 + '@sapphire/utilities': + specifier: ^3.15.3 + version: 3.15.3 + amqplib: + specifier: ^0.10.3 + version: 0.10.3 + discord-api-types: + specifier: ^0.37.69 + version: 0.37.69 + gen-esm-wrapper: + specifier: ^1.1.3 + version: 1.1.3 + tslib: + specifier: ^2.6.2 + version: 2.6.2 + devDependencies: + '@types/amqplib': + specifier: ^0.10.4 + version: 0.10.4 + packages/kanao-schema: dependencies: drizzle-orm: @@ -200,6 +296,34 @@ packages: resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} dev: false + /@cordis/bitfield@1.2.0: + resolution: {integrity: sha512-ta4arMcYFqu4AE+RH4gEJaX8JEe2CckW8gCQoeJIUGw5KecIaAnywQZ6iFlHtgxsyyas5VsBQU01682xilnWbg==} + deprecated: see https://github.com/cordis-lib/cordis#deprecation + dependencies: + '@cordis/error': 1.2.0 + tslib: 2.6.2 + dev: false + + /@cordis/error@1.2.0: + resolution: {integrity: sha512-f4JAj+ixVfOWacVpe3LktNsOYhZ+oaoBkciJ6wWFLqLk/xHEfxUlBwcNwrZM5ck1mklV+uK1nAmKEhBHf66xRg==} + deprecated: see https://github.com/cordis-lib/cordis#deprecation + dependencies: + tslib: 2.6.2 + dev: false + + /@discordjs/builders@1.7.0: + resolution: {integrity: sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/formatters': 0.3.3 + '@discordjs/util': 1.0.2 + '@sapphire/shapeshift': 3.9.6 + discord-api-types: 0.37.61 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.3 + tslib: 2.6.2 + dev: false + /@discordjs/collection@1.5.3: resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} engines: {node: '>=16.11.0'} @@ -210,6 +334,13 @@ packages: engines: {node: '>=18'} dev: false + /@discordjs/formatters@0.3.3: + resolution: {integrity: sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==} + engines: {node: '>=16.11.0'} + dependencies: + discord-api-types: 0.37.61 + dev: false + /@discordjs/rest@2.2.0: resolution: {integrity: sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==} engines: {node: '>=16.11.0'} @@ -217,7 +348,7 @@ packages: '@discordjs/collection': 2.0.0 '@discordjs/util': 1.0.2 '@sapphire/async-queue': 1.5.1 - '@sapphire/snowflake': 3.5.2 + '@sapphire/snowflake': 3.5.3 '@vladfrangu/async_event_emitter': 2.2.4 discord-api-types: 0.37.61 magic-bytes.js: 1.7.0 @@ -846,10 +977,17 @@ packages: resolution: {integrity: sha512-h0Yc82W/PakS1CLgdDw2xxiCdJrZr0JFxfRlXj7dooG0kAnbEec9ivMHPlgiWtbqqiK4HRol+79iRkBD3ADYxQ==} dev: false + /@nezuchan/decorators@0.2.0: + resolution: {integrity: sha512-AeTk7sqlQNv4qb+HHgzmYtQB65TxeIe1/Tibvi8Tq3n1209x98G5Jh+mLtOQ2gDZ1hfSBeqiFKrwVXJIjtnuIw==} + dependencies: + '@sapphire/pieces': 4.2.2 + '@sapphire/utilities': 3.15.3 + dev: false + /@nezuchan/utilities@0.6.2(amqplib@0.10.3): resolution: {integrity: sha512-1Nsf9SCzVlCvG61SvwJI9KUwJtXqt8/KGdDVHlPeoQuzo66Kfu1Vr1a/aKKFWArJTocqQ7i70e1quYXn2Tog0g==} dependencies: - '@sapphire/snowflake': 3.5.2 + '@sapphire/snowflake': 3.5.3 amqp-connection-manager: 4.1.14(amqplib@0.10.3) transitivePeerDependencies: - amqplib @@ -905,6 +1043,13 @@ packages: engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: false + /@sapphire/lexure@1.1.7: + resolution: {integrity: sha512-6PqU2/V+w1k4DHbZ8erIH+iaT/kAmLfReiWNUURt1akfrPTWqlVYWfuxkHXF0JMPk53r4NIkZoitiWwGUtPF+Q==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dependencies: + '@sapphire/result': 2.6.6 + dev: false + /@sapphire/pieces@4.2.2: resolution: {integrity: sha512-DvAC+zTgm5o41D6iX+jBjMp+rRmoHPKNYnav6v6vQLTxBJb+iFMmup9ZREiuXdrh1ejrmVRZojnnI59xQgezwQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -919,8 +1064,16 @@ packages: engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: false - /@sapphire/snowflake@3.5.2: - resolution: {integrity: sha512-FTm9RdyELF21PQN5dS/HLRs90XqWclHa+p0gkonc+BA2X2QKfFySHSjUbO65rmArd/ghR9Ahj2fMfedTZEqzOw==} + /@sapphire/shapeshift@3.9.6: + resolution: {integrity: sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g==} + engines: {node: '>=v18'} + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + dev: false + + /@sapphire/snowflake@3.5.3: + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: false @@ -1595,6 +1748,13 @@ packages: safer-buffer: 2.1.2 dev: false + /assert@1.5.1: + resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} + dependencies: + object.assign: 4.1.5 + util: 0.10.4 + dev: false + /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -1761,7 +1921,6 @@ packages: function-bind: 1.1.2 get-intrinsic: 1.2.4 set-function-length: 1.2.1 - dev: true /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1995,7 +2154,6 @@ packages: get-intrinsic: 1.2.4 gopd: 1.0.1 has-property-descriptors: 1.0.1 - dev: true /define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} @@ -2004,7 +2162,6 @@ packages: define-data-property: 1.1.2 has-property-descriptors: 1.0.1 object-keys: 1.1.1 - dev: true /denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} @@ -2271,7 +2428,6 @@ packages: /es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - dev: true /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} @@ -2820,7 +2976,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} @@ -2953,7 +3108,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true /function.prototype.name@1.1.6: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} @@ -2969,6 +3123,13 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /gen-esm-wrapper@1.1.3: + resolution: {integrity: sha512-LNHZ+QpaCW/0VhABIbXn45V+P8kFvjjwuue9hbV23eOjuFVz6c0FE3z1XpLX9pSjLW7UmtCkXo5F9vhZWVs8oQ==} + hasBin: true + dependencies: + is-valid-identifier: 2.0.2 + dev: false + /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2978,7 +3139,6 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 hasown: 2.0.0 - dev: true /get-stream@3.0.0: resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} @@ -3095,7 +3255,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.4 - dev: true /got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} @@ -3153,17 +3312,14 @@ packages: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: get-intrinsic: 1.2.4 - dev: true /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} - dev: true /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - dev: true /has-tostringtag@1.0.2: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} @@ -3177,7 +3333,6 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 - dev: true /heap@0.2.7: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} @@ -3241,6 +3396,10 @@ packages: wrappy: 1.0.2 dev: true + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + dev: false + /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3416,6 +3575,12 @@ packages: which-typed-array: 1.1.14 dev: true + /is-valid-identifier@2.0.2: + resolution: {integrity: sha512-mpS5EGqXOwzXtKAg6I44jIAqeBfntFLxpAth1rrKbxtKyI6LPktyDYpHBI+tHlduhhX/SF26mFXmxQu995QVqg==} + dependencies: + assert: 1.5.1 + dev: false + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -3567,6 +3732,10 @@ packages: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -3697,6 +3866,7 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + requiresBuild: true /nan@2.18.0: resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} @@ -3771,7 +3941,6 @@ packages: /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - dev: true /object.assign@4.1.5: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} @@ -3781,7 +3950,6 @@ packages: define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 - dev: true /object.fromentries@2.0.7: resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} @@ -4355,7 +4523,6 @@ packages: get-intrinsic: 1.2.4 gopd: 1.0.1 has-property-descriptors: 1.0.1 - dev: true /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} @@ -4732,6 +4899,10 @@ packages: typescript: 5.3.3 dev: true + /ts-mixer@6.0.3: + resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} + dev: false + /tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} dependencies: @@ -4942,6 +5113,12 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + dependencies: + inherits: 2.0.3 + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: diff --git a/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberAddListener.ts b/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberAddListener.ts index a17cd99b..ee0b0905 100644 --- a/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberAddListener.ts +++ b/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberAddListener.ts @@ -40,7 +40,7 @@ export class GuildMemberAddListener extends Listener { guildId: payload.data.d.guild_id, avatar: payload.data.d.avatar, flags: payload.data.d.flags, - communicationDisabledUntil: payload.data.d.premium_since, + communicationDisabledUntil: payload.data.d.communication_disabled_until, deaf: payload.data.d.deaf, joinedAt: payload.data.d.joined_at, mute: payload.data.d.mute, @@ -52,7 +52,7 @@ export class GuildMemberAddListener extends Listener { set: { avatar: payload.data.d.avatar, flags: payload.data.d.flags, - communicationDisabledUntil: payload.data.d.premium_since, + communicationDisabledUntil: payload.data.d.communication_disabled_until, deaf: payload.data.d.deaf, joinedAt: payload.data.d.joined_at, mute: payload.data.d.mute, @@ -65,7 +65,8 @@ export class GuildMemberAddListener extends Listener { for (const role of payload.data.d.roles) { await this.store.drizzle.insert(memberRoles).values({ id: payload.data.d.user!.id, - roleId: role + roleId: role, + guildId: payload.data.d.guild_id }).onConflictDoNothing({ target: memberRoles.id }); } diff --git a/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberUpdateListener.ts b/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberUpdateListener.ts index 034bdf58..ac3e28c0 100644 --- a/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberUpdateListener.ts +++ b/services/kanao-gateway/src/Listeners/Caches/GuildMembers/GuildMemberUpdateListener.ts @@ -85,7 +85,8 @@ export class GuildMemberUpdateListener extends Listener { for (const role of payload.data.d.roles) { await this.store.drizzle.insert(memberRoles).values({ id: payload.data.d.user.id, - roleId: role + roleId: role, + guildId: payload.data.d.guild_id }).onConflictDoNothing({ target: memberRoles.id }); } } diff --git a/services/kanao-gateway/src/Listeners/Caches/Guilds/GuildCreateListener.ts b/services/kanao-gateway/src/Listeners/Caches/Guilds/GuildCreateListener.ts index 5932d36f..4921987c 100644 --- a/services/kanao-gateway/src/Listeners/Caches/Guilds/GuildCreateListener.ts +++ b/services/kanao-gateway/src/Listeners/Caches/Guilds/GuildCreateListener.ts @@ -131,24 +131,24 @@ export class GuildCreateListener extends Listener { await this.store.drizzle.insert(users).values({ id: member.user.id, username: member.user.username, - discriminator: member.user?.discriminator ?? null, - globalName: member.user?.global_name ?? null, - avatar: member.user?.avatar ?? null, - bot: member.user?.bot ?? false, - flags: member.user?.flags, - premiumType: member.user?.premium_type, - publicFlags: member.user?.public_flags + discriminator: member.user.discriminator ?? null, + globalName: member.user.global_name ?? null, + avatar: member.user.avatar ?? null, + bot: member.user.bot ?? false, + flags: member.user.flags, + premiumType: member.user.premium_type, + publicFlags: member.user.public_flags }).onConflictDoUpdate({ target: users.id, set: { username: member.user.username, - discriminator: member.user?.discriminator ?? null, - globalName: member.user?.global_name ?? null, - avatar: member.user?.avatar ?? null, - bot: member.user?.bot ?? false, - flags: member.user?.flags, - premiumType: member.user?.premium_type, - publicFlags: member.user?.public_flags + discriminator: member.user.discriminator ?? null, + globalName: member.user.global_name ?? null, + avatar: member.user.avatar ?? null, + bot: member.user.bot ?? false, + flags: member.user.flags, + premiumType: member.user.premium_type, + publicFlags: member.user.public_flags } }); } @@ -184,7 +184,8 @@ export class GuildCreateListener extends Listener { for (const role of member.roles) { await this.store.drizzle.insert(memberRoles).values({ id: member.user.id, - roleId: role + roleId: role, + guildId: payload.data.d.id }).onConflictDoNothing({ target: memberRoles.id }); } } diff --git a/services/kanao-gateway/src/Listeners/Caches/Messages/MessageCreateListener.ts b/services/kanao-gateway/src/Listeners/Caches/Messages/MessageCreateListener.ts index 3dc51ff3..a666fb1d 100644 --- a/services/kanao-gateway/src/Listeners/Caches/Messages/MessageCreateListener.ts +++ b/services/kanao-gateway/src/Listeners/Caches/Messages/MessageCreateListener.ts @@ -106,7 +106,8 @@ export class MessageCreateListener extends Listener { for (const role of payload.data.d.member.roles) { await this.store.drizzle.insert(memberRoles).values({ id: payload.data.d.author.id, - roleId: role + roleId: role, + guildId: payload.data.d.guild_id }).onConflictDoNothing({ target: memberRoles.id }); } } diff --git a/services/kanao-gateway/src/Stores/ListenerStore.ts b/services/kanao-gateway/src/Stores/ListenerStore.ts index 3074d4c5..460532d8 100644 --- a/services/kanao-gateway/src/Stores/ListenerStore.ts +++ b/services/kanao-gateway/src/Stores/ListenerStore.ts @@ -1,11 +1,11 @@ import type EventEmitter from "node:events"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import type * as schema from "@nezuchan/kanao-schema"; import { Store } from "@sapphire/pieces"; import type { ChannelWrapper } from "amqp-connection-manager"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import type { Logger } from "pino"; -import type * as schema from "../Schema/index.js"; import { Listener } from "./Listener.js"; export class ListenerStore extends Store { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index f11bcdb6..ea2a9874 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "include": [ - "./services/**/*.ts" + "./services/**/*.ts", + "./packages/**/*.ts" ] } \ No newline at end of file diff --git a/turbo.json b/turbo.json index 62811809..7592cbfe 100644 --- a/turbo.json +++ b/turbo.json @@ -2,6 +2,7 @@ "$schema": "https://turborepo.org/schema.json", "pipeline": { "build": { + "dependsOn": ["^build"], "outputs": ["dist/**"] }, "lint": {