diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c00f6f1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +charset = utf-8 +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 4 +max_line_length = 150 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a9ed889 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +METEORIUM_POSTGRES_URL= +METEORIUM_BOT_TOKEN= +METEORIUM_APPLICATION_ID= +METEORIUM_HOLODEX_APIKEY= +METEORIUM_GENIUS_APIKEY= +METEORIUM_APPDEPLOY_GUILDIDS= +METEORIUM_RUNTIMELOG_CHANNELIDS= +METEORIUM_NOREG_TESTINTERACTIONS= diff --git a/.gitignore b/.gitignore index d7c632e..5ec809f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -config.json +# Compile deps and results node_modules -.ENV -dist \ No newline at end of file +dist + +# Configuration files +.env diff --git a/.prettierrc b/.prettierrc index 5359961..4e26006 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,7 @@ "semi": true, "singleQuote": false, "useTabs": false, - "printWidth": 120 + "printWidth": 120, + "endOfLine": "lf", + "trailingComma": "all" } diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1b65db6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["EditorConfig.EditorConfig", "esbenp.prettier-vscode", "mikestead.dotenv", "prisma.prisma"] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5cec8db --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build", + "type": "shell", + "command": "yarn compile", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index f288702..8171959 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,19 @@ - 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 -. +Copyright (c) 2024 RadiatedExodus (RealEthanPlayzDev) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 77494a5..573432b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,28 @@ # Meteorium -A Discord bot developed by RadiatedExodus (RealEthanPlayzDev) as a side (and personal) project, written in Javascript using Node.js and Discord.js, also being used as a way for me to learn JavaScript and TypeScript. +This is a hobby Discord bot I've written, the bot is only used for a few servers. +Feel free to look around, make suggestion, and report bugs. -Since the ts rewrite, some of the source might ressemble [PojavBot](https://github.com/PojavLauncherTeam/PojavBot) as I took several references from there. - -## UNSTABLE - THIS IS A FULL REWRITE +## FULL REWRITE (v3) This branch is a full rewrite, not all features have been implemented! +## Feature parity with v2 + +- [*] Moderation +- [ ] Music +- [ ] Info +- [ ] HolodexAPI +- [ ] MojangAPI +- [ ] RbxAPI +- [ ] ServerInfo +- [ ] UserInfo +- [ ] Tag +- [ ] Ping + ## Installing required dependencies -Ensure `yarn` is installed (`npm install --global yarn`), then just run it at the root of the repository +Meteorium uses `yarn` to manage Node packages. Ensure `yarn` is installed (`npm install --global yarn`), then just run it at the root of the repository ``` yarn @@ -33,34 +45,8 @@ yarn start ## Configuration file -The configuration file uses `dotenv`, create a file named ".ENV" on the project root and use the following example: - -``` -METEORIUMBOTTOKEN=bot_token_here -METEORIUMPOSTGRESURL=postgres_url_here -METEORIUMHOLODEXTOKEN=holodex_token_here_optional -METEORIUMAPPLICATIONID=bot_app_id_here -RATELIMITMAXLIMIT=rate_limit_maximum_limit_before_nodejs_terminates_PUT_A_NUMBER_HERE -RATELIMITMAXLIMITTIME=after_when_should_ratelimit_reset_PUT_A_NUMBER_HERE -DEPLOYGUILDIDS=guildids_for_deployment_seperated_by,commas,and_so_on -GENIUSAPIKEY=genius_api_key_here -``` - -## Credits - -- [discord.js](https://github.com/discordjs/discord.js) -- [holodex.js](https://github.com/HolodexNet/holodex.js) -- [noblox.js](https://github.com/noblox/noblox.js) -- [Prisma](https://www.prisma.io) -- [dotenv](https://github.com/motdotla/dotenv) +See the `.env.example` -## Acknowledgements +## Special thanks -- All discord.js contributors and authors -- All holodex.js contributors and authors -- All noblox.js contributors and authors -- All mongodb contributors and authors -- All dotenv contributors and authors -- All Prisma contributors and authors -- All PostgreSQL contributors and authors -- Syjalo +- [@Abdelrahmanwalidhassan's `ms` fork](https://github.com/Abdelrahmanwalidhassan/ms) diff --git a/package.json b/package.json index 8c3fa61..4e75f2b 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,43 @@ { "name": "meteorium", - "version": "2.0.0", - "description": "A Discord bot created by RadiatedExodus as a hobby project.", + "version": "3.0.0", + "description": "RadiatedExodus's hobby Discord bot project, built with TypeScript, Prisma, and DiscordJS.", "main": "dist/index.js", + "type": "module", "scripts": { "compile": "tsc", - "start": "node --enable-source-maps --es-module-specifier-resolution=node ." + "start": "node dist", + "installUtils:fillMissingGuildData": "node ./dist/installUtils/fillMissingGuildData.js", + "installUtils:enableGuildFeature": "node ./dist/installUtils/enableGuildFeature.js" }, "repository": { "type": "git", "url": "git+https://github.com/RealEthanPlayzDev/Meteorium.git" }, "author": "RadiatedExodus", - "license": "GPL-3.0", + "license": "MIT", "bugs": { "url": "https://github.com/RealEthanPlayzDev/Meteorium/issues" }, - "homepage": "https://github.com/RealEthanPlayzDev/Meteorium#readme", + "homepage": "https://github.com/RealEthanPlayzDev/Meteorium", "devDependencies": { - "prettier": "3.0.3", - "prisma": "^5.2.0", - "typescript": "^5.1.6" + "@types/node": "^20.11.17", + "prettier": "^3.2.5", + "prisma": "^5.9.1", + "typescript": "^5.3.3" }, "dependencies": { - "@discord-player/extractor": "^4.4.6", "@discordjs/opus": "^0.9.0", "@discordjs/voice": "^0.16.1", "@prisma/client": "^5.9.1", - "@types/ms": "^0.7.32", - "axios": "^1.6.3", - "chalk": "4.0.0", - "discord-player": "^6.6.7", + "bufferutil": "^4.0.8", + "chalk": "^5.3.0", "discord.js": "^14.14.1", - "dotenv": "^16.0.3", + "dotenv": "^16.4.2", "holodex.js": "^2.0.5", - "libsodium-wrappers": "^0.7.10", - "moment": "^2.29.4", - "ms": "^2.1.3", + "moment": "^2.30.1", "noblox.js": "^4.15.1", - "play-dl": "^1.9.7", - "soundcloud-scraper": "^5.0.3", - "spotify-url-info": "^3.2.13", - "youtube-sr": "^4.3.10", - "ytdl-core": "^4.11.5" + "utf-8-validate": "^6.0.3", + "zlib-sync": "^0.1.9" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5ba4741..9ad2b33 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,19 +7,7 @@ generator client { datasource db { provider = "postgresql" - url = env("METEORIUMPOSTGRESURL") -} - -model Guild { - GuildId String @unique - EnforceSayInExecutor Boolean @default(false) - DisabledCommands String[] @default([]) - DisabledCommandCategories String[] @default([]) - PublicModLogChannelId String @default("") - LoggingChannelId String @default("") - JoinLeaveLogChannelId String @default("") - CurrentCaseId Int @default(0) - BanAppealLink String @default("") + url = env("METEORIUM_POSTGRES_URL") } enum ModerationAction { @@ -31,37 +19,51 @@ enum ModerationAction { Unban } +enum GuildFeatures { + Moderation + UserVerification + Tags + Music + DiscordInfo + HolodexAPI + MojangAPI + RobloxAPI +} + +model Guild { + GuildId String @unique + EnforceSayInExecutor Boolean @default(false) + JoinLeaveLogChannelId String @default("") + PublicModLogChannelId String @default("") + LoggingChannelId String @default("") + BanAppealLink String @default("") + EnabledGuildFeatures GuildFeatures[] + ModerationCase ModerationCase[] + Tag Tag[] +} + model ModerationCase { GlobalCaseId Int @id @default(autoincrement()) + Guild Guild @relation(fields: [GuildId], references: [GuildId], onDelete: Cascade) + GuildId String CaseId Int Action ModerationAction TargetUserId String ModeratorUserId String - GuildId String Reason String - AttachmentProof String + AttachmentProof String @default("") Duration String @default("0") - CreatedAt DateTime @default(now()) ModeratorNote String @default("") ModeratorAttachment String @default("") NotAppealable Boolean @default(false) + Active Boolean @default(true) // Used in ban + RelatedCaseId Int @default(-1) // Used in ban, does NOT use GlobalCaseId PublicModLogMsgId String @default("") + CreatedAt DateTime @default(now()) ActiveTempBans ActiveTempBans[] ModerationCaseHistory ModerationCaseHistory[] -} -model Tag { - GlobalTagId Int @id @default(autoincrement()) - TagName String - GuildId String - Content String - Image String -} - -model ActiveTempBans { - ActiveTempBanId Int @id @default(autoincrement()) - GlobalCaseId Int - Case ModerationCase @relation(fields: [GlobalCaseId], references: [GlobalCaseId], onDelete: Cascade) + @@unique(name: "UniqueCaseIdPerGuild", fields: [GuildId, CaseId]) } model ModerationCaseHistory { @@ -69,11 +71,27 @@ model ModerationCaseHistory { ModerationCase ModerationCase @relation(fields: [GlobalCaseId], references: [GlobalCaseId], onDelete: Cascade) GlobalCaseId Int EditedAt DateTime @default(now()) - Editor String + EditorUserId String Reason String? AttachmentProof String? Duration String? ModeratorNote String? ModeratorAttachment String? NotAppealable Boolean? + Removed Boolean? +} + +model ActiveTempBans { + ActiveTempBanId Int @id @default(autoincrement()) + GlobalCaseId Int @unique + Case ModerationCase @relation(fields: [GlobalCaseId], references: [GlobalCaseId], onDelete: Cascade) +} + +model Tag { + GlobalTagId Int @id @default(autoincrement()) + Guild Guild @relation(fields: [GuildId], references: [GuildId], onDelete: Cascade) + GuildId String + TagName String + Content String + Attachment String @default("") } diff --git a/src/classes/client.ts b/src/classes/client.ts new file mode 100644 index 0000000..00bb3bb --- /dev/null +++ b/src/classes/client.ts @@ -0,0 +1,63 @@ +import { Client, ClientOptions } from "discord.js"; +import { config } from "dotenv"; +import { PrismaClient } from "@prisma/client"; +import { HolodexApiClient } from "holodex.js"; + +import Logging from "./logging.js"; +import MeteoriumInteractionManager from "../interactions/index.js"; +import MeteoriumEventManager from "../events/index.js"; +import MeteoriumDatabaseUtilities from "./dbUtils.js"; +import MeteoriumGuildFeatureManager from "./guildFeatureManager.js"; + +function parseDotEnvConfig() { + if (!process.env.METEORIUM_BOT_TOKEN) config(); + return { + BotToken: String(process.env.METEORIUM_BOT_TOKEN), + ApplicationId: String(process.env.METEORIUM_APPLICATION_ID), + HolodexApiKey: String(process.env.METEORIUM_HOLODEX_APIKEY), + GeniusApiKey: String(process.env.METEORIUM_GENIUS_APIKEY), + ApplicationDeployGuildIds: String(process.env.METEORIUM_APPDEPLOY_GUILDIDS).split(","), + RuntimeLogChannelIds: String(process.env.METEORIUM_RUNTIMELOG_CHANNELIDS).split(","), + DontRegisterTestInteractions: Boolean(process.env.METEORIUM_NOREG_TESTINTERACTIONS), + }; +} + +export default class MeteoriumClient extends Client { + public logging = new Logging("Meteorium"); + public config = parseDotEnvConfig(); + public db = new PrismaClient(); + public dbUtils = new MeteoriumDatabaseUtilities(this); + public guildFeatures = new MeteoriumGuildFeatureManager(this); + public interactions = new MeteoriumInteractionManager(this); + public events = new MeteoriumEventManager(this); + public holodex = new HolodexApiClient({ apiKey: this.config.HolodexApiKey }); + + public constructor(options: ClientOptions) { + super(options); + + // Register all events and hook them + this.events.register(); + + // Register all interactions + this.interactions.registerAllInteractions(); + + return this; + } + + public async login() { + const loginNS = this.logging.getNamespace("Login"); + + // Hook events + this.events.hook(); + + // Login + loginNS.info("Logging in to Discord"); + return super.login(this.config.BotToken); + } + + public async destroy() { + await super.destroy(); + await this.db.$disconnect(); + return; + } +} diff --git a/src/classes/dbUtils.ts b/src/classes/dbUtils.ts new file mode 100644 index 0000000..213e07f --- /dev/null +++ b/src/classes/dbUtils.ts @@ -0,0 +1,294 @@ +import { EmbedBuilder, MessageCreateOptions, MessagePayload, User, time, userMention } from "discord.js"; +import { ModerationAction, ModerationCase } from "@prisma/client"; +import MeteoriumClient from "./client.js"; +import MeteoriumEmbedBuilder from "./embedBuilder.js"; + +export type CaseData = { + GlobalCaseId: number; + CaseId: number; + Action: ModerationAction; + TargetUserId: string; + ModeratorUserId: string; + Active: boolean; + RelatedCaseId: number; + PublicLogMsgId: string; + CreatedAt: Date; + + Reason: string; + AttachmentProof: string; + Duration: string; + ModeratorNote: string; + ModeratorAttachment: string; + NotAppealable: boolean; + Removed: boolean; +}; + +export type NewCaseData = { + Action: ModerationAction; + GuildId: string; + TargetUserId: string; + ModeratorUserId: string; + RelatedCaseId?: number; + Reason: string; + AttachmentProof?: string; + Duration?: string; + ModeratorNote?: string; + ModeratorAttachment?: string; + NotAppealable?: boolean; +}; + +export default class MeteoriumDatabaseUtilities { + public client: MeteoriumClient; + + public constructor(client: MeteoriumClient) { + this.client = client; + } + + public async getCaseData(guildId: string, caseId: number, historyTake?: number): Promise { + // Get case + const caseDb = await this.client.db.moderationCase.findUnique({ + where: { UniqueCaseIdPerGuild: { GuildId: guildId, CaseId: caseId } }, + include: { ModerationCaseHistory: { take: historyTake } }, + }); + if (!caseDb) return false; + + // Build a final data dictionary + const finalData: CaseData = { + GlobalCaseId: caseDb.GlobalCaseId, + CaseId: caseDb.CaseId, + Action: caseDb.Action, + TargetUserId: caseDb.TargetUserId, + ModeratorUserId: caseDb.ModeratorUserId, + Active: caseDb.Active, + RelatedCaseId: caseDb.RelatedCaseId, + PublicLogMsgId: caseDb.PublicModLogMsgId, + CreatedAt: caseDb.CreatedAt, + + Reason: caseDb.Reason, + AttachmentProof: caseDb.AttachmentProof, + Duration: caseDb.Duration, + ModeratorNote: caseDb.ModeratorNote, + ModeratorAttachment: caseDb.ModeratorAttachment, + NotAppealable: caseDb.NotAppealable, + Removed: false, + }; + + // Apply edits + for (const edit of caseDb.ModerationCaseHistory) { + finalData.Reason = edit.Reason != null ? edit.Reason : finalData.Reason; + finalData.AttachmentProof = edit.AttachmentProof != null ? edit.AttachmentProof : finalData.AttachmentProof; + finalData.Duration = edit.Duration != null ? edit.Duration : finalData.Duration; + finalData.ModeratorNote = edit.ModeratorNote != null ? edit.ModeratorNote : finalData.ModeratorNote; + finalData.ModeratorAttachment = + edit.ModeratorAttachment != null ? edit.ModeratorAttachment : finalData.ModeratorAttachment; + finalData.NotAppealable = edit.NotAppealable != null ? edit.NotAppealable : finalData.NotAppealable; + finalData.Removed = edit.Removed != null ? edit.Removed : finalData.Removed; + } + + return finalData; + } + + public async getCasesWithLatestHistory( + guildId: string, + targetUserId?: string, + take?: number, + skip?: number, + ): Promise> { + // Original cases + const cases = await this.client.db.moderationCase.findMany({ + where: { GuildId: guildId, TargetUserId: targetUserId }, + orderBy: { CaseId: "desc" }, + take: take, + skip: skip, + }); + + const latestCases: Array = []; + for (const caseDataOriginal of cases) { + const latestCaseData = await this.getCaseData(guildId, caseDataOriginal.CaseId); + if (!latestCaseData) throw new Error(`could not get case data for case ${caseDataOriginal.CaseId}`); + latestCases.push(latestCaseData); + } + + return latestCases; + } + + public async generateCaseEmbedFromData( + caseData: CaseData, + requester?: User, + full?: boolean, + inclRequester?: boolean, + ): Promise { + // Get user datas + const modUser = await this.client.users.fetch(caseData.ModeratorUserId).catch(() => null); + const targetUser = await this.client.users.fetch(caseData.TargetUserId).catch(() => null); + + const embed = new MeteoriumEmbedBuilder(requester); + + // Set the author field + embed.setAuthor({ + name: `Case #${caseData.CaseId} | ${caseData.Action.toLowerCase()} | ${targetUser != null ? `${targetUser.username} (${targetUser.id})` : caseData.TargetUserId}`, + iconURL: targetUser != null ? targetUser.displayAvatarURL({ extension: "png", size: 256 }) : undefined, + }); + + // Set the attachment proof if any exist + embed.setThumbnail(caseData.AttachmentProof == "" ? null : caseData.AttachmentProof); + + // Include requester? + if (inclRequester && requester) + embed.addFields([ + { name: "Requester", value: `${userMention(requester.id)} (${requester.username} - ${requester.id})` }, + ]); + + // Add target and moderator fields + embed.addFields([ + { + name: "Moderator", + value: `${userMention(caseData.ModeratorUserId)} (${modUser != null ? `${modUser.username} - ${modUser.id}` : caseData.ModeratorUserId})`, + }, + { + name: "Target", + value: `${userMention(caseData.TargetUserId)} (${targetUser != null ? `${targetUser.username} - ${targetUser.id}` : caseData.TargetUserId})`, + }, + ]); + + // Reason + embed.addFields([{ name: "Reason", value: caseData.Reason }]); + + // Duration + if (caseData.Action == ModerationAction.Mute || caseData.Action == ModerationAction.TempBan) + embed.addFields([{ name: "Duration", value: caseData.Duration }]); + + // Appealable + if (caseData.Action == ModerationAction.Ban) + embed.addFields([{ name: "Appealable", value: caseData.NotAppealable ? "No" : "Yes" }]); + + // Active ban and related appeal case id + if (caseData.Action == ModerationAction.Ban && full) + embed.addFields([ + { name: "Active ban", value: caseData.Active ? "Yes" : "No" }, + { + name: "Appeal case id", + value: caseData.RelatedCaseId != -1 ? caseData.RelatedCaseId.toString() : "N/A", + }, + ]); + + // Full data + if (full) { + embed.addFields([ + { name: "Moderator note", value: caseData.ModeratorNote == "" ? "N/A" : caseData.ModeratorNote }, + { name: "Removed", value: caseData.Removed ? "Yes" : "No" }, + ]); + embed.setImage(caseData.ModeratorAttachment == "" ? null : caseData.ModeratorAttachment); + } + + // Created at + embed.addFields([ + { name: "Created at", value: `${time(caseData.CreatedAt, "F")} (${time(caseData.CreatedAt, "R")})` }, + ]); + + return embed; + } + + public async generateCaseEmbedFromCaseId( + guildId: string, + caseId: number, + requester?: User, + full?: boolean, + historyTake?: number, + inclRequester?: boolean, + ): Promise { + const caseData = await this.getCaseData(guildId, caseId, historyTake); + if (!caseData) return false; + return await this.generateCaseEmbedFromData(caseData, requester, full, inclRequester); + } + + public async sendGuildLog(guildId: string, reply: string | MessagePayload | MessageCreateOptions) { + const guildSettings = await this.client.db.guild.findUnique({ where: { GuildId: guildId } }); + if (!guildSettings) return; + if (guildSettings.LoggingChannelId == "") return; + + const channel = await this.client.channels.fetch(guildSettings.LoggingChannelId).catch(() => null); + if (!channel || !channel.isTextBased()) return; + + return await channel.send(reply); + } + + public async sendGuildPubLog(guildId: string, reply: string | MessagePayload | MessageCreateOptions) { + const guildSettings = await this.client.db.guild.findUnique({ where: { GuildId: guildId } }); + if (!guildSettings) return; + if (guildSettings.LoggingChannelId == "") return; + + const channel = await this.client.channels.fetch(guildSettings.PublicModLogChannelId).catch(() => null); + if (!channel || !channel.isTextBased()) return; + + return await channel.send(reply); + } + + public async createModerationCase( + data: NewCaseData, + afterDbCreateCallback?: (caseDb: ModerationCase) => Promise, + ) { + // Create case + const caseDb = await this.client.db.moderationCase.create({ + data: { + GuildId: data.GuildId, + CaseId: (await this.client.db.moderationCase.count({ where: { GuildId: data.GuildId } })) + 1, + Action: data.Action, + TargetUserId: data.TargetUserId, + ModeratorUserId: data.ModeratorUserId, + Reason: data.Reason, + AttachmentProof: data.AttachmentProof, + Duration: data.Duration, + ModeratorNote: data.ModeratorNote, + ModeratorAttachment: data.ModeratorAttachment, + NotAppealable: data.NotAppealable, + RelatedCaseId: data.RelatedCaseId, + }, + }); + + // Do callback + if (afterDbCreateCallback) await afterDbCreateCallback(caseDb); + + // Generate embed + const embed = await this.generateCaseEmbedFromData( + { + ...caseDb, + Removed: false, + PublicLogMsgId: "", + }, + undefined, + false, + false, + ); + + // Send in public log + const pubLog = await this.sendGuildPubLog(data.GuildId, { embeds: [embed] }); + if (pubLog) + await this.client.db.moderationCase.update({ + where: { GlobalCaseId: caseDb.GlobalCaseId }, + data: { PublicModLogMsgId: pubLog.id }, + }); + + // Generate full embed + const fullEmbed = await this.generateCaseEmbedFromData( + { + ...caseDb, + Removed: false, + PublicLogMsgId: pubLog ? pubLog.id : "", + }, + await this.client.users.fetch(data.ModeratorUserId).catch(() => undefined), + true, + true, + ); + + // Send in private log + await this.sendGuildLog(data.GuildId, { embeds: [fullEmbed] }); + + return { + globalCaseId: caseDb.GlobalCaseId, + caseId: caseDb.CaseId, + embed: embed, + fullEmbed: fullEmbed, + }; + } +} diff --git a/src/classes/embedBuilder.ts b/src/classes/embedBuilder.ts new file mode 100644 index 0000000..69cc51a --- /dev/null +++ b/src/classes/embedBuilder.ts @@ -0,0 +1,28 @@ +import { EmbedBuilder, APIEmbed, EmbedData, User } from "discord.js"; + +export default class MeteoriumEmbedBuilder extends EmbedBuilder { + public constructor(requester?: User, data?: EmbedData | APIEmbed) { + super(data); + + this.setTimestamp(); + this.setNormalColor(); + this.setFooter({ + text: `${requester ? `Requested by ${requester.username} (${requester.id}) | ` : ""}Meteorium | v3`, + iconURL: requester + ? requester.avatarURL({ extension: "png", size: 128 }) || requester.defaultAvatarURL + : undefined, + }); + + return this; + } + + public setNormalColor() { + this.setColor([0, 153, 255]); + return this; + } + + public setErrorColor() { + this.setColor([255, 0, 0]); + return this; + } +} diff --git a/src/classes/guildFeatureManager.ts b/src/classes/guildFeatureManager.ts new file mode 100644 index 0000000..e40fbb5 --- /dev/null +++ b/src/classes/guildFeatureManager.ts @@ -0,0 +1,40 @@ +export { GuildFeatures } from "@prisma/client"; +import { GuildFeatures } from "@prisma/client"; +import MeteoriumClient from "./client.js"; + +export default class MeteoriumGuildFeatureManager { + public client: MeteoriumClient; + + public constructor(client: MeteoriumClient) { + this.client = client; + } + + public async hasFeatureEnabled(guildId: string, feature: GuildFeatures) { + const guildSettings = await this.client.db.guild.findUnique({ where: { GuildId: guildId } }); + if (!guildSettings) return false; + return guildSettings.EnabledGuildFeatures.indexOf(feature) != -1; + } + + public async enableFeature(guildId: string, feature: GuildFeatures) { + if (await this.hasFeatureEnabled(guildId, feature)) return; + await this.client.db.guild.update({ + where: { GuildId: guildId }, + data: { + EnabledGuildFeatures: { push: [feature] }, + }, + }); + return; + } + + public async disableFeature(guildId: string, feature: GuildFeatures) { + const guildSettings = await this.client.db.guild.findUnique({ where: { GuildId: guildId } }); + if (!guildSettings) return false; + await this.client.db.guild.update({ + where: { GuildId: guildId }, + data: { + EnabledGuildFeatures: guildSettings.EnabledGuildFeatures.filter((v) => v != feature), + }, + }); + return; + } +} diff --git a/src/classes/logging.ts b/src/classes/logging.ts new file mode 100644 index 0000000..e09a94f --- /dev/null +++ b/src/classes/logging.ts @@ -0,0 +1,84 @@ +import chalk from "chalk"; +import type { ChalkInstance } from "chalk"; +import moment from "moment"; + +const RED = chalk.red; +const YELLOW = chalk.yellow; +const CYAN = chalk.cyan; +const MAGENTA = chalk.magenta; +const GRAY = chalk.gray; + +function getCurrentDTStr() { + return moment().format("DD-MM-YYYY hh:mm:ss:SSS A Z"); +} + +function getFullName(obj: LoggingNamespace) { + let name = obj.name; + let ptr: Logging | LoggingNamespace = obj.parent; + while (ptr instanceof LoggingNamespace) { + name = `${ptr.name}/${name}`; + ptr = ptr.parent; + } + name = `${ptr.name}/${name}`; + return name; +} + +export default class Logging { + public namespaces: Array; + public name: string; + public constructor(name: string) { + this.name = name; + this.namespaces = new Array(); + } + registerNamespace(name: string): LoggingNamespace { + const Namespace = new LoggingNamespace(name, this); + this.namespaces.push(Namespace); + return Namespace; + } + getNamespace(name: string): LoggingNamespace { + for (const namespace of this.namespaces) { + if (namespace.name == name) return namespace; + } + return this.registerNamespace(name); + } +} + +export class LoggingNamespace { + public name: string; + public namespaces: Array; + public parent: Logging | LoggingNamespace; + public constructor(name: string, root: Logging | LoggingNamespace) { + this.name = name; + this.namespaces = []; + this.parent = root; + } + registerNamespace(name: string): LoggingNamespace { + const Namespace = new LoggingNamespace(name, this); + this.namespaces.push(Namespace); + return Namespace; + } + getNamespace(name: string): LoggingNamespace { + for (const namespace of this.namespaces) { + if (namespace.name == name) return namespace; + } + return this.registerNamespace(name); + } + write(color: ChalkInstance, msg: string, ...data: string[]) { + return console.log(color(msg), ...data); + } + verbose(msg: string, ...data: string[]) { + return this.write(GRAY, `[${getCurrentDTStr()}] [VRB] [${getFullName(this)}] ${msg}`, ...data); + } + debug(msg: string, ...data: string[]) { + return this.write(MAGENTA, `[${getCurrentDTStr()}] [DBG] [${getFullName(this)}] ${msg}`, ...data); + } + info(msg: string, ...data: string[]) { + return this.write(CYAN, `[${getCurrentDTStr()}] [INF] [${getFullName(this)}] ${msg}`, ...data); + } + warn(msg: string, ...data: string[]) { + return this.write(YELLOW, `[${getCurrentDTStr()}] [WRN] [${getFullName(this)}] ${msg}`, ...data); + } + error(msg: string, ...data: string[]) { + return this.write(RED, `[${getCurrentDTStr()}] [ERR] [${getFullName(this)}] ${msg}`, ...data); + } +} diff --git a/src/classes/ms.ts b/src/classes/ms.ts new file mode 100644 index 0000000..fb0446e --- /dev/null +++ b/src/classes/ms.ts @@ -0,0 +1,241 @@ +// https://github.com/vercel/ms +// https://github.com/Abdelrahmanwalidhassan/ms + +// Helpers. +const s = 1000; +const m = s * 60; +const h = m * 60; +const d = h * 24; +const w = d * 7; +const mo = d * 30.4375; +const y = d * 365.25; + +type Unit = + | "Years" + | "Year" + | "Yrs" + | "Yr" + | "Y" + | "Months" + | "Month" + | "Mo" + | "Weeks" + | "Week" + | "W" + | "Days" + | "Day" + | "D" + | "Hours" + | "Hour" + | "Hrs" + | "Hr" + | "H" + | "Minutes" + | "Minute" + | "Mins" + | "Min" + | "M" + | "Seconds" + | "Second" + | "Secs" + | "Sec" + | "s" + | "Milliseconds" + | "Millisecond" + | "Msecs" + | "Msec" + | "Ms"; + +type UnitAnyCase = Unit | Uppercase | Lowercase; + +export type StringValue = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}` | string; + +interface Options { + /** + * Set to `true` to use verbose formatting. Defaults to `false`. + */ + long?: boolean; +} + +/** + * Parse or format the given value. + * + * @param value - The string or number to convert + * @param options - Options for the conversion + * @throws Error if `value` is not a non-empty string or a number + */ +function msFn(value: StringValue, options?: Options): number; +function msFn(value: number, options?: Options): string; +function msFn(value: StringValue | number, options?: Options): number | string { + try { + if (typeof value === "string" && value.length > 0) { + return parse(value); + } else if (typeof value === "number" && isFinite(value)) { + return options?.long ? fmtLong(value) : fmtShort(value); + } + throw new Error("Value is not a string or number."); + } catch (error) { + const message = isError(error) + ? `${error.message}. value=${JSON.stringify(value)}` + : "An unknown error has occurred."; + throw new Error(message); + } +} + +/** + * Parse the given string and return milliseconds. + * + * @param str - A string to parse to milliseconds + * @returns The parsed value in milliseconds, or `NaN` if the string can't be + * parsed + */ +function parse(str: string): number { + if (str.length > 100) { + throw new Error("Value exceeds the maximum length of 100 characters."); + } + const match = + /^(?-?(?:\d+)?\.?\d+) *(?milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mo?|years?|yrs?|y)?$/i.exec( + str, + ); + // Named capture groups need to be manually typed today. + // https://github.com/microsoft/TypeScript/issues/32098 + const groups = match?.groups as { value: string; type?: string } | undefined; + if (!groups) { + return NaN; + } + const n = parseFloat(groups.value); + const type = (groups.type || "ms").toLowerCase() as Lowercase; + switch (type) { + case "years": + case "year": + case "yrs": + case "yr": + case "y": + return n * y; + case "months": + case "month": + case "mo": + return n * mo; + case "weeks": + case "week": + case "w": + return n * w; + case "days": + case "day": + case "d": + return n * d; + case "hours": + case "hour": + case "hrs": + case "hr": + case "h": + return n * h; + case "minutes": + case "minute": + case "mins": + case "min": + case "m": + return n * m; + case "seconds": + case "second": + case "secs": + case "sec": + case "s": + return n * s; + case "milliseconds": + case "millisecond": + case "msecs": + case "msec": + case "ms": + return n; + default: + // This should never occur. + throw new Error(`The unit ${type as string} was matched, but no matching case exists.`); + } +} + +/** + * Parse the given StringValue and return milliseconds. + * + * @param value - A typesafe StringValue to parse to milliseconds + * @returns The parsed value in milliseconds, or `NaN` if the string can't be + * parsed + */ +export function parseStrict(value: StringValue): number { + return parse(value); +} + +// eslint-disable-next-line import/no-default-export +export default msFn; + +/** + * Short format for `ms`. + */ +function fmtShort(ms: number): StringValue { + const msAbs = Math.abs(ms); + if (msAbs >= d) { + return `${Math.round(ms / d)}d`; + } + if (msAbs >= h) { + return `${Math.round(ms / h)}h`; + } + if (msAbs >= m) { + return `${Math.round(ms / m)}m`; + } + if (msAbs >= s) { + return `${Math.round(ms / s)}s`; + } + return `${ms}ms`; +} + +/** + * Long format for `ms`. + */ +function fmtLong(ms: number): StringValue { + const msAbs = Math.abs(ms); + if (msAbs >= d) { + return plural(ms, msAbs, d, "day"); + } + if (msAbs >= h) { + return plural(ms, msAbs, h, "hour"); + } + if (msAbs >= m) { + return plural(ms, msAbs, m, "minute"); + } + if (msAbs >= s) { + return plural(ms, msAbs, s, "second"); + } + return `${ms} ms`; +} + +/** + * Format the given integer as a string. + * + * @param ms - milliseconds + * @param options - Options for the conversion + * @returns The formatted string + */ +export function format(ms: number, options?: Options): string { + if (typeof ms !== "number" || !isFinite(ms)) { + throw new Error("Value provided to ms.format() must be of type number."); + } + return options?.long ? fmtLong(ms) : fmtShort(ms); +} + +/** + * Pluralization helper. + */ +function plural(ms: number, msAbs: number, n: number, name: string): StringValue { + const isPlural = msAbs >= n * 1.5; + return `${Math.round(ms / n)} ${name}${isPlural ? "s" : ""}` as StringValue; +} + +/** + * A type guard for errors. + * + * @param value - The value to test + * @returns A boolean `true` if the provided value is an Error-like object + */ +function isError(value: unknown): value is Error { + return typeof value === "object" && value !== null && "message" in value; +} diff --git a/src/commands/Fun/Music.ts b/src/commands/Fun/Music.ts deleted file mode 100644 index 2698af8..0000000 --- a/src/commands/Fun/Music.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("music") - .setDescription("Command to control music related functions") - .addSubcommand((subcommand) => - subcommand - .setName("play") - .setDescription("Play sound/music from YouTube") - .addStringOption((option) => - option - .setName("query") - .setDescription("A link/search term to the target YouTube video") - .setRequired(true), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("lyrics") - .setDescription("Attempt to search for a song lyrics for the current song or a specific song") - .addStringOption((option) => - option - .setName("songtitle") - .setDescription("Title for a specific song to search for it's lyrics") - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("volume") - .setDescription("Set the volume of the music player") - .addNumberOption((option) => - option - .setName("volumepercentage") - .setDescription( - "Volume percentage (1-100), if not specified then will reply with current volume.", - ) - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("bassboost") - .setDescription("Bass boost filter toggle") - .addBooleanOption((option) => - option - .setName("enabled") - .setDescription( - "Whether bass boost is enabled or not, if not specified, returns the current enabled state.", - ) - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand.setName("currenttrack").setDescription("Gets information about the current track"), - ) - .addSubcommand((subcommand) => subcommand.setName("back").setDescription("Goes back to the previous track")) - .addSubcommand((subcommand) => subcommand.setName("resume").setDescription("Resumes the current track")) - .addSubcommand((subcommand) => subcommand.setName("pause").setDescription("Pause the current track")) - .addSubcommand((subcommand) => subcommand.setName("stop").setDescription("Stop and disconnect the bot")) - .addSubcommand((subcommand) => subcommand.setName("skip").setDescription("Skips the current track")) - .addSubcommand((subcommand) => - subcommand.setName("queue").setDescription("Get the queue of songs in this server"), - ) - .addSubcommand((subcommand) => subcommand.setName("clearqueue").setDescription("Clears the queue")), - async Callback(interaction, client) { - const musicNS = client.Logging.GetNamespace("Command/Music"); - - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - // Check subcommand and queue - const SubcommandTarget = interaction.options.getSubcommand(); - - // Get the current queue node - let Queue = client.DiscordPlayer.nodes.get(interaction.guildId); - - // Fetching guild members - if (!interaction.guild.available) - return await interaction.editReply({ - content: "Guild/server not available. (Is there a outage at the moment?)", - }); - - switch (SubcommandTarget) { - case "play": { - const Query = interaction.options.getString("query", true); - const VoiceChannel = interaction.member.voice.channel; - if (!VoiceChannel) - return await interaction.reply({ - content: "You are not in a voice channel!", - }); - if ( - interaction.guild.members.me?.voice.channelId && - interaction.member.voice.channelId !== interaction.guild.members.me?.voice.channelId - ) - return await interaction.reply({ - content: "You are not in the same voice channel!", - }); - - const SearchResult = await client.DiscordPlayer.search(Query, { - requestedBy: interaction.user, - }); - if (!SearchResult.hasTracks()) - return await interaction.reply({ - content: "Found no tracks for this query.", - }); - - try { - await client.DiscordPlayer.play(VoiceChannel, SearchResult, { - nodeOptions: { - metadata: interaction, - selfDeaf: true, - leaveOnEmpty: true, - leaveOnEnd: true, - }, - }); - } catch (e) { - musicNS.error(`Error occured while trying to play on discord-player: ${e}`); - return await interaction.editReply({ - content: "An error has occured while trying to play your query.", - }); - } - - return await interaction.editReply({ - content: `Now playing your query (${Query})`, - }); - } - case "lyrics": { - const Query = interaction.options.getString("songtitle", false) - ? interaction.options.getString("songtitle", false) - : Queue?.currentTrack?.title; - if (!Query) - return await interaction.editReply({ - content: "No query.", - }); - const lyrics = await client.LyricsExtractor.search(Query).catch(() => null); - if (!lyrics) - return await interaction.editReply({ - content: "No lyrics found.", - }); - const TrimmedLyrics = lyrics.lyrics.substring(0, 1997); - - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle(lyrics.title) - .setURL(lyrics.url) - .setThumbnail(lyrics.thumbnail) - .setAuthor({ - name: lyrics.artist.name, - iconURL: lyrics.artist.image, - url: lyrics.artist.url, - }) - .setDescription(TrimmedLyrics.length === 1997 ? `${TrimmedLyrics}...` : TrimmedLyrics); - - return await interaction.editReply({ embeds: [Embed] }); - } - case "volume": { - const VolumePercentage = interaction.options.getNumber("volumepercentage", false); - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - if (!VolumePercentage) - return await interaction.editReply({ - content: `The current volume is ${Queue.node.volume}%`, - }); - if (VolumePercentage < 0 || VolumePercentage > 100) - return await interaction.editReply({ - content: "The volume must be betweeen 1% and 100%", - }); - Queue.node.setVolume(VolumePercentage); - return await interaction.editReply({ - content: `Set the volume to ${VolumePercentage}%`, - }); - } - case "bassboost": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - const Enabled = interaction.options.getBoolean("enabled", false); - const OldEnabledState = Queue.filters.ffmpeg.getFiltersEnabled().includes("bassboost"); - if (Enabled === null) - return await interaction.editReply({ - content: `Current bass boost enabled state: ${OldEnabledState ? "Enabled" : "Disabled"}`, - }); - Queue.filters.ffmpeg.setFilters(["bassboost", "normalizer2"]); - return await interaction.editReply({ - content: "Enabled bass boost (TODO: Add disabling the effect on v6.x.x upgrade)", - }); - } - case "currenttrack": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - if (!Queue.currentTrack) - return await interaction.editReply({ - content: "The bot isn't playing anything.", - }); - const Track = Queue.currentTrack; - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle(Track.title) - .setDescription(`${Track.author} - ${Track.title}`) - .setThumbnail(Track.thumbnail) - .setURL(Track.url) - .addFields([ - { name: "Author", value: String(Track.author) }, - { name: "Duration", value: String(Track.duration) }, - { - name: "Requested by", - value: String(Track.requestedBy), - }, - { name: "Views", value: String(Track.views) }, - { name: "Id", value: String(Track.id) }, - ]); - return await interaction.editReply({ embeds: [Embed] }); - } - case "back": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - Queue.history.back(); - return await interaction.editReply({ - content: "Now playing the previous track.", - }); - } - case "resume": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - Queue.node.resume(); - return await interaction.editReply({ - content: "Resume the queue.", - }); - } - case "pause": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - Queue.node.pause(); - return await interaction.editReply({ - content: "Paused the queue.", - }); - } - case "stop": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - Queue.delete(); - return await interaction.editReply({ - content: "Stopped and disconnected the bot.", - }); - } - case "skip": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - const AmountOfTracksToSkip = interaction.options.getNumber("amountoftracks", false); - if (!AmountOfTracksToSkip) { - Queue.node.skip(); - return interaction.editReply({ - content: "Skipped the current track.", - }); - } else { - Queue.node.skipTo(AmountOfTracksToSkip); - return interaction.editReply({ - content: `Skipped ${AmountOfTracksToSkip} tracks.`, - }); - } - } - case "queue": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel.", - }); - if (!Queue || (!Queue.node.isPlaying() && Queue.tracks.size === 0)) - return await interaction.editReply({ - content: "The bot isn't playing anything and there is nothing at the queue.", - }); - const CurrentTrack = Queue.currentTrack; - const QueueTracks = Queue.tracks - .toArray() - .slice(0, 25) - .map((track, i) => { - return `${i + 1}. [**${track.title}**](${track.url}) - ${track.requestedBy}`; - }); - - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Music/sound queue") - .setDescription( - `Keep in mind only the first 25 music(s)/sound(s) are listed here:\n${QueueTracks.join("\n")}`, - ); - if (CurrentTrack) - Embed.setFields({ - name: "Currently playing", - value: `[**${CurrentTrack.title}**](${CurrentTrack.url}) - ${CurrentTrack.requestedBy}`, - }); - - return await interaction.editReply({ embeds: [Embed] }); - } - case "clearqueue": { - if (!Queue) - return await interaction.editReply({ - content: "The bot isn't connected to any voice channel or the queue is empty.", - }); - Queue.clear(); - return await interaction.editReply({ - content: "TODO: Rewrite for discord-player v6.x.x", - }); - } - } - return; - }, -}; diff --git a/src/commands/Info/Help.ts b/src/commands/Info/Help.ts deleted file mode 100644 index a2c616e..0000000 --- a/src/commands/Info/Help.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, SlashCommandBuilder, ButtonStyle } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder().setName("help").setDescription("Shows information about the bot"), - async Callback(interaction) { - return await interaction.reply({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("About Meteorium") - .setDescription( - "A Discord bot developed by RadiatedExodus (RealEthanPlayzDev) as a side (and personal) project, written in TypeScript using Node.js and Discord.js, also being used as a way for me to learn TypeScript/JavaScript.", - ), - ], - components: [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setStyle(ButtonStyle.Link) - .setURL("https://github.com/RealEthanPlayzDev/Meteorium") - .setLabel("Open GitHub repository"), - ), - ], - }); - }, -}; diff --git a/src/commands/Info/HolodexAPI.ts b/src/commands/Info/HolodexAPI.ts deleted file mode 100644 index a82edb7..0000000 --- a/src/commands/Info/HolodexAPI.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("holodexapi") - .setDescription("Shows information about a vtuber's content. (Powered by https://holodex.net)") - .addSubcommand((subcommand) => - subcommand - .setName("getchannelinfo") - .setDescription("Gets information about a vtuber's channel") - .addStringOption((option) => - option.setName("channelid").setDescription("The vtuber's YouTube channel id").setRequired(true), - ) - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("If true, the response will be only shown to you") - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("getvideoinfo") - .setDescription("Gets information about a vtuber's video") - .addStringOption((option) => - option.setName("videoid").setDescription("The vtuber's YouTube video id").setRequired(true), - ) - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("If true, the response will be only shown to you") - .setRequired(false), - ), - ), - async Callback(interaction, client) { - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - const SubcommandTarget = interaction.options.getSubcommand(); - switch (SubcommandTarget) { - case "getchannelinfo": { - const Channel = ( - await client.HolodexClient.getChannel(interaction.options.getString("channelid", true)) - ).toRaw(); - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Channel") - .setDescription("A channel") - .setAuthor({ - name: Channel.name, - url: `https://www.youtube.com/channel/${Channel.id}`, - }) - .addFields([ - { - name: "English name", - value: Channel.english_name || Channel.name, - }, - { name: "Organization", value: Channel.org || "None" }, - { - name: "Sub-organization", - value: Channel.suborg || "None", - }, - { - name: "Inactive", - value: Channel.inactive ? "Yes" : "No", - }, - ]); - - if (Channel?.view_count) Embed.addFields([{ name: "Total view count", value: Channel.view_count }]); - if (Channel?.video_count) - Embed.addFields([ - { - name: "Total video count", - value: Channel.video_count, - }, - ]); - if (Channel?.subscriber_count) - Embed.addFields([ - { - name: "Subscribers", - value: Channel.subscriber_count, - }, - ]); - if (Channel?.photo) Embed.setThumbnail(Channel.photo); - - await interaction.editReply({ embeds: [Embed] }); - break; - } - case "getvideoinfo": { - const Video = ( - await client.HolodexClient.getVideo(interaction.options.getString("videoid", true)) - ).toRaw(); - - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Video") - .setDescription("A video") - .setAuthor({ - name: Video.title, - url: `https://www.youtube.com/watch?v=${Video.id}`, - }) - .addFields([ - // Video metadata - { - name: `Video link`, - value: `https://www.youtube.com/watch?v=${Video.id}`, - }, - { name: "Video type", value: Video.type }, - { name: "Status", value: Video.status }, - { - name: "Video topic id", - value: Video.topic_id ? Video.topic_id : "None", - }, - { - name: "Video duration", - value: String(Video.duration), - }, - ]); - - if (Video.songs) { - let Songs = ""; - if (Video.songs) { - for (const Song of Video.songs) { - let str = `Name: ${Song.name}\nOriginal artist: ${Song.original_artist}\niTunes id: ${ - Song.itunesid ? Song.itunesid : "Unknown/None" - }\nSong duration: ${Song.end}`; - if (Songs === "") { - Songs = str; - } else { - Songs += "\n" + str; - } - } - } - if (Songs === "") Songs = "No songs"; - Embed.addFields([ - { - name: `Songs (${Songs.length} total)`, - value: Songs === "" ? "Unable to parse song/No songs available" : Songs, - }, - ]); - } - - Embed.addFields([ - // Anything related to time/date - { - name: "Published at", - value: Video.published_at ? Video.published_at : "Unknown date", - }, - { - name: "Available at", - value: Video.available_at ? Video.available_at : "Unknown date", - }, - ]); - - if (Video.start_scheduled) - Embed.addFields([ - { - name: "Scheduled premiere start time", - value: Video.start_scheduled, - }, - ]); - if (Video.start_actual) - Embed.addFields([ - { - name: "Premiere started at", - value: Video.start_actual, - }, - ]); - if (Video.end_actual) Embed.addFields([{ name: "Premiere ended at", value: Video.end_actual }]); - - Embed.addFields([ - // Channel info - { name: "Channel name", value: Video.channel.name }, - { - name: "Channel English name", - value: Video.channel.english_name - ? Video.channel.english_name - : "No English version of the name", - }, - ]); - - if (Video.channel.org) - Embed.addFields([ - { - name: "Channel organization", - value: Video.channel.org === "" ? "Independent" : Video.channel.org, - }, - ]); - if (Video.channel.suborg) - Embed.addFields([ - { - name: "Channel suborganization", - value: Video.channel.suborg === "" ? "Independent" : Video.channel.suborg, - }, - ]); - - Embed.addFields([ - { - name: "Channel link", - value: `https://www.youtube.com/channel/${Video.channel.id}`, - }, - { - name: "For more information about the channel", - value: "Do the command ``/holodexapi getchannelinfo channelid:" + Video.channel.id + "``", - }, - ]); - - if (Video.channel?.photo) - Embed.setThumbnail(Video.channel.photo === "" ? "No channel photo" : Video.channel.photo); - - await interaction.editReply({ embeds: [Embed] }); - break; - } - } - return; - }, -}; diff --git a/src/commands/Info/MojangAPI.ts b/src/commands/Info/MojangAPI.ts deleted file mode 100644 index 9644d1d..0000000 --- a/src/commands/Info/MojangAPI.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import * as MojangAPI from "../../util/MojangAPI"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("mojangapi") - .setDescription("Get Minecraft information from Mojang APIs") - .addSubcommandGroup((group) => - group - .setName("profile") - .setDescription("Get a user's profile information") - .addSubcommand((subcommand) => - subcommand - .setName("username") - .setDescription("By username") - .addStringOption((option) => - option.setName("username").setDescription("The username of the player").setRequired(true), - ) - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("If true, the response will only be shown to you") - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("uuid") - .setDescription("By uuid") - .addStringOption((option) => - option.setName("uuid").setDescription("The uuid of the player").setRequired(true), - ) - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("If true, the response will only be shown to you") - .setRequired(false), - ), - ), - ), - async Callback(interaction, _) { - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - const SubcommandGroup = interaction.options.getSubcommandGroup(); - const Subcommand = interaction.options.getSubcommand(); - - switch (SubcommandGroup) { - case "profile": { - let UUID: string; - if (Subcommand == "uuid") UUID = interaction.options.getString("uuid", true); - else { - const res = await MojangAPI.getUUIDFromName(interaction.options.getString("name", true)); - if (res.code == 204 || res.code == 404) - return await interaction.editReply("The username/profiile doesn't exist."); - if (res.code != 200) - return await interaction.editReply( - "Failed while fetching information from Mojang's API (username to uuid status code not 200)", - ); - UUID = res.data.id; - } - - const Profile = await MojangAPI.getProfile(UUID); - if (Profile.code != 200) - return await interaction.editReply( - "Failed while fetching information from Mojang's API (username to uuid status code not 200)", - ); - const ProfileTextures = MojangAPI.decodeTexturesB64(Profile.data.properties[0].value); - - const Embed = new MeteoriumEmbedBuilder() - .setNormalColor() - .setTitle(Profile.data.name) - .addFields([ - { name: "UUID", value: Profile.data.id }, - { - name: "Profile actions", - value: - Profile.data.profileActions.length == 0 - ? "N/A" - : Profile.data.profileActions.toString(), - }, - { - name: "Skin url", - value: - ProfileTextures.textures.SKIN == undefined - ? `Default (${MojangAPI.decodeDefaultSkin(UUID)})` - : ProfileTextures.textures.SKIN.url, - }, - { - name: "Skin type", - value: - ProfileTextures.textures.SKIN == undefined - ? "N/A" - : ProfileTextures.textures.SKIN.metadata == undefined - ? "Classic" - : "Slim", - }, - { - name: "Cape url", - value: - ProfileTextures.textures.CAPE == undefined ? "N/A" : ProfileTextures.textures.CAPE.url, - }, - ]); - - return await interaction.editReply({ embeds: [ Embed ] }); - } - } - }, -}; diff --git a/src/commands/Info/Ping.ts b/src/commands/Info/Ping.ts deleted file mode 100644 index cc1f08e..0000000 --- a/src/commands/Info/Ping.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("ping") - .setDescription("Gives you the bot's websocket and roundtrip ping"), - async Callback(interaction, client) { - const DeferredMessage = await interaction.deferReply({ - fetchReply: true, - }); - return interaction.editReply( - `Websocket ping: ${client.ws.ping} ms\nRoundtrip ping: ${ - DeferredMessage.createdTimestamp - interaction.createdTimestamp - } ms`, - ); - }, -}; diff --git a/src/commands/Info/RbxAPI.ts b/src/commands/Info/RbxAPI.ts deleted file mode 100644 index 5f8bb36..0000000 --- a/src/commands/Info/RbxAPI.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -// import { getProductInfo } from 'noblox.js'; -import * as nobloxjs from "noblox.js"; // This is a workaround to solve noblox.js's module export error, see https://github.com/noblox/noblox.js/issues/670 -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("rbxapi") - .setDescription("Shows information about something from Roblox (powered by noblox.js)") - .addSubcommand((subcommand) => - subcommand - .setName("fetchassetinfo") - .setDescription("Gets information about a asset using a asset id") - .addNumberOption((option) => option.setName("assetid").setDescription("The asset id").setRequired(true)) - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("If true, the response will be only shown to you") - .setRequired(false), - ), - ), - async Callback(interaction, _) { - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - const SubcommandTarget = interaction.options.getSubcommand(); - switch (SubcommandTarget) { - case "assetid": { - const AssetInfo = await nobloxjs.getProductInfo(interaction.options.getNumber("assetid", true)); - const EmbedFields = [ - { - name: "Creator", - value: `@${AssetInfo.Creator.Name} (${AssetInfo.Creator.Id})`, - }, - { - name: "AssetId", - value: String(AssetInfo.AssetId ? AssetInfo.AssetId : "N/A"), - }, - { - name: "ProductId", - value: String(AssetInfo.ProductId ? AssetInfo.ProductId : "N/A"), - }, - { - name: "Asset type id", - value: String(AssetInfo.AssetTypeId ? AssetInfo.AssetTypeId : "N/A"), - }, - { - name: "Sales", - value: String(AssetInfo.Sales ? AssetInfo.Sales : "N/A"), - }, - { - name: "Remaining", - value: String(AssetInfo.Remaining ? AssetInfo.Remaining : "N/A"), - }, - { - name: "Created at", - value: String(AssetInfo.Created ? AssetInfo.Created : "N/A"), - }, - { - name: "Last updated at", - value: String(AssetInfo.Updated ? AssetInfo.Updated : "N/A"), - }, - { - name: "Price (in Robux)", - value: String(AssetInfo.PriceInRobux ? AssetInfo.PriceInRobux : "N/A"), - }, - { - name: "On sale", - value: String(AssetInfo.IsForSale ? AssetInfo.IsForSale : "N/A"), - }, - { - name: "Asset is a limited", - value: String(AssetInfo.IsLimited ? AssetInfo.IsLimited : "N/A"), - }, - { - name: "Asset is a unique limited", - value: String(AssetInfo.IsLimitedUnique ? AssetInfo.IsLimitedUnique : "N/A"), - }, - { - name: "IsNew", - value: String(AssetInfo.IsNew ? AssetInfo.IsNew : "N/A"), - }, - { - name: "IsPublicDomain", - value: String(AssetInfo.IsPublicDomain ? AssetInfo.IsPublicDomain : "N/A"), - }, - { - name: "Minimum membership level", - value: String(AssetInfo.MinimumMembershipLevel ? AssetInfo.MinimumMembershipLevel : "N/A"), - }, - { - name: "ContentRatingTypeId", - value: String(AssetInfo.ContentRatingTypeId ? AssetInfo.ContentRatingTypeId : "N/A"), - }, - ]; - - return await interaction.editReply({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle(AssetInfo.Name) - .setDescription(AssetInfo.Description) - .setURL(`https://roblox.com/library/${AssetInfo.AssetId}`) - .addFields(EmbedFields), - ], - }); - } - } - return; - }, -}; diff --git a/src/commands/Info/ServerInfo.ts b/src/commands/Info/ServerInfo.ts deleted file mode 100644 index 2c1ab9f..0000000 --- a/src/commands/Info/ServerInfo.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { SlashCommandBuilder, userMention, time, channelMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("serverinfo") - .setDescription("Returns information about the current server") - .addBooleanOption((option) => - option.setName("ephemeral").setDescription("If true, the response is only shown to you").setRequired(false), - ), - async Callback(interaction, client) { - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - - if (!interaction.guild.available) - return await interaction.reply({ - content: - "The guild isn't available at the moment (usually caused by Discord's servers having a outage). Try again later.", - ephemeral: Ephemeral, - }); - - await interaction.deferReply({ ephemeral: Ephemeral }); - - const Guild = interaction.guild; - const GuildOwner = await client.users.fetch(Guild.ownerId).catch(() => null); - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: Guild.id } }); - const TotalModCases = await client.Database.moderationCase.count({ where: { GuildId: Guild.id } }); - - const MainEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle(Guild.name) - .setDescription(Guild.description != "" ? Guild.description : null) - .setThumbnail(Guild.iconURL({ extension: "png", size: 1024 })) - .setImage(Guild.bannerURL({ extension: "png" })) - .setURL(Guild.vanityURLCode ? `https://discord.gg/${Guild.vanityURLCode}` : "https://discord.com") - .addFields([ - { name: "Server id", value: Guild.id }, - { - name: "Server owner", - value: GuildOwner - ? `${GuildOwner.username} (${userMention(GuildOwner.id)} - ${GuildOwner.id})` - : interaction.guild.ownerId, - }, - { name: "Member count", value: Guild.memberCount.toString() }, - { name: "Created at", value: `${time(Guild.createdAt, "F")} (${time(Guild.createdAt, "R")})` }, - { - name: "Rules channel", - value: Guild.rulesChannelId ? channelMention(Guild.rulesChannelId) : "Not set", - }, - { - name: "Total boosts", - value: Guild.premiumSubscriptionCount ? Guild.premiumSubscriptionCount.toString() : "N/A", - }, - { name: "Server boost tier", value: Guild.premiumTier ? Guild.premiumTier.toString() : "N/A" }, - { name: "Partner server", value: Guild.partnered ? "Yes" : "No" }, - { name: "Verified server", value: Guild.verified ? "Yes" : "No" }, - { - name: "Vanity URL", - value: Guild.vanityURLCode ? `https://discord.gg/${Guild.vanityURLCode}` : "N/A", - }, - { name: "Recorded punishments/cases", value: TotalModCases.toString() }, - { - name: "Ban appeal link", - value: GuildSetting && GuildSetting.BanAppealLink != "" ? GuildSetting.BanAppealLink : "N/A", - }, - ]); - - const SplashDiscoveryUrl = new MeteoriumEmbedBuilder() - .setURL(Guild.vanityURLCode ? `https://discord.gg/${Guild.vanityURLCode}` : "https://discord.com") - .setImage(Guild.discoverySplashURL({ extension: "png" })); - - return await interaction.editReply({ embeds: [MainEmbed, SplashDiscoveryUrl] }); - }, -}; diff --git a/src/commands/Info/Tag.ts b/src/commands/Info/Tag.ts deleted file mode 100644 index 142c43b..0000000 --- a/src/commands/Info/Tag.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import { Tag } from "@prisma/client"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("tag") - .setDescription("Tag suggestion system") - .addSubcommand((subcommand) => - subcommand - .setName("create") - .setDescription("Create a new tag") - .addStringOption((option) => option.setName("name").setDescription("The tag name").setRequired(true)) - .addStringOption((option) => - option.setName("content").setDescription("The content of this tag").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("image") - .setDescription("Optional image to be shown with the tag") - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("delete") - .setDescription("Delete a existing tag") - .addStringOption((option) => - option - .setName("name") - .setDescription("The name of the tag to be deleted") - .setRequired(true) - .setAutocomplete(true), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("edit") - .setDescription("Edit a existing tag") - .addStringOption((option) => - option - .setName("name") - .setDescription("The name of the tag to be edited") - .setRequired(true) - .setAutocomplete(true), - ) - .addStringOption((option) => - option.setName("content").setDescription("The new content of this tag").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("image") - .setDescription("Optional image to be shown with the tag") - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("removeimage") - .setDescription("Set this to true to remove the image (overrides image)") - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("show") - .setDescription("Show a tag") - .addStringOption((option) => - option - .setName("name") - .setDescription("The name of the tag to be shown") - .setRequired(true) - .setAutocomplete(true), - ) - .addUserOption((option) => - option.setName("suggestto").setDescription("Suggest this tag to someone").setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("detach") - .setDescription( - "If true, the tag message will be sent detached from interaction result message", - ) - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("list") - .setDescription("List tags made in this server") - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("If true, list will be shown only to you") - .setRequired(false), - ), - ), - async Callback(interaction, client) { - const Subcommand = interaction.options.getSubcommand(true); - switch (Subcommand) { - case "create": { - const TagName = interaction.options.getString("name", true); // All subcommands require the name field - - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.reply({ content: "You do not have permission to manage tags." }); - - const ExistingTagExist = - (await client.Database.tag.count({ where: { TagName: TagName, GuildId: interaction.guildId } })) != - 0; - if (ExistingTagExist) - return await interaction.reply({ - content: "There is already a existing tag with the same name.", - ephemeral: true, - }); - - const Content = interaction.options.getString("content", true); - const Image = interaction.options.getAttachment("image", false); - - await client.Database.tag.create({ - data: { - TagName: TagName, - GuildId: interaction.guildId, - Content: Content, - Image: Image ? Image.url : "", - }, - }); - - const TagEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ name: "Tag suggestion" }) - .setDescription(Content) - .setImage(Image ? Image?.url : null) - .setColor("Green"); - - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: interaction.guild.id }, - }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Tag created") - .setFields([ - { - name: "Creator", - value: `${interaction.user.username} (${interaction.user.id}) (<@${interaction.user.id}>)`, - }, - { name: "Content", value: Content }, - ]) - .setImage(Image ? Image?.url : null) - .setColor("Green"), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ content: `Created tag with name ${TagName}`, embeds: [TagEmbed] }); - } - case "delete": { - const TagName = interaction.options.getString("name", true); // All subcommands require the name field - - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.reply({ content: "You do not have permission to manage tags." }); - - const Tag = await client.Database.tag.findFirst({ - where: { TagName: TagName, GuildId: interaction.guildId }, - }); - if (!Tag) return await interaction.reply({ content: "Tag does not exist.", ephemeral: true }); - - const TagEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ name: "Tag suggestion" }) - .setDescription(Tag.Content) - .setImage(Tag.Image != "" ? Tag.Image : null) - .setColor("Green"); - - const ActionRow = new ActionRowBuilder().addComponents([ - new ButtonBuilder().setCustomId("yes").setLabel("Yes").setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId("no").setLabel("No").setStyle(ButtonStyle.Danger), - ]); - - const ConfirmationReplyResult = await interaction.reply({ - content: `Are you sure you want to remove the following tag? (${TagName})`, - embeds: [TagEmbed], - components: [ActionRow], - ephemeral: true, - fetchReply: true, - }); - - const ResultCollector = ConfirmationReplyResult.createMessageComponentCollector({ - time: 60000, - max: 1, - }); - ResultCollector.on("collect", async (result) => { - if (result.user.id != interaction.user.id) { - await result.reply({ - content: "You can't interact with this interaction as you were not the original executer!", - ephemeral: true, - }); - return; - } - switch (result.customId) { - case "yes": { - await client.Database.tag.delete({ where: { GlobalTagId: Tag.GlobalTagId } }); - await interaction.editReply({ - content: `Tag ${TagName} deleted.`, - embeds: [], - components: [], - }); - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: interaction.guild.id }, - }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Confirmed tag removal") - .setFields([ - { - name: "Remover", - value: `${interaction.user.username} (${interaction.user.id}) (<@${interaction.user.id}>)`, - }, - { name: "Content", value: Tag.Content }, - ]) - .setImage(Tag.Image != "" ? Tag.Image : null) - .setColor("Red"), - ], - }); - }) - .catch(() => null); - break; - } - case "no": { - await interaction.editReply({ - content: "Cancelled tag deletion.", - embeds: [], - components: [], - }); - break; - } - default: - break; - } - }); - ResultCollector.on("end", async (result) => { - if (result.size < 1) - await interaction.editReply({ content: "Command timed out.", embeds: [], components: [] }); - }); - break; - } - case "edit": { - const TagName = interaction.options.getString("name", true); // All subcommands require the name field - - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.reply({ content: "You do not have permission to manage tags." }); - - const ExistingTagExist = await client.Database.tag.findFirst({ - where: { TagName: TagName, GuildId: interaction.guildId }, - }); - if (!ExistingTagExist) - return await interaction.reply({ - content: "Tag doesn't exist.", - ephemeral: true, - }); - - const Content = interaction.options.getString("content", false); - const Image = interaction.options.getAttachment("image", false); - const RemoveImage = interaction.options.getBoolean("removeimage", false); - - await client.Database.tag.update({ - where: { GlobalTagId: ExistingTagExist.GlobalTagId }, - data: { - Content: Content ? Content : ExistingTagExist.Content, - Image: RemoveImage ? "" : Image ? Image.url : ExistingTagExist.Image, - }, - }); - - const TagEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ name: "Tag suggestion" }) - .setDescription(Content) - .setImage( - RemoveImage - ? null - : Image - ? Image.url - : ExistingTagExist.Image != "" - ? ExistingTagExist.Image - : null, - ) - .setColor("Green"); - - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: interaction.guild.id }, - }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Tag edited") - .setFields([ - { - name: "Editor", - value: `${interaction.user.username} (${interaction.user.id}) (<@${interaction.user.id}>)`, - }, - { - name: "Content", - value: Content ? Content : ExistingTagExist.Content, - }, - ]) - .setImage( - RemoveImage - ? null - : Image - ? Image.url - : ExistingTagExist.Image != "" - ? ExistingTagExist.Image - : null, - ) - .setColor("Yellow"), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ - content: "Edited tag suggestion", - embeds: [TagEmbed], - }); - } - case "show": { - const TagName = interaction.options.getString("name", true); // All subcommands require the name field - - const SuggestToUser = interaction.options.getUser("suggestto", false); - const DetachMessage = interaction.options.getBoolean("detach", false); - - const Tag = await client.Database.tag.findFirst({ - where: { TagName: TagName, GuildId: interaction.guildId }, - }); - if (!Tag) return await interaction.reply({ content: "Tag doesn't exist!", ephemeral: true }); - - const TagEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ - name: SuggestToUser - ? `Tag suggestion for @${SuggestToUser.username} (${SuggestToUser.id})` - : "Tag suggestion", - iconURL: SuggestToUser ? SuggestToUser.displayAvatarURL({ extension: "png" }) : undefined, - }) - .setDescription(Tag.Content) - .setImage(Tag.Image != "" ? Tag.Image : null) - .setColor("Green"); - - if (DetachMessage) { - if (!interaction.channel) - return await interaction.reply({ content: "interaction.channel not set", ephemeral: true }); - await interaction.channel!.send({ - content: SuggestToUser ? `<@${SuggestToUser.id}>` : undefined, - embeds: [TagEmbed], - }); - } - - return await interaction.reply({ - content: DetachMessage - ? "Sent tag suggestion" - : SuggestToUser - ? `<@${SuggestToUser.id}>` - : undefined, - embeds: DetachMessage ? undefined : [TagEmbed], - ephemeral: DetachMessage ? true : false, - }); - } - case "list": { - const Ephemeral = interaction.options.getBoolean("ephemeral", false); - await interaction.deferReply({ ephemeral: Ephemeral ? true : false }); - - const Tags = await client.Database.tag.findMany({ - where: { GuildId: interaction.guildId }, - orderBy: [{ TagName: "asc" }], - }); - - if (Tags.length == 0) - return await interaction.editReply({ content: "This server does not have any tags." }); - - const TagPages: Tag[][] = [[]]; - for (let i = 0; i < Tags.length; i++) { - if ((i + 1) % 10 == 0) TagPages.push([]); - TagPages.at(-1)!.push(Tags[i]!); - } - - const GeneratePageEmbed = (index: number) => { - if (TagPages[index] == undefined) throw Error("invalid page index"); - return new MeteoriumEmbedBuilder() - .setAuthor({ - name: `Available server tags - ${interaction.guild.name}`, - iconURL: interaction.guild.iconURL()!, - }) - .setFields([ - ...TagPages[index]!.map((Tag) => ({ - name: Tag.TagName, - value: Tag.Content, - })), - ]) - .setFooter({ - text: TagPages.length > 1 ? `Page ${index + 1}/${TagPages.length}` : "", - }) - .setTimestamp() - .setNormalColor(); - }; - - const GenerateActionRow = (index: number) => { - return new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId(String(index - 1)) - .setLabel("Previous page") - .setEmoji({ name: "◀️" }) - .setStyle(ButtonStyle.Primary) - .setDisabled(index <= 0), - new ButtonBuilder() - .setCustomId(String(index + 1)) - .setLabel("Next page") - .setEmoji({ name: "▶️" }) - .setStyle(ButtonStyle.Primary) - .setDisabled(index < 0 || index == TagPages.length - 1), - ]); - }; - - const GenerateMessageOptions = (index: number) => ({ - embeds: [GeneratePageEmbed(index)], - components: TagPages.length > 1 ? [GenerateActionRow(index)] : undefined, - fetchReply: true, - }); - - const InitialSendResult = await interaction.editReply(GenerateMessageOptions(0)); - if (TagPages.length <= 1) return; - - const ResultCollector = InitialSendResult.createMessageComponentCollector({ idle: 150000 }); - ResultCollector.on("collect", async (result) => { - if (result.user.id != interaction.user.id) { - await interaction.reply({ - content: "You're not the one who requested this command!", - ephemeral: true, - }); - return; - } - - let Index = -1; - try { - Index = Number(result.customId); - } catch {} - if (Index == -1) { - await interaction.reply({ content: "Invalid page index", ephemeral: true }); - return; - } - - await InitialSendResult.edit(GenerateMessageOptions(+Index)); - }); - ResultCollector.on("end", async () => { - await interaction.editReply({ components: [GenerateActionRow(-1)] }); - }); - - break; - } - default: - break; - } - }, - async Autocomplete(interaction, client) { - const Subcommand = interaction.options.getSubcommand(true); - if (Subcommand != "create") { - const Focus = interaction.options.getFocused(true); - if (Focus.name != "name") return; - const Tags = await client.Database.tag.findMany({ - where: { GuildId: interaction.guildId, TagName: { startsWith: Focus.value } }, - take: 25, - }); - return await interaction.respond(Tags.map((choice) => ({ name: choice.TagName, value: choice.TagName }))); - } - }, -}; diff --git a/src/commands/Info/UserInfo.ts b/src/commands/Info/UserInfo.ts deleted file mode 100644 index f2a3f8b..0000000 --- a/src/commands/Info/UserInfo.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { GuildMember, SlashCommandBuilder, User } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("userinfo") - .setDescription("Returns information about the specified user(s)") - .addUserOption((option) => - option - .setName("user") - .setDescription("The target user, to use userids or multiple users, use the users option instead") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("users").setDescription("The target user(s), accepts userid and user mention"), - ) - .addBooleanOption((option) => - option.setName("ephemeral").setDescription("If true, the response is only shown to you").setRequired(false), - ), - async Callback(interaction, client) { - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - const ParsedUsers = []; - const ParseFailedUsers = []; - const Embeds = []; - let TargetParseUsers = []; - - // If the user option was specified, add it to the target user(s) to be parsed - if (interaction.options.getUser("user", false)) { - TargetParseUsers.push(interaction.options.getUser("user", true).id); - } - - // If the users option was specified, parse it and add the user(s) to the target user(s) to be parsed - if (interaction.options.getString("users", false)) { - TargetParseUsers = TargetParseUsers.concat(interaction.options.getString("users", true).split(",")); - } - - // If nothing is in the target user(s) to be parsed, add the interaction executor's user - if (TargetParseUsers.length === 0) { - TargetParseUsers.push(interaction.user.id); - } - - // Parsing the targets into a member/user object - for (const TargetParseUser of TargetParseUsers) { - let Member; - let User; - try { - Member = - interaction.guild.members.resolve(TargetParseUser.replace(/[<@!>]/g, "")) || - interaction.guild.members.cache.find( - (tMember) => - tMember.user.username.toLowerCase() === TargetParseUser.toLowerCase() || - tMember.displayName.toLowerCase() === TargetParseUser.toLowerCase() || - tMember.user.tag === TargetParseUser.toLowerCase(), - ); - User = client.users.cache.find( - (tUser) => - tUser.username.toLowerCase() === TargetParseUser.toLowerCase() || - tUser.tag.toLowerCase() === TargetParseUser.toLowerCase(), - ); - if (!User) User = await client.users.fetch(TargetParseUser.toLowerCase()); - } catch {} - if (Member) { - ParsedUsers.push(Member); - } else if (User) { - ParsedUsers.push(User); - } else { - ParseFailedUsers.push(TargetParseUser); - } - } - - for (const ParsedUser of ParsedUsers) { - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user); - - if (ParsedUser instanceof GuildMember) { - // User status and is client device parsing - let UserStatus = "Unknown"; - if (ParsedUser.presence && ParsedUser.presence["status"]) { - const ClientStatus = `Desktop: ${ParsedUser.presence.clientStatus?.desktop || "N/A"} | Mobile: ${ - ParsedUser.presence.clientStatus?.mobile || "N/A" - } | Web: ${ParsedUser.presence.clientStatus?.web || "N/A"}`; - if (ParsedUser.presence.status === "dnd") { - UserStatus = `do not disturb - ${ClientStatus}`; - } else { - UserStatus = `${ParsedUser.presence.status} - ${ClientStatus}`; - } - } - - Embed.setDescription(String(ParsedUser)) - .setTitle(ParsedUser.user.tag) - .setAuthor({ - name: "Guild member", - url: `https://discordapp.com/users/${ParsedUser.user.id}`, - }) - .setThumbnail(ParsedUser.user.displayAvatarURL()) - .setColor(ParsedUser.displayColor ? ParsedUser.displayColor : [0, 153, 255]) - .addFields([ - { name: "Status", value: UserStatus }, - { name: "UserId", value: ParsedUser.user.id }, - { - name: "Joined Discord at", - value: `\n${ - ParsedUser.user.createdAt - }\n()`, - }, - ]); - - if (ParsedUser?.joinedTimestamp) { - Embed.addFields([ - { - name: "Joined this server at", - value: `\n()`, - }, - ]); - } - - if (ParsedUser?.premiumSince && ParsedUser?.premiumSinceTimestamp) { - Embed.addFields([ - { - name: "Server Nitro Booster", - value: `${ - ParsedUser.premiumSince - ? `Booster since ()` - : "Not a booster" - }`, - }, - ]); - } - - Embed.addFields([ - { - name: `Roles (${ - ParsedUser.roles.cache.filter((role) => role.name !== "@everyone").size - } in total without @everyone)`, - value: ParsedUser.roles.cache.filter((role) => role.name !== "@everyone").size - ? (() => - ParsedUser.roles.cache - .filter((role) => role.name !== "@everyone") - .sort((role1, role2) => role2.rawPosition - role1.rawPosition) - .map((role) => role) - .join(", "))() - : "———", - }, - ]); - } else if (ParsedUser instanceof User) { - Embed.setDescription(String(ParsedUser)) - .setTitle(ParsedUser.tag) - .setAuthor({ - name: "User", - url: `https://discordapp.com/users/${ParsedUser.id}`, - }) - .setThumbnail(ParsedUser.displayAvatarURL()) - .addFields([ - { name: "UserId", value: ParsedUser.id }, - { - name: "Joined Discord at", - value: `\n${ - ParsedUser.createdAt - }\n()`, - }, - ]); - } - Embeds.push(Embed); - } - return interaction.editReply({ - content: `Successfully parsed ${ParsedUsers.length} users out of ${TargetParseUsers.length} total users (${ParseFailedUsers.length} failed)`, - embeds: Embeds, - }); - }, -}; diff --git a/src/commands/Moderation/Ban.ts b/src/commands/Moderation/Ban.ts deleted file mode 100644 index 2a79805..0000000 --- a/src/commands/Moderation/Ban.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import ms from "ms"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("ban") - .setDescription("Bans someone inside this server and create a new case regarding it") - .addUserOption((option) => option.setName("user").setDescription("The user to be banned").setRequired(true)) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why the user was banned").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addBooleanOption((option) => - option.setName("notappealable").setDescription("If true, this case cannot be appealed").setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ) - .addStringOption((option) => - option - .setName("delmsghistory") - .setDescription("Delete message history time") - .setRequired(false) - .setAutocomplete(true), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("BanMembers")) - return await interaction.reply({ - content: "You do not have permission to ban users from this server.", - }); - - const User = interaction.options.getUser("user", true); - const Reason = interaction.options.getString("reason", true); - const AttachmentProof = interaction.options.getAttachment("proof", false); - const NotAppealable = interaction.options.getBoolean("notappealable", false) || false; - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const DeleteMessageHistory = interaction.options.getString("delmsghistory", false) || undefined; - const GuildUser = await interaction.guild.members.fetch(User).catch(() => null); - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - if (User.id == interaction.user.id) - return await interaction.reply({ content: "You can't ban yourself!", ephemeral: true }); - if (User.bot) - return await interaction.reply({ content: "You can't ban bots! (do it manually)", ephemeral: true }); - if ( - GuildUser && - GuildUser.moderatable && - GuildUser.roles.highest.position >= interaction.member.roles.highest.position - ) - return interaction.reply({ - content: "You (or the bot) can't moderate this user due to lack of permission/hierachy.", - ephemeral: true, - }); - - await interaction.deferReply({ ephemeral: GuildSchema?.PublicModLogChannelId != "" }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: ModerationAction.Ban, - TargetUserId: User.id, - ModeratorUserId: interaction.user.id, - GuildId: interaction.guildId, - Reason: Reason, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - CreatedAt: new Date(), - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - NotAppealable: NotAppealable, - }, - }); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | ban | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(interaction.user.id), - }, - { name: "Reason", value: Reason }, - { name: "Appealable", value: NotAppealable ? "No" : "Yes" }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - const AppealEmbed = new MeteoriumEmbedBuilder(undefined, User) - .setTitle(NotAppealable ? "You cannot appeal your ban." : "Your ban is appealable.") - .setDescription( - NotAppealable - ? "Your ban was marked unappealable, you have been permanently banned." - : GuildSchema.BanAppealLink != "" - ? "You can appeal your ban, use the following link below to appeal." - : "You can appeal your ban, contact a server moderator.", - ); - - if (NotAppealable) AppealEmbed.setErrorColor(); - else { - AppealEmbed.setColor("Yellow"); - if (GuildSchema.BanAppealLink != "") - AppealEmbed.addFields([{ name: "Ban appeal link", value: GuildSchema.BanAppealLink }]); - } - - try { - const DirectMessageChannnel = await User.createDM(); - await DirectMessageChannnel.send({ embeds: [LogEmbed, AppealEmbed] }); - } catch (err) { - client.Logging.GetNamespace("Moderation/Ban").warn(`Could not dm ${User.id}\n${err}`); - } - - const DelMsgHistoryParsed = DeleteMessageHistory ? ms(DeleteMessageHistory) * 1000 : undefined; - await interaction.guild.members.ban(User, { - reason: `Case ${CaseResult.CaseId} by ${interaction.user.username} (${interaction.user.id}): ${Reason}`, - deleteMessageSeconds: DelMsgHistoryParsed - ? DelMsgHistoryParsed >= 604800 - ? 604800 - : DelMsgHistoryParsed - : undefined, - }); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: "Ban" }, - { name: "Reason", value: Reason }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Appealable", value: NotAppealable ? "No" : "Yes" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.editReply({ - content: - PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - }); - }, - async Autocomplete(interaction) { - const Focus = interaction.options.getFocused(true); - if (Focus.name == "delmsghistory") - return await interaction.respond([ - { name: "1 day", value: "1d" }, - { name: "2 days", value: "2d" }, - { name: "3 days", value: "3d" }, - { name: "4 days", value: "4d" }, - { name: "5 days", value: "5d" }, - { name: "6 days", value: "6d" }, - { name: "7 days", value: "7d" }, - ]); - return await interaction.respond([]); - }, -}; diff --git a/src/commands/Moderation/Case.ts b/src/commands/Moderation/Case.ts deleted file mode 100644 index 292a289..0000000 --- a/src/commands/Moderation/Case.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { SlashCommandBuilder, userMention } from "discord.js"; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import { ModerationAction } from "@prisma/client"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("case") - .setDescription("Views a user's case/punishment record") - .addIntegerOption((option) => - option.setName("case").setDescription("The case id for the case to be viewed").setRequired(true), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("ViewAuditLog")) - return await interaction.reply({ - content: "You do not have permission to view a user's punishment/case.", - }); - - const CaseId = interaction.options.getInteger("case", true); - const Case = await client.Database.moderationCase.findFirst({ - where: { CaseId: CaseId, GuildId: interaction.guildId }, - include: { ModerationCaseHistory: { orderBy: { ModerationCaseHistoryId: "asc" } } }, - }); - if (Case == null) - return await interaction.reply({ - content: `Case ${CaseId} does not exist.`, - }); - - const TargetUser = await client.users.fetch(Case.TargetUserId).catch(() => null); - - const ParsedCase = { - Reason: Case.Reason, - AttachmentProof: Case.AttachmentProof, - Duration: Case.Duration, - ModeratorNote: Case.ModeratorNote, - ModeratorAttachment: Case.ModeratorAttachment, - NotAppealable: Case.NotAppealable, - }; - - for (const edit of Case.ModerationCaseHistory) { - ParsedCase.Reason = edit.Reason != null ? edit.Reason : ParsedCase.Reason; - ParsedCase.AttachmentProof = - edit.AttachmentProof != null ? edit.AttachmentProof : ParsedCase.AttachmentProof; - ParsedCase.Duration = edit.Duration != null ? edit.Duration : ParsedCase.Duration; - ParsedCase.ModeratorNote = edit.ModeratorNote != null ? edit.ModeratorNote : ParsedCase.ModeratorNote; - ParsedCase.ModeratorAttachment = - edit.ModeratorAttachment != null ? edit.ModeratorAttachment : ParsedCase.ModeratorAttachment; - ParsedCase.NotAppealable = edit.NotAppealable != null ? edit.NotAppealable : ParsedCase.NotAppealable; - } - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - const ModUser = await interaction.client.users.fetch(Case.ModeratorUserId).catch(() => null); - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("View case") - .setFields([ - { - name: "Detail requester (viewer)", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Case moderator", - value: ModUser - ? `${ModUser.username} (${ModUser.id}) (${userMention(ModUser.id)})` - : `<@${Case.ModeratorUserId}> (${Case.ModeratorUserId})`, - }, - { - name: "Offending user", - value: TargetUser - ? `${TargetUser.username} (${TargetUser.id}) (${userMention( - TargetUser.id, - )})` - : `${userMention(Case.TargetUserId)} (${Case.TargetUserId})`, - }, - { name: "Action", value: String(Case.Action) }, - { name: "Reason", value: ParsedCase.Reason }, - { - name: "Proof", - value: ParsedCase.AttachmentProof ? Case.AttachmentProof : "N/A", - }, - { - name: "Appealable", - value: - Case.Action == ModerationAction.Ban - ? ParsedCase.NotAppealable - ? "No" - : "Yes" - : "Not applicable", - }, - { - name: "Moderator note", - value: ParsedCase.ModeratorNote != "" ? ParsedCase.ModeratorNote : "N/A", - }, - { - name: "Moderator attachment", - value: - ParsedCase.ModeratorAttachment != "" - ? ParsedCase.ModeratorAttachment - : "N/A", - }, - ]) - .setImage(ParsedCase.AttachmentProof != "" ? ParsedCase.AttachmentProof : null) - .setThumbnail( - ParsedCase.ModeratorAttachment != "" ? ParsedCase.ModeratorAttachment : null, - ), - ], - }); - }) - .catch(() => null); - - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseId} | ${Case.Action} | ${ - TargetUser != null ? TargetUser.username : Case.TargetUserId - }`, - iconURL: TargetUser != null ? TargetUser.displayAvatarURL({ extension: "png" }) : undefined, - }) - .addFields( - { name: "User", value: userMention(Case.TargetUserId) }, - { - name: "Moderator", - value: userMention(Case.ModeratorUserId), - }, - { name: "Reason", value: ParsedCase.Reason }, - { name: "Moderator note", value: ParsedCase.ModeratorNote != "" ? ParsedCase.ModeratorNote : "N/A" }, - ) - .setImage(ParsedCase.AttachmentProof == "" ? null : ParsedCase.AttachmentProof) - .setThumbnail(ParsedCase.ModeratorAttachment == "" ? null : ParsedCase.ModeratorAttachment) - .setColor("Red"); - - if (Case.Action == ModerationAction.Ban) - Embed.addFields({ name: "Appealable", value: ParsedCase.NotAppealable ? "No" : "Yes" }); - - return await interaction.reply({ - embeds: [Embed], - }); - }, -}; diff --git a/src/commands/Moderation/CreateCase.ts b/src/commands/Moderation/CreateCase.ts deleted file mode 100644 index 6fc7749..0000000 --- a/src/commands/Moderation/CreateCase.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("createcase") - .setDescription("Creates a new moderation case") - .addStringOption((option) => - option - .setName("action") - .setDescription("The action moderator took") - .setRequired(true) - .addChoices( - { name: "ban", value: "ban" }, - { name: "unban", value: "unban" }, - { name: "kick", value: "kick" }, - { name: "mute", value: "mute" }, - { name: "warn", value: "warn" }, - { name: "tempban", value: "tempban" }, - ), - ) - .addUserOption((option) => - option.setName("user").setDescription("The user that caused this case").setRequired(true), - ) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why this case exists").setRequired(true), - ) - .addUserOption((option) => - option - .setName("moderator") - .setDescription("The moderator who took action against user, if empty defaults to you") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("duration").setDescription("The duration of the temporary ban/mute").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("notappealable") - .setDescription("If true, this case cannot be appealed (bans only)") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("publog") - .setDescription("Send the case details to the public moderation log channel") - .setRequired(false), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("ViewAuditLog")) - return await interaction.reply({ - content: "You do not have permission to manually add cases in this server.", - }); - - const Moderator = interaction.options.getUser("moderator", false) || interaction.user; - const User = interaction.options.getUser("user", true); - const ActionStr = interaction.options.getString("action", true); - const Reason = interaction.options.getString("reason", true); - const Duration = (await interaction.options.getString("duration", false)) || undefined; - const AttachmentProof = interaction.options.getAttachment("proof", false); - const NotAppealable = interaction.options.getBoolean("notappealable", false) || false; - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const SendInPublicModLog = interaction.options.getBoolean("publog", false) || false; - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - if (Moderator.bot) return await interaction.reply({ content: "Moderator can't be a bot!", ephemeral: true }); - if (User.bot) - return await interaction.reply({ content: "Moderators can't take action against bots!", ephemeral: true }); - - let Action: ModerationAction = ModerationAction.Warn; - switch (ActionStr) { - case "ban": { - Action = ModerationAction.Ban; - break; - } - case "unban": { - Action = ModerationAction.Unban; - break; - } - case "kick": { - Action = ModerationAction.Kick; - break; - } - case "mute": { - Action = ModerationAction.Mute; - break; - } - case "warn": { - Action = ModerationAction.Warn; - break; - } - case "tempban": { - Action = ModerationAction.TempBan; - break; - } - default: { - throw new Error("CreateCase switch statement reached impossible conclusion"); - } - } - - if ((Action == ModerationAction.Mute || Action == ModerationAction.TempBan) && !Duration) - interaction.reply({ content: "You need to specify the duration.", ephemeral: true }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: Action, - TargetUserId: User.id, - ModeratorUserId: Moderator.id, - GuildId: interaction.guildId, - Reason: Reason, - Duration: Action == ModerationAction.Mute || Action == ModerationAction.TempBan ? Duration : undefined, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - NotAppealable: Action == ModerationAction.Ban ? NotAppealable : undefined, - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - }, - }); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | ${ActionStr} | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(Moderator.id), - }, - { name: "Reason", value: Reason }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action (create case)") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${Moderator.username} (${Moderator.id}) (${userMention( - Moderator.id, - )})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: ActionStr }, - { name: "Reason", value: Reason }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ - content: - SendInPublicModLog && PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - ephemeral: GuildSchema?.PublicModLogChannelId != "", - }); - }, -}; diff --git a/src/commands/Moderation/EditCase.ts b/src/commands/Moderation/EditCase.ts deleted file mode 100644 index 5c7e885..0000000 --- a/src/commands/Moderation/EditCase.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { SlashCommandBuilder, userMention, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import { ModerationAction } from "@prisma/client"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("editcase") - .setDescription("Edit a user's case/punishment record") - .addIntegerOption((option) => - option.setName("case").setDescription("The case id for the case to be viewed").setRequired(true), - ) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why this case exists").setRequired(false), - ) - .addStringOption((option) => - option.setName("duration").setDescription("The duration of the temporary ban/mute").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("notappealable") - .setDescription("If true, this case cannot be appealed (bans only)") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("Administrator")) - return await interaction.reply({ - content: "You do not have permission to view a user's punishment/case.", - }); - - const NewReason = interaction.options.getString("reason", false) || undefined; - const NewDuration = (await interaction.options.getString("duration", false)) || undefined; - const NewAttachmentProof = interaction.options.getAttachment("proof", false) || undefined; - const NewNotAppealable = interaction.options.getBoolean("notappealable", false) || undefined; - const NewModeratorNote = interaction.options.getString("modnote", false) || undefined; - const NewModeratorAttachment = interaction.options.getAttachment("modattach", false) || undefined; - - if ( - !NewReason && - !NewDuration && - !NewAttachmentProof && - !NewNotAppealable && - !NewModeratorNote && - !NewModeratorAttachment - ) - return await interaction.reply({ - content: "There are no modified fields.", - ephemeral: true, - }); - - const CaseId = interaction.options.getInteger("case", true); - const Case = await client.Database.moderationCase.findFirst({ - where: { CaseId: CaseId, GuildId: interaction.guildId }, - include: { ModerationCaseHistory: { orderBy: { ModerationCaseHistoryId: "asc" } } }, - }); - if (Case == null) - return await interaction.reply({ - content: `Case ${CaseId} does not exist.`, - }); - - const TargetUser = await client.users.fetch(Case.TargetUserId).catch(() => null); - const ParsedCase = { - Reason: Case.Reason, - AttachmentProof: Case.AttachmentProof, - Duration: Case.Duration, - ModeratorNote: Case.ModeratorNote, - ModeratorAttachment: Case.ModeratorAttachment, - NotAppealable: Case.NotAppealable, - }; - - for (const edit of Case.ModerationCaseHistory) { - ParsedCase.Reason = edit.Reason != null ? edit.Reason : ParsedCase.Reason; - ParsedCase.AttachmentProof = - edit.AttachmentProof != null ? edit.AttachmentProof : ParsedCase.AttachmentProof; - ParsedCase.Duration = edit.Duration != null ? edit.Duration : ParsedCase.Duration; - ParsedCase.ModeratorNote = edit.ModeratorNote != null ? edit.ModeratorNote : ParsedCase.ModeratorNote; - ParsedCase.ModeratorAttachment = - edit.ModeratorAttachment != null ? edit.ModeratorAttachment : ParsedCase.ModeratorAttachment; - ParsedCase.NotAppealable = edit.NotAppealable != null ? edit.NotAppealable : ParsedCase.NotAppealable; - } - - const OldCaseEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseId} | ${Case.Action} | ${ - TargetUser != null ? TargetUser.username : Case.TargetUserId - }`, - iconURL: TargetUser != null ? TargetUser.displayAvatarURL({ extension: "png" }) : undefined, - }) - .addFields( - { name: "User", value: userMention(Case.TargetUserId) }, - { - name: "Moderator", - value: userMention(Case.ModeratorUserId), - }, - { name: "Reason", value: ParsedCase.Reason }, - { name: "Moderator note", value: ParsedCase.ModeratorNote != "" ? ParsedCase.ModeratorNote : "N/A" }, - ) - .setImage(ParsedCase.AttachmentProof == "" ? null : ParsedCase.AttachmentProof) - .setThumbnail(ParsedCase.ModeratorAttachment == "" ? null : ParsedCase.ModeratorAttachment) - .setColor("Red"); - - const EditedCaseEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseId} | ${Case.Action} | ${ - TargetUser != null ? TargetUser.username : Case.TargetUserId - }`, - iconURL: TargetUser != null ? TargetUser.displayAvatarURL({ extension: "png" }) : undefined, - }) - .addFields( - { name: "User", value: userMention(Case.TargetUserId) }, - { - name: "Moderator", - value: userMention(Case.ModeratorUserId), - }, - { name: "Reason", value: NewReason ? NewReason : ParsedCase.Reason }, - { - name: "Moderator note", - value: NewModeratorNote - ? NewModeratorNote - : ParsedCase.ModeratorNote != "" - ? ParsedCase.ModeratorNote - : "N/A", - }, - ) - .setImage( - NewAttachmentProof - ? NewAttachmentProof.url - : ParsedCase.AttachmentProof == "" - ? null - : ParsedCase.AttachmentProof, - ) - .setThumbnail( - NewModeratorAttachment - ? NewModeratorAttachment.url - : ParsedCase.ModeratorAttachment == "" - ? null - : ParsedCase.ModeratorAttachment, - ) - .setColor("Red"); - - if (Case.Action == ModerationAction.Ban) { - OldCaseEmbed.addFields({ name: "Appealable", value: ParsedCase.NotAppealable ? "No" : "Yes" }); - EditedCaseEmbed.addFields({ - name: "Appealable", - value: NewNotAppealable != null ? (NewNotAppealable ? "No" : "Yes") : Case.NotAppealable ? "No" : "Yes", - }); - } - - if (Case.Action == ModerationAction.Mute || Case.Action == ModerationAction.TempBan) { - OldCaseEmbed.addFields([{ name: "Duration", value: ParsedCase.Duration }]); - EditedCaseEmbed.addFields([{ name: "Duration", value: NewDuration ? NewDuration : ParsedCase.Duration }]); - } - - const ActionRow = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("yes").setLabel("Yes").setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId("no").setLabel("No").setStyle(ButtonStyle.Danger), - ); - - const ConfirmationInteractionResult = await interaction.reply({ - content: "Are you sure you want to edit this punishment?", - components: [ActionRow], - embeds: [OldCaseEmbed, EditedCaseEmbed], - ephemeral: true, - fetchReply: true, - }); - - const RowResultCollector = ConfirmationInteractionResult.createMessageComponentCollector({ - time: 60000, - max: 1, - }); - RowResultCollector.on("collect", async (result) => { - switch (result.customId) { - case "yes": { - const SuccessEditEmbed = new MeteoriumEmbedBuilder() - .setTitle("Case edited") - .setDescription(`Case ${CaseId} edited. (Edit #${Case.ModerationCaseHistory.length + 1})`) - .setColor("Green"); - - await client.Database.moderationCaseHistory.create({ - data: { - GlobalCaseId: Case.GlobalCaseId, - Editor: interaction.user.id, - Reason: NewReason, - AttachmentProof: NewAttachmentProof ? NewAttachmentProof.url : undefined, - Duration: NewDuration, - ModeratorNote: NewModeratorNote, - ModeratorAttachment: NewModeratorAttachment ? NewModeratorAttachment.url : undefined, - NotAppealable: NewNotAppealable, - }, - }); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${Case.CaseId} | ${String(Case.Action).toLowerCase()} | ${ - TargetUser != null ? TargetUser.username : Case.TargetUserId - }`, - iconURL: TargetUser != null ? TargetUser.displayAvatarURL({ extension: "png" }) : undefined, - }) - .addFields( - { name: "User", value: userMention(Case.TargetUserId) }, - { - name: "Moderator", - value: userMention(Case.ModeratorUserId), - }, - { name: "Reason", value: NewReason ? NewReason : ParsedCase.Reason }, - ) - .setImage( - NewAttachmentProof - ? NewAttachmentProof.url - : ParsedCase.AttachmentProof == "" - ? null - : ParsedCase.AttachmentProof, - ) - .setFooter({ text: `Id: ${Case.TargetUserId}` }) - .setTimestamp() - .setColor("Red"); - - if (Case.Action == ModerationAction.Ban) - LogEmbed.addFields({ - name: "Appealable", - value: - NewNotAppealable != null - ? NewNotAppealable - ? "No" - : "Yes" - : Case.NotAppealable - ? "No" - : "Yes", - }); - - if (Case.Action == ModerationAction.Mute || Case.Action == ModerationAction.TempBan) - LogEmbed.addFields([ - { name: "Duration", value: NewDuration ? NewDuration : ParsedCase.Duration }, - ]); - - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: interaction.guild.id }, - }); - - let Content = ""; - if (Case.PublicModLogMsgId != "" && GuildSetting && GuildSetting.PublicModLogChannelId != "") { - const Channel = - (await interaction.guild.channels.fetch(GuildSetting.PublicModLogChannelId)) || undefined; - const Message = - Channel && Channel.isTextBased() - ? await Channel.messages.fetch(Case.PublicModLogMsgId) - : undefined; - if (Message && Message.editable) await Message.edit({ embeds: [LogEmbed] }); - else Content = "(Warning: could not edit public mod log message)"; - } - - await interaction.editReply({ - content: Content, - embeds: [SuccessEditEmbed, LogEmbed], - components: [], - }); - - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - const ModUser = await interaction.client.users - .fetch(Case.ModeratorUserId) - .catch(() => null); - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Confirmed user case/punishment record editing") - .setFields([ - { - name: "Editor", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Case moderator", - value: ModUser - ? `${ModUser.username} (${ModUser.id}) (${userMention( - ModUser.id, - )})` - : `${userMention(Case.ModeratorUserId)} (${ - Case.ModeratorUserId - })`, - }, - { - name: "Offending user", - value: TargetUser - ? `${TargetUser.username} (${TargetUser.id}) (${userMention( - TargetUser.id, - )})` - : `${userMention(Case.TargetUserId)} (${ - Case.TargetUserId - })`, - }, - { name: "Action", value: String(Case.Action) }, - { - name: "Reason", - value: NewReason ? NewReason : ParsedCase.Reason, - }, - { - name: "Proof", - value: NewAttachmentProof - ? NewAttachmentProof.url - : ParsedCase.AttachmentProof != "" - ? ParsedCase.AttachmentProof - : "N/A", - }, - { - name: "Appealable", - value: - Case.Action == ModerationAction.Ban - ? Case.NotAppealable - ? "No" - : "Yes" - : "Not applicable", - }, - { - name: "Duration", - value: - Case.Action == ModerationAction.TempBan || - Case.Action == ModerationAction.Mute - ? NewNotAppealable != null - ? NewNotAppealable - ? "No" - : "Yes" - : ParsedCase.NotAppealable - ? "No" - : "Yes" - : "Not applicable", - }, - { - name: "Moderator note", - value: NewModeratorNote - ? NewModeratorNote - : ParsedCase.ModeratorNote != "" - ? ParsedCase.ModeratorNote - : "N/A", - }, - ]) - .setImage( - NewAttachmentProof - ? NewAttachmentProof.url - : ParsedCase.AttachmentProof == "" - ? null - : ParsedCase.AttachmentProof, - ) - .setThumbnail( - NewModeratorAttachment - ? NewModeratorAttachment.url - : ParsedCase.ModeratorAttachment == "" - ? null - : ParsedCase.ModeratorAttachment, - ) - .setColor("Red"), - ], - }); - }) - .catch(() => null); - - break; - } - case "no": { - await interaction.editReply({ - content: "Cancelled user punishment/case record editing.", - embeds: [], - components: [], - }); - break; - } - default: - break; - } - }); - RowResultCollector.on("end", async (result) => { - if (result.size < 1) - await interaction.editReply({ content: "Command timed out.", embeds: [], components: [] }); - }); - }, -}; diff --git a/src/commands/Moderation/Kick.ts b/src/commands/Moderation/Kick.ts deleted file mode 100644 index 01b67ed..0000000 --- a/src/commands/Moderation/Kick.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("kick") - .setDescription("Kicks someone inside this server and create a new case regarding it") - .addUserOption((option) => option.setName("user").setDescription("The user to be kicked").setRequired(true)) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why the user was kicked").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("KickMembers")) - return await interaction.editReply({ - content: "You do not have permission to kick users from this server.", - }); - - const User = interaction.options.getUser("user", true); - const Reason = interaction.options.getString("reason", true); - const AttachmentProof = interaction.options.getAttachment("proof", false); - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const GuildUser = await interaction.guild.members.fetch(User).catch(() => null); - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - if (User.id == interaction.user.id) - return await interaction.reply({ content: "You can't kick yourself!", ephemeral: true }); - if (User.bot) - return await interaction.reply({ content: "You can't kick bots! (do it manually)", ephemeral: true }); - if ( - !GuildUser || - !GuildUser.moderatable || - GuildUser.roles.highest.position >= interaction.member.roles.highest.position - ) - return interaction.reply({ - content: "You (or the bot) can't moderate this user due to lack of permission/hierachy.", - ephemeral: true, - }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: ModerationAction.Kick, - TargetUserId: User.id, - ModeratorUserId: interaction.user.id, - GuildId: interaction.guildId, - Reason: Reason, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - CreatedAt: new Date(), - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - }, - }); - await interaction.guild.members.kick( - User, - `Case ${CaseResult.CaseId} by ${interaction.user.username} (${interaction.user.id}): ${Reason}`, - ); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | kick | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(interaction.user.id), - }, - { name: "Reason", value: Reason }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: "Kick" }, - { name: "Reason", value: Reason }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ - content: - PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - ephemeral: GuildSchema?.PublicModLogChannelId != "", - }); - }, -}; diff --git a/src/commands/Moderation/Mute.ts b/src/commands/Moderation/Mute.ts deleted file mode 100644 index ddae366..0000000 --- a/src/commands/Moderation/Mute.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import ms from "ms"; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("mute") - .setDescription("Mutes someone inside this server and create a new case regarding it") - .addUserOption((option) => option.setName("user").setDescription("The user to be muted").setRequired(true)) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why the user was muted").setRequired(true), - ) - .addStringOption((option) => - option.setName("duration").setDescription("The duration of the mute").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.editReply({ - content: "You do not have permission to mute users from this server.", - }); - - const User = interaction.options.getUser("user", true); - const Reason = interaction.options.getString("reason", true); - const Duration = interaction.options.getString("duration", true); - const AttachmentProof = interaction.options.getAttachment("proof", false); - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const Timeout = ms(Duration); - const GuildUser = await interaction.guild.members.fetch(User).catch(() => null); - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - if (Timeout < 1) return await interaction.reply({ content: "Invalid mute duration", ephemeral: true }); - if (Timeout >= ms("29d")) - return await interaction.reply({ - content: - "It is not possible to mute a user longer than 29 days. Please specify a duration below 29 days.", - ephemeral: true, - }); - if (User.id == interaction.user.id) - return await interaction.reply({ content: "You can't mute yourself!", ephemeral: true }); - if (User.bot) return await interaction.reply({ content: "You can't mute bots!", ephemeral: true }); - if ( - !GuildUser || - !GuildUser.moderatable || - GuildUser.roles.highest.position >= interaction.member.roles.highest.position - ) - return interaction.reply({ - content: "You (or the bot) can't moderate this user due to lack of permission/hierachy.", - ephemeral: true, - }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: ModerationAction.Mute, - TargetUserId: User.id, - ModeratorUserId: interaction.user.id, - GuildId: interaction.guildId, - Reason: Reason, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - Duration: Duration, - CreatedAt: new Date(), - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - }, - }); - await GuildUser.timeout( - Timeout, - `Case ${CaseResult.CaseId} by ${interaction.user.username} (${interaction.user.id}): ${Reason}`, - ); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | mute | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(interaction.user.id), - }, - { name: "Reason", value: Reason }, - { name: "Duration", value: Duration }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: "Mute" }, - { name: "Reason", value: Reason }, - { name: "Duration", value: `${Duration} (${Timeout})` }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ - content: - PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - ephemeral: GuildSchema?.PublicModLogChannelId != "", - }); - }, -}; diff --git a/src/commands/Moderation/Punishments.ts b/src/commands/Moderation/Punishments.ts deleted file mode 100644 index 65115fe..0000000 --- a/src/commands/Moderation/Punishments.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ModerationAction, ModerationCase } from "@prisma/client"; -import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("punishments") - .setDescription("List all moderation cases/punishments a user has") - .addUserOption((option) => option.setName("user").setDescription("The user").setRequired(true)), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("ViewAuditLog")) - return await interaction.editReply({ - content: "You do not have permission to view user punishments.", - }); - - const User = interaction.options.getUser("user", true); - if (User.bot) - return await interaction.reply({ - content: "Bots cannot be moderated, they can't have any moderation records.", - }); - - await interaction.deferReply(); - - const Punishments = await client.Database.moderationCase.findMany({ - where: { TargetUserId: User.id, GuildId: interaction.guildId }, - orderBy: [{ CaseId: "desc" }], - }); - - let TotalKick = 0, - TotalWarn = 0, - TotalBan = 0, - TotalMute = 0; - - if (Punishments.length == 0) { - return await interaction.editReply({ - embeds: [ - new MeteoriumEmbedBuilder() - .setAuthor({ - name: User.username, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .setDescription("This user has no recorded punishments/cases.") - .setTimestamp() - .setNormalColor(), - ], - }); - } else { - const PunishmentPages: ModerationCase[][] = [[]]; - for (let i = 0; i < Punishments.length; i++) { - const Case = Punishments[i]!; - if ((i + 1) % 10 == 0) PunishmentPages.push([]); - PunishmentPages.at(-1)!.push(Case); - switch (Case.Action) { - case ModerationAction.Ban: { - TotalBan++; - break; - } - case ModerationAction.TempBan: { - TotalBan++; - break; - } - case ModerationAction.Kick: { - TotalKick++; - break; - } - case ModerationAction.Mute: { - TotalMute++; - break; - } - case ModerationAction.Warn: { - TotalWarn++; - break; - } - default: - break; - } - } - - const GeneratePageEmbed = (index: number) => { - if (PunishmentPages[index] == undefined) throw Error("invalid page index"); - return new MeteoriumEmbedBuilder() - .setAuthor({ - name: User.username, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .setFields([ - ...PunishmentPages[index]!.map((Case) => ({ - name: `Case ${Case.CaseId} - ${Case.Action}`, - value: Case.Reason, - })), - ]) - .setFooter({ - text: `${ - PunishmentPages.length > 1 ? `Page ${index + 1}/${PunishmentPages.length} | ` : "" - }Warned: ${TotalWarn} | Muted: ${TotalMute} | Kicked: ${TotalKick} | Banned: ${TotalBan}`, - }) - .setNormalColor(); - }; - - const GenerateActionRow = (index: number) => { - return new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId(String(index - 1)) - .setLabel("Previous page") - .setEmoji({ name: "◀️" }) - .setStyle(ButtonStyle.Primary) - .setDisabled(index <= 0), - new ButtonBuilder() - .setCustomId(String(index + 1)) - .setLabel("Next page") - .setEmoji({ name: "▶️" }) - .setStyle(ButtonStyle.Primary) - .setDisabled(index < 0 || index == PunishmentPages.length - 1), - ]); - }; - - const GenerateMessageOptions = (index: number) => ({ - embeds: [GeneratePageEmbed(index)], - components: PunishmentPages.length > 1 ? [GenerateActionRow(index)] : undefined, - fetchReply: true, - }); - - const InitialSendResult = await interaction.editReply(GenerateMessageOptions(0)); - if (PunishmentPages.length <= 1) return; - - const ResultCollector = InitialSendResult.createMessageComponentCollector({ idle: 150000 }); - ResultCollector.on("collect", async (result) => { - if (result.user.id != interaction.user.id) { - await interaction.reply({ - content: "You're not the one who requested this command!", - ephemeral: true, - }); - return; - } - - let Index = -1; - try { - Index = Number(result.customId); - } catch {} - if (Index == -1) { - await interaction.reply({ content: "Invalid page index", ephemeral: true }); - return; - } - - await InitialSendResult.edit(GenerateMessageOptions(+Index)); - }); - ResultCollector.on("end", async () => { - await interaction.editReply({ components: [GenerateActionRow(-1)] }); - }); - } - }, -}; diff --git a/src/commands/Moderation/ReactTo.ts b/src/commands/Moderation/ReactTo.ts deleted file mode 100644 index 1627340..0000000 --- a/src/commands/Moderation/ReactTo.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ChannelType, SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("reactto") - .setDescription("Reacts to a message") - .addStringOption((option) => - option - .setName("messageid") - .setDescription("The message where the bot will react to (message id)") - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("emoji") - .setDescription( - "The emoji to be used on reacting to the message (supports both emoji id and normal emoji name)", - ) - .setRequired(true), - ) - .addBooleanOption((option) => - option.setName("ephemeral").setDescription("If true, any interaction feedbacks will be only shown to you"), - ) - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The text/thread channel where the message you want to react to is located") - .addChannelTypes(ChannelType.GuildText), - ), - async Callback(interaction, client) { - const reactToNS = client.Logging.GetNamespace("Commands/ReactTo"); - - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.editReply({ content: "You do not have permission to use ReactTo." }); - - const Channel = interaction.options.getChannel("channel", false) - ? interaction.options.getChannel("channel", false) - : interaction.channel; - const MessageId = interaction.options.getString("messageid", true); - const Emoji = interaction.options.getString("emoji", true); - - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.editReply({ - content: "You do not have permission to use this command. (Missing ManageMessages permission)", - }); - if (!Channel) - return await interaction.editReply({ - content: "No channel? Please try again later", - }); - if (!Channel.isTextBased()) - return await interaction.editReply({ - content: "Please provide a text-based channel!", - }); - - let TargetMessage, TargetEmoji; - try { - TargetMessage = await Channel.messages.fetch(MessageId); - } catch (e) { - reactToNS.error(`Error while getting message: ${e}`); - } - try { - if (interaction.guild.emojis.cache.get(Emoji)) { - TargetEmoji = interaction.guild.emojis.cache.get(Emoji); - } else if (interaction.guild.emojis.cache.find((emoji) => Emoji === emoji.name)) { - TargetEmoji = interaction.guild.emojis.cache.find((emoji) => Emoji === emoji.name); - } else { - TargetEmoji = undefined; - } - } catch (e) { - reactToNS.error(`Error while getting emoji: ${e}`); - } - - if (!TargetMessage) - return await interaction.editReply({ - content: `Cannot find message "${MessageId}" in channel <#${Channel.id}> (${Channel.id})`, - }); - if (!TargetEmoji) - return await interaction.editReply({ - content: "Cannot find emoji in cache", - }); - - await TargetMessage.react(TargetEmoji); - return await interaction.editReply({ - content: "Successfully reacted to message", - }); - }, -}; diff --git a/src/commands/Moderation/RemoveCase.ts b/src/commands/Moderation/RemoveCase.ts deleted file mode 100644 index e88b1d6..0000000 --- a/src/commands/Moderation/RemoveCase.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, userMention } from "discord.js"; -import { ModerationAction } from "@prisma/client"; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("removecase") - .setDescription("Removes a user punishment/case") - .addIntegerOption((option) => - option.setName("case").setDescription("The user punishment/case record to be removed").setRequired(true), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("ViewAuditLog")) - return await interaction.editReply({ - content: "You do not have permission to remove user punishments.", - }); - - const CaseId = interaction.options.getInteger("case", true); - - const Case = await client.Database.moderationCase.findFirst({ - where: { CaseId: CaseId, GuildId: interaction.guildId }, - }); - if (Case == null) return await interaction.reply({ content: `Case ${CaseId} does not exist.` }); - - const TargetUser = await client.users.fetch(Case.TargetUserId).catch(() => null); - - const ActionRow = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("yes").setLabel("Yes").setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId("no").setLabel("No").setStyle(ButtonStyle.Danger), - ); - - const ConfirmationEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ - name: `Case: #${CaseId} | ${Case.Action} | ${ - TargetUser != null ? TargetUser.username : Case.TargetUserId - }`, - iconURL: TargetUser != null ? TargetUser.displayAvatarURL({ extension: "png" }) : undefined, - }) - .addFields( - { name: "User", value: userMention(Case.TargetUserId) }, - { - name: "Moderator", - value: userMention(Case.ModeratorUserId), - }, - { name: "Reason", value: Case.Reason }, - { name: "Moderator note", value: Case.ModeratorNote != "" ? Case.ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: Case.ModeratorAttachment != "" ? Case.ModeratorAttachment : "N/A", - }, - ) - .setImage(Case.AttachmentProof != "" ? Case.AttachmentProof : null) - .setThumbnail(Case.ModeratorAttachment != "" ? Case.ModeratorAttachment : null) - .setFooter({ text: `Id: ${Case.TargetUserId}` }) - .setTimestamp() - .setColor("Red"); - - if (Case.Action == ModerationAction.Mute) - ConfirmationEmbed.addFields([{ name: "Duration", value: Case.Duration }]); - - if (Case.Action == ModerationAction.Ban) - ConfirmationEmbed.addFields({ name: "Appealable", value: Case.NotAppealable ? "No" : "Yes" }); - - const ConfirmationInteractionResult = await interaction.reply({ - content: "Are you sure you want to remove this punishment?", - components: [ActionRow], - embeds: [ConfirmationEmbed], - ephemeral: true, - fetchReply: true, - }); - - const RowResultCollector = ConfirmationInteractionResult.createMessageComponentCollector({ - time: 60000, - max: 1, - }); - RowResultCollector.on("collect", async (result) => { - switch (result.customId) { - case "yes": { - const SuccessDeleteEmbed = new MeteoriumEmbedBuilder() - .setTitle("Case removed") - .setDescription(`Case ${CaseId} removed.`) - .setColor("Green"); - - await client.Database.moderationCase.delete({ where: { GlobalCaseId: Case.GlobalCaseId } }); - if (Case.Action == ModerationAction.Mute) { - const GuildUser = await interaction.guild.members.fetch(Case.TargetUserId).catch(() => null); - if (GuildUser) - await GuildUser.timeout( - null, - `Case ${CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, - ); - } else if (Case.Action == ModerationAction.Ban) - await interaction.guild.members.unban( - Case.TargetUserId, - `Case ${CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, - ); - else if (Case.Action == ModerationAction.TempBan) { - await interaction.guild.members.unban( - Case.TargetUserId, - `Case ${CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, - ); - const ATB = await client.Database.activeTempBans.findFirst({ - where: { GlobalCaseId: Case.GlobalCaseId }, - }); - if (ATB) - await client.Database.activeTempBans.delete({ - where: { ActiveTempBanId: ATB.ActiveTempBanId }, - }); - } - - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: interaction.guild.id }, - }); - - let Content = ""; - if (Case.PublicModLogMsgId != "" && GuildSetting && GuildSetting.PublicModLogChannelId != "") { - const Channel = - (await interaction.guild.channels.fetch(GuildSetting.PublicModLogChannelId)) || undefined; - const Message = - Channel && Channel.isTextBased() - ? await Channel.messages.fetch(Case.PublicModLogMsgId) - : undefined; - if (Message && Message.deletable) await Message.delete(); - else Content = "(Warning: could not delete public mod log message)"; - } - - await interaction.editReply({ content: Content, embeds: [SuccessDeleteEmbed], components: [] }); - - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - const ModUser = await interaction.client.users - .fetch(Case.ModeratorUserId) - .catch(() => null); - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Confirmed user case/punishment record removal") - .setFields([ - { - name: "Remover", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Case moderator", - value: ModUser - ? `${ModUser.username} (${ModUser.id}) (${userMention( - ModUser.id, - )})` - : `${userMention(Case.ModeratorUserId)} (${ - Case.ModeratorUserId - })`, - }, - { - name: "Offending user", - value: TargetUser - ? `${TargetUser.username} (${TargetUser.id}) (${userMention( - TargetUser.id, - )})` - : `${userMention(Case.TargetUserId)} (${ - Case.TargetUserId - })`, - }, - { name: "Action", value: String(Case.Action) }, - { name: "Reason", value: Case.Reason }, - { - name: "Proof", - value: - Case.AttachmentProof != "" ? Case.AttachmentProof : "N/A", - }, - { - name: "Appealable", - value: - Case.Action == ModerationAction.Ban - ? Case.NotAppealable - ? "No" - : "Yes" - : "Not applicable", - }, - { name: "Moderator note", value: Case.ModeratorNote }, - { - name: "Moderator attachment", - value: - Case.ModeratorAttachment != "" - ? Case.ModeratorAttachment - : "N/A", - }, - ]) - .setImage(Case.AttachmentProof != "" ? Case.AttachmentProof : null) - .setThumbnail( - Case.ModeratorAttachment != "" ? Case.ModeratorAttachment : null, - ) - .setColor("Red"), - ], - }); - }) - .catch(() => null); - break; - } - case "no": { - await interaction.editReply({ - content: "Cancelled user punishment/case record removal.", - embeds: [], - components: [], - }); - break; - } - default: - break; - } - }); - RowResultCollector.on("end", async (result) => { - if (result.size < 1) - await interaction.editReply({ content: "Command timed out.", embeds: [], components: [] }); - }); - - return; - }, -}; diff --git a/src/commands/Moderation/SayIn.ts b/src/commands/Moderation/SayIn.ts deleted file mode 100644 index 481b9b1..0000000 --- a/src/commands/Moderation/SayIn.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ChannelType, SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("sayin") - .setDescription("Says a message in a channel") - .addStringOption((option) => - option.setName("message").setDescription("The message to be sent").setRequired(true), - ) - .addChannelOption((option) => - option - .setName("channel") - .setDescription( - "Optional channel where message will be sent (if not specified it will be sent to the current channel", - ) - .addChannelTypes(ChannelType.GuildText), - ) - .addBooleanOption((option) => - option - .setName("showexecutorname") - .setDescription( - "Show the executor name or not (can be overriden by EnforceSayinExecutor, doesn't include admins)", - ), - ) - .addBooleanOption((option) => - option - .setName("ephemeral") - .setDescription("Shows the success message or not (if false then success message only shows to you)"), - ) - .addStringOption((option) => - option - .setName("replyto") - .setDescription("Fill with the message id of the target message you want to reply to"), - ), - async Callback(interaction, client) { - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.editReply({ content: "You do not have permission to use SayIn." }); - - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: String(interaction.guildId) }, - }); - if (!GuildSetting) - return await interaction.editReply({ - content: "No guild setting inside database?", - }); - - const ShowExecutorName = - GuildSetting.EnforceSayInExecutor && !interaction.member.permissions.has("Administrator", true) - ? true - : interaction.options.getBoolean("showexecutorname", false) == false - ? false - : true; - const Message = ShowExecutorName - ? `${interaction.options.getString("message", true)}\n\n(Sayin command executed by ${ - interaction.user.tag - } (${interaction.user.id}))` - : interaction.options.getString("message", true); - const Channel = interaction.options.getChannel("channel", false) - ? interaction.options.getChannel("channel") - : interaction.channel; - const ReplyTarget = interaction.options.getString("replyto", false); - - if (!ShowExecutorName && !interaction.member.permissions.has("Administrator")) - return await interaction.editReply({ - content: - "You do not have permission to not show the executor's name. (Requires Administrator permission to be imnune)", - }); - if (!Channel) - return await interaction.editReply({ - content: "No channel? Please try again later.", - }); - if (!Channel.isTextBased()) - return await interaction.editReply({ - content: "Please specify a text-based channel!", - }); - - await Channel.send({ - content: Message, - reply: ReplyTarget ? { messageReference: ReplyTarget } : undefined, - }); - - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel != null && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("SayIn execution info") - .setFields([ - { - name: "Executor", - value: `${interaction.user.username} (${interaction.user.id}) (<@${interaction.user.id}>)`, - }, - { name: "Executor name shown", value: ShowExecutorName ? "Yes" : "No" }, - { - name: "SayIn executor name shown enforcement", - value: GuildSetting.EnforceSayInExecutor ? "Enforcing" : "Permissive", - }, - { name: "Message", value: interaction.options.getString("message", true) }, - { name: "Channel", value: `<#${Channel.id}> (${Channel.id})` }, - { name: "Reply target", value: ReplyTarget ? ReplyTarget : "N/A" }, - ]) - .setColor("Yellow"), - ], - }); - }) - .catch(() => null); - - return await interaction.editReply({ - content: "Successfully sent message.", - }); - }, -}; diff --git a/src/commands/Moderation/Settings.ts b/src/commands/Moderation/Settings.ts deleted file mode 100644 index b78bde3..0000000 --- a/src/commands/Moderation/Settings.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { ChannelType, SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("settings") - .setDescription("Bot configuration utility command") - .addSubcommandGroup((subcommandgroup) => - subcommandgroup - .setName("generalmoderation") - .setDescription("Moderation-related functionality configuration") - .addSubcommand((subcommand) => - subcommand - .setName("enforcesayinexecutor") - .setDescription( - "If true, /sayin command will enforce telling the executor's name no matter what. (Admins are imnune)", - ) - .addBooleanOption((option) => - option.setName("enabled").setDescription("Enabled or not").setRequired(true), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("logchannel") - .setDescription("The channel where command verbose logging will be sent at") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel where verbose logging will be sent at") - .setRequired(true) - .addChannelTypes(ChannelType.GuildText), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("publicmodlogchannel") - .setDescription( - "The channel where moderation logs will be sent at (instead of the current chat)", - ) - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel where moderation logs will be sent at") - .setRequired(true) - .addChannelTypes(ChannelType.GuildText), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("joinleavelogchannel") - .setDescription("The channel where join and leave logs will be sent at") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel where moderation logs will be sent at") - .setRequired(true) - .addChannelTypes(ChannelType.GuildText), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("banappeallink") - .setDescription( - "The ban appeals link, sent to people who got banned in dms if they can appeal.", - ) - .addStringOption((option) => - option - .setName("link") - .setDescription("The link for the ban appeal form. Don't specify to unset.") - .setRequired(false), - ), - ), - ) - .addSubcommandGroup((subcommandgroup) => - subcommandgroup - .setName("disabledcommands") - .setDescription("Configuration for disabled commands at this server") - .addSubcommand((subcommand) => - subcommand - .setName("add") - .setDescription("List of commands (seperated in commas ',' if multiple) that will be disabled.") - .addStringOption((option) => - option - .setName("commands") - .setDescription("The command(s) (seperated in commas ',' if multiple)") - .setRequired(true), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("remove") - .setDescription("List of commands (seperated in commas ',' if multiple) that will be enabled.") - .addStringOption((option) => - option - .setName("commands") - .setDescription("The command(s) (seperated in commas ',' if multiple)") - .setRequired(true), - ), - ) - .addSubcommand((subcommand) => - subcommand.setName("list").setDescription("Returns a list of commands that are disabled."), - ), - ), - async Callback(interaction, client) { - const settingsNS = client.Logging.GetNamespace("Commands/Settings"); - - const Ephemeral = interaction.options.getBoolean("ephemeral", false) ? true : false; - await interaction.deferReply({ ephemeral: Ephemeral }); - - // Permission check - if (!interaction.member.permissions.has("Administrator", true)) { - return await interaction.editReply({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Cannot configure the bot") - .setDescription("You do not have the administrator permissions to configure the bot.") - .setErrorColor(), - ], - }); - } - - // Getting the schema for this guild from the database - const GuildSchema = await client.Database.guild.findUnique({ - where: { GuildId: interaction.guildId }, - }); - if (!GuildSchema) - return await interaction.editReply({ - content: "Guild does not have schematic?", - }); - - // Subcommand switch - const SubcommandGroup = interaction.options.getSubcommandGroup(true); - const Subcommand = interaction.options.getSubcommand(true); - - switch (SubcommandGroup) { - case "generalmoderation": { - switch (Subcommand) { - case "enforcesayinexecutor": { - const Enabled = interaction.options.getBoolean("enabled", true); - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { EnforceSayInExecutor: Enabled }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully configured \`\`EnforceSayinExecutor\`\` setting (new value is ${Enabled})`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - case "logchannel": { - const Channel = interaction.options.getChannel("channel", true); - if (!Channel.isTextBased()) - return await interaction.editReply({ - content: "The channel has to be a text-based channel!", - }); - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { LoggingChannelId: Channel.id }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully configured \`\`logchannel\`\` setting (new value is ${Channel.id})`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - case "publicmodlogchannel": { - const Channel = interaction.options.getChannel("channel", true); - if (!Channel.isTextBased()) - return await interaction.editReply({ - content: "The channel has to be a text-based channel!", - }); - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { PublicModLogChannelId: Channel.id }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully configured \`\`publicmodlogchannel\`\` setting (new value is ${Channel.id})`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - case "joinleavelogchannel": { - const Channel = interaction.options.getChannel("channel", true); - if (!Channel.isTextBased()) - return await interaction.editReply({ - content: "The channel has to be a text-based channel!", - }); - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { JoinLeaveLogChannelId: Channel.id }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully configured \`\`joinleavelogchannel\`\` setting (new value is ${Channel.id})`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - case "banappeallink": { - const Link = interaction.options.getString("link", false) || ""; - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { BanAppealLink: Link }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully configured \`\`banappeallink\`\` setting (new value is ${Link})`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - default: - break; - } - break; - } - case "disabledcommands": { - switch (Subcommand) { - case "add": { - const TargetDisabledCommands = interaction.options.getString("commands", true).split(","); - const UpdatedDisabledCommands = GuildSchema.DisabledCommands.concat(TargetDisabledCommands); - - // Check if command names are valid - let InvalidCommands = []; - for (const Command of TargetDisabledCommands) - if (!client.Commands.get(Command)) InvalidCommands.push(Command); - if (InvalidCommands.length !== 0) - return await interaction.editReply({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Invalid command(s)") - .setDescription( - `The following commands do not exist:\`\`\`\n${InvalidCommands.join( - ", ", - )}\n\`\`\`\nEnsure you type command names correctly and are seperated using "," (like,this,for,example).`, - ), - ], - }); - - // Update in database - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { - DisabledCommands: UpdatedDisabledCommands, - }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully added the new disabled commands.`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - case "remove": { - const TargetRemoveDisabledCommands = interaction.options.getString("commands", true).split(","); - const UpdatedDisabledCommands = GuildSchema.DisabledCommands; - - // Remove the commands that the user wants to remove - UpdatedDisabledCommands.filter((item) => { - return TargetRemoveDisabledCommands.indexOf(item) === -1; - }); - - // Update in database - client.Database.guild - .update({ - where: { GuildId: GuildSchema.GuildId }, - data: { - DisabledCommands: UpdatedDisabledCommands, - }, - }) - .then(async () => { - return await interaction.editReply({ - content: `Successfully removed the disabled commands.`, - }); - }) - .catch(async (err) => { - settingsNS.error(`Error while update guild configuration:\n${err}`); - return await interaction.editReply({ - content: - "An error occured while updating the guild configuration. Please try again later.", - }); - }); - break; - } - case "list": { - return await interaction.editReply({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("List of disabled commands") - .setDescription(`\`\`\`\n${GuildSchema.DisabledCommands.join(", ")}\n\`\`\``), - ], - }); - } - } - break; - } - } - return; - }, -}; diff --git a/src/commands/Moderation/TempBan.ts b/src/commands/Moderation/TempBan.ts deleted file mode 100644 index 8a2b77a..0000000 --- a/src/commands/Moderation/TempBan.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; -import ms from "ms"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("tempban") - .setDescription("Temporarily bans someone inside this server and create a new case regarding it") - .addUserOption((option) => - option.setName("user").setDescription("The user to be temporarily banned").setRequired(true), - ) - .addStringOption((option) => - option - .setName("reason") - .setDescription("The reason on why the user was temporarily banned") - .setRequired(true), - ) - .addStringOption((option) => - option.setName("duration").setDescription("The duration of the temporary ban").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("BanMembers")) - return await interaction.editReply({ - content: "You do not have permission to temporarily ban users from this server.", - }); - - const User = interaction.options.getUser("user", true); - const Reason = interaction.options.getString("reason", true); - const Duration = await interaction.options.getString("duration", true); - const AttachmentProof = interaction.options.getAttachment("proof", false); - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const GuildUser = await interaction.guild.members.fetch(User).catch(() => null); - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - await interaction.deferReply({ ephemeral: GuildSchema?.PublicModLogChannelId != "" }); - - if (User.id == interaction.user.id) - return await interaction.reply({ content: "You can't ban yourself!", ephemeral: true }); - if (User.bot) - return await interaction.reply({ content: "You can't ban bots! (do it manually)", ephemeral: true }); - if ( - GuildUser && - GuildUser.moderatable && - GuildUser.roles.highest.position >= interaction.member.roles.highest.position - ) - return interaction.reply({ - content: "You (or the bot) can't moderate this user due to lack of permission/hierachy.", - ephemeral: true, - }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: ModerationAction.TempBan, - TargetUserId: User.id, - ModeratorUserId: interaction.user.id, - GuildId: interaction.guildId, - Reason: Reason, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - Duration: Duration, - CreatedAt: new Date(), - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - }, - }); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | tempban | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(interaction.user.id), - }, - { name: "Reason", value: Reason }, - { name: "Duration", value: Duration }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - try { - const DirectMessageChannnel = await User.createDM(); - await DirectMessageChannnel.send({ embeds: [LogEmbed] }); - } catch (err) { - client.Logging.GetNamespace("Moderation/TempBan").warn(`Could not dm ${User.id}\n${err}`); - } - - await interaction.guild.members.ban(User, { - reason: `Case ${CaseResult.CaseId} by ${interaction.user.username} (${interaction.user.id}): ${Reason}`, - }); - - await client.Database.activeTempBans.create({ - data: { - GlobalCaseId: CaseResult.GlobalCaseId, - }, - }); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: "Temp Ban" }, - { name: "Reason", value: Reason }, - { name: "Duration", value: Duration }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.editReply({ - content: - PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - }); - }, - Init(client) { - setInterval(async () => { - await client.Database.$transaction(async (tx) => { - const ActiveTBs = await tx.activeTempBans.findMany({ include: { Case: true } }); - const Promises = [ - ActiveTBs.map(async (active) => { - const CreatedAt = active.Case.CreatedAt; - const ExpiresAt = new Date(Number(active.Case.CreatedAt) + ms(active.Case.Duration)); - if (ExpiresAt <= CreatedAt) { - const Guild = await client.guilds.fetch(active.Case.GuildId); - const User = await client.users.fetch(active.Case.TargetUserId); - await Guild.members.unban(User); - return tx.activeTempBans.delete({ where: { ActiveTempBanId: active.ActiveTempBanId } }); - } - return; - }), - ]; - await Promise.all(Promises); - return; - }); - }, 10000); - }, -}; diff --git a/src/commands/Moderation/Unban.ts b/src/commands/Moderation/Unban.ts deleted file mode 100644 index 8e8fbf3..0000000 --- a/src/commands/Moderation/Unban.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("unban") - .setDescription("Unban someone that is banned and create a new case regarding it") - .addUserOption((option) => option.setName("user").setDescription("The user to be unbanned").setRequired(true)) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why the user was unbanned").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("BanMembers")) - return await interaction.editReply({ - content: "You do not have permission to unban users from this server.", - }); - - const User = interaction.options.getUser("user", true); - const Reason = interaction.options.getString("reason", true); - const AttachmentProof = interaction.options.getAttachment("proof", false); - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - if (User.id == interaction.user.id) return await interaction.reply({ content: "the what", ephemeral: true }); - if (User.bot) - return await interaction.reply({ - content: "You can't ban bots so unban the bot manually", - ephemeral: true, - }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: ModerationAction.Unban, - TargetUserId: User.id, - ModeratorUserId: interaction.user.id, - GuildId: interaction.guildId, - Reason: Reason, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - CreatedAt: new Date(), - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - }, - }); - await interaction.guild.members.unban( - User, - `Case ${CaseResult.CaseId} by ${interaction.user.username} (${interaction.user.id}): ${Reason}`, - ); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | unban | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(interaction.user.id), - }, - { name: "Reason", value: Reason }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: "Unban" }, - { name: "Reason", value: Reason }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ - content: - PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - ephemeral: GuildSchema?.PublicModLogChannelId != "", - }); - }, -}; diff --git a/src/commands/Moderation/Warn.ts b/src/commands/Moderation/Warn.ts deleted file mode 100644 index 8b3ddfc..0000000 --- a/src/commands/Moderation/Warn.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { ModerationAction } from "@prisma/client"; -import { SlashCommandBuilder, userMention } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("warn") - .setDescription("Warns someone inside this server and create a new case regarding it") - .addUserOption((option) => option.setName("user").setDescription("The user to be warned").setRequired(true)) - .addStringOption((option) => - option.setName("reason").setDescription("The reason on why the user was warned").setRequired(true), - ) - .addAttachmentOption((option) => - option - .setName("proof") - .setDescription("An media containing proof to prove the reason valid") - .setRequired(false), - ) - .addStringOption((option) => - option.setName("modnote").setDescription("Interal moderator notes").setRequired(false), - ) - .addAttachmentOption((option) => - option - .setName("modattach") - .setDescription("Internal media attachment only visible to moderators") - .setRequired(false), - ), - async Callback(interaction, client) { - // TODO: Warning users shouldn't be attached to viewing audit logs, find a good permission for this - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.editReply({ - content: "You do not have permission to warn users from this server.", - }); - - const User = interaction.options.getUser("user", true); - const Reason = interaction.options.getString("reason", true); - const AttachmentProof = interaction.options.getAttachment("proof", false); - const ModeratorNote = interaction.options.getString("modnote", false) || ""; - const ModeratorAttachment = interaction.options.getAttachment("modattach", false); - const GuildUser = await interaction.guild.members.fetch(User).catch(() => null); - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; - - if (User.id == interaction.user.id) - return await interaction.reply({ content: "You can't warn yourself!", ephemeral: true }); - if (User.bot) return await interaction.reply({ content: "You can't warn bots!", ephemeral: true }); - if ( - !GuildUser || - !GuildUser.moderatable || - GuildUser.roles.highest.position >= interaction.member.roles.highest.position - ) - return interaction.reply({ - content: "You (or the bot) can't moderate this user due to lack of permission/hierachy.", - ephemeral: true, - }); - - await client.Database.guild.update({ - where: { GuildId: interaction.guildId }, - data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, - }); - const CaseResult = await client.Database.moderationCase.create({ - data: { - CaseId: GuildSchema.CurrentCaseId + 1, - Action: ModerationAction.Warn, - TargetUserId: User.id, - ModeratorUserId: interaction.user.id, - GuildId: interaction.guildId, - Reason: Reason, - AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - CreatedAt: new Date(), - ModeratorNote: ModeratorNote, - ModeratorAttachment: ModeratorAttachment ? ModeratorAttachment.url : "", - }, - }); - - const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setAuthor({ - name: `Case: #${CaseResult.CaseId} | warn | ${User.username}`, - iconURL: User.displayAvatarURL({ extension: "png" }), - }) - .addFields( - { name: "User", value: userMention(User.id) }, - { - name: "Moderator", - value: userMention(interaction.user.id), - }, - { name: "Reason", value: Reason }, - ) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setFooter({ text: `Id: ${User.id}` }) - .setTimestamp() - .setColor("Red"); - - const PublicModLogChannel = await interaction.guild.channels - .fetch(GuildSchema.PublicModLogChannelId) - .catch(() => null); - let PublicModLogMsgId = ""; - if (PublicModLogChannel && PublicModLogChannel.isTextBased()) - PublicModLogMsgId = (await PublicModLogChannel.send({ embeds: [LogEmbed] })).id; - - if (PublicModLogMsgId != "") - await client.Database.moderationCase.update({ - where: { GlobalCaseId: CaseResult.GlobalCaseId }, - data: { PublicModLogMsgId: PublicModLogMsgId }, - }); - - const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); - if (GuildSetting && GuildSetting.LoggingChannelId != "") - client.channels - .fetch(GuildSetting.LoggingChannelId) - .then(async (channel) => { - if (channel && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Moderation action") - .setFields([ - { name: "Case id", value: String(CaseResult.CaseId) }, - { - name: "Moderator", - value: `${interaction.user.username} (${ - interaction.user.id - }) (${userMention(interaction.user.id)})`, - }, - { - name: "Offending user", - value: `${User.username} (${User.id}) (${userMention(User.id)})`, - }, - { name: "Action", value: "Warn" }, - { name: "Reason", value: Reason }, - { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, - { name: "Moderator note", value: ModeratorNote != "" ? ModeratorNote : "N/A" }, - { - name: "Moderator attachment", - value: ModeratorAttachment ? ModeratorAttachment.url : "N/A", - }, - ]) - .setImage(AttachmentProof ? AttachmentProof.url : null) - .setThumbnail(ModeratorAttachment ? ModeratorAttachment.url : null), - ], - }); - }) - .catch(() => null); - - return await interaction.reply({ - content: - PublicModLogChannel != null && PublicModLogChannel.isTextBased() - ? undefined - : "(Warning: could not send log message to the public mod log channel)", - embeds: [LogEmbed], - ephemeral: GuildSchema?.PublicModLogChannelId != "", - }); - }, -}; diff --git a/src/commands/Tests/DeferredErrorTest.ts b/src/commands/Tests/DeferredErrorTest.ts deleted file mode 100644 index f63b3cb..0000000 --- a/src/commands/Tests/DeferredErrorTest.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("deferrederrortest") - .setDescription("Command to test error handling (deferred reply version)"), - async Callback(interaction) { - await interaction.deferReply(); - throw new Error("This is a error test command, it throws a error obviously."); - }, -}; diff --git a/src/commands/Tests/EmbedTest.ts b/src/commands/Tests/EmbedTest.ts deleted file mode 100644 index 197633c..0000000 --- a/src/commands/Tests/EmbedTest.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder().setName("embedtest").setDescription("Command to test embeds"), - async Callback(interaction) { - return await interaction.reply({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("The command worked") - .setDescription("Hello there") - .addFields({ name: "Field 1", value: "why not?" }), - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Error version") - .setDescription("bruh") - .setErrorColor(), - ], - }); - }, -}; diff --git a/src/commands/Tests/ErrorTest.ts b/src/commands/Tests/ErrorTest.ts deleted file mode 100644 index 9cc8109..0000000 --- a/src/commands/Tests/ErrorTest.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder().setName("errortest").setDescription("Command to test error handling"), - async Callback() { - throw new Error("This is a error test command, it throws a error obviously."); - }, -}; diff --git a/src/commands/Tests/OptionsTest.ts b/src/commands/Tests/OptionsTest.ts deleted file mode 100644 index 1064ee8..0000000 --- a/src/commands/Tests/OptionsTest.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder() - .setName("optionstest") - .setDescription("Command to test handling option(s) data") - .addUserOption((option) => option.setName("user").setRequired(true).setDescription("This is a user option")), - async Callback(interaction) { - const User = interaction.options.getUser("user", true); - await interaction.reply(User.tag); - await interaction.followUp("This is a follow up message"); - return; - }, -}; diff --git a/src/commands/Tests/Test.ts b/src/commands/Tests/Test.ts deleted file mode 100644 index e5eb615..0000000 --- a/src/commands/Tests/Test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import type { MeteoriumCommand } from ".."; - -export const Command: MeteoriumCommand = { - InteractionData: new SlashCommandBuilder().setName("test").setDescription("This is a test command"), - async Callback(interaction) { - return await interaction.reply({ content: "The test command worked." }); - }, -}; diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index 0b0e0d7..0000000 --- a/src/commands/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { AutocompleteInteraction, Awaitable, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; -import type { MeteoriumClient } from "../util/MeteoriumClient"; - -// Category - Tests -export * as test from "./Tests/Test"; -export * as embedtest from "./Tests/EmbedTest"; -export * as errortest from "./Tests/ErrorTest"; -export * as deferrederrortest from "./Tests/DeferredErrorTest"; -export * as optionstest from "./Tests/OptionsTest"; - -// Category - Info -export * as help from "./Info/Help"; -export * as ping from "./Info/Ping"; -export * as userinfo from "./Info/UserInfo"; -export * as holodexapi from "./Info/HolodexAPI"; -export * as rbxapi from "./Info/RbxAPI"; -export * as tag from "./Info/Tag"; -export * as serverinfo from "./Info/ServerInfo"; - -// Category - Fun -export * as music from "./Fun/Music"; - -// Category - Moderation -export * as settings from "./Moderation/Settings"; -export * as sayin from "./Moderation/SayIn"; -export * as reactto from "./Moderation/ReactTo"; -export * as ban from "./Moderation/Ban"; -export * as kick from "./Moderation/Kick"; -export * as mute from "./Moderation/Mute"; -export * as warn from "./Moderation/Warn"; -export * as punishments from "./Moderation/Punishments"; -export * as case from "./Moderation/Case"; -export * as removecase from "./Moderation/RemoveCase"; -export * as unban from "./Moderation/Unban"; -export * as createcase from "./Moderation/CreateCase"; -export * as tempban from "./Moderation/TempBan"; -export * as editcase from "./Moderation/EditCase"; - -export type MeteoriumCommand = { - InteractionData: Pick; - Callback(interaction: ChatInputCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; - Autocomplete?(interaction: AutocompleteInteraction<"cached">, client: MeteoriumClient): Awaitable; - Init?(client: MeteoriumClient): Awaitable; -}; diff --git a/src/contextmenu/index.ts b/src/contextmenu/index.ts deleted file mode 100644 index 2cf2816..0000000 --- a/src/contextmenu/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { - Awaitable, - MessageContextMenuCommandInteraction, - UserContextMenuCommandInteraction, - ContextMenuCommandBuilder, - ApplicationCommandType, -} from "discord.js"; -import type { MeteoriumClient } from "../util/MeteoriumClient"; - -// User context menus -export * as userinfo from "./user/UserInfo"; - -// Message context menus -export * as sayinreply from "./message/SayInReply"; - -export type MeteoriumUserContextMenuAction = { - Name: string; - Type: ApplicationCommandType.User; - InteractionData: Pick; - Callback(interaction: UserContextMenuCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; - Init?(client: MeteoriumClient): Awaitable; -}; - -export type MeteoriumMessageContextMenuAction = { - Name: string; - Type: ApplicationCommandType.Message; - InteractionData: Pick; - Callback(interaction: MessageContextMenuCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; - Init?(client: MeteoriumClient): Awaitable; -}; diff --git a/src/contextmenu/message/SayInReply.ts b/src/contextmenu/message/SayInReply.ts deleted file mode 100644 index 708dd1b..0000000 --- a/src/contextmenu/message/SayInReply.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - ContextMenuCommandBuilder, - ApplicationCommandType, - ActionRowBuilder, - ModalBuilder, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; -import type { MeteoriumMessageContextMenuAction } from ".."; - -export const ContextMenuAction: MeteoriumMessageContextMenuAction = { - Name: "SayIn reply", - Type: ApplicationCommandType.Message, - InteractionData: new ContextMenuCommandBuilder().setName("SayIn reply").setType(ApplicationCommandType.Message), - async Callback(interaction, client) { - if (!interaction.member.permissions.has("ManageMessages")) - return await interaction.reply({ content: "You do not have permission to use SayIn." }); - - const modal = new ModalBuilder().setCustomId("SayInReplyModal").setTitle("SayIn reply configuration"); - - const messageInput = new TextInputBuilder() - .setCustomId("Message") - .setLabel("Message") - .setPlaceholder("The content of the reply message") - .setStyle(TextInputStyle.Paragraph) - .setRequired(true); - - const showExecutorNameInput = new TextInputBuilder() - .setCustomId("ShowExecutorName") - .setLabel("Show executor name") - .setPlaceholder('Strictly "yes" or "no", leave blank to specify no') - .setStyle(TextInputStyle.Short) - .setRequired(false); - - modal.addComponents( - new ActionRowBuilder().addComponents(messageInput), - new ActionRowBuilder().addComponents(showExecutorNameInput), - ); - - await interaction.showModal(modal); - const modalSubmitInteraction = await interaction.awaitModalSubmit({ - filter: (interaction) => interaction.customId == "SayInReplyModal", - time: 60000, - }); - - const GuildSetting = await client.Database.guild.findUnique({ - where: { GuildId: String(interaction.guildId) }, - }); - if (!GuildSetting) - return await interaction.editReply({ - content: "No guild setting inside database?", - }); - - const ShowExecutorName = - GuildSetting.EnforceSayInExecutor && !interaction.member.permissions.has("Administrator", true) - ? true - : modalSubmitInteraction.fields.getTextInputValue("ShowExecutorName").toLowerCase() == "no" - ? false - : true; - - const Message = ShowExecutorName - ? `${modalSubmitInteraction.fields.getTextInputValue("Message")}\n\n(Sayin command executed by ${ - interaction.user.tag - } (${interaction.user.id}))` - : modalSubmitInteraction.fields.getTextInputValue("Message"); - - await interaction.targetMessage.channel.send({ - content: Message, - reply: { messageReference: interaction.targetMessage }, - }); - - return await modalSubmitInteraction.reply({ content: "Sent reply", ephemeral: true }); - }, -}; diff --git a/src/contextmenu/user/UserInfo.ts b/src/contextmenu/user/UserInfo.ts deleted file mode 100644 index 28f1a68..0000000 --- a/src/contextmenu/user/UserInfo.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { GuildMember, ContextMenuCommandBuilder, User, ApplicationCommandType } from "discord.js"; -import type { MeteoriumUserContextMenuAction } from ".."; -import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; - -export const ContextMenuAction: MeteoriumUserContextMenuAction = { - Name: "Get user info", - Type: ApplicationCommandType.User, - InteractionData: new ContextMenuCommandBuilder().setName("Get user info").setType(ApplicationCommandType.User), - async Callback(interaction) { - const Embed = new MeteoriumEmbedBuilder(undefined, interaction.user); - const ParsedUser = await interaction.guild.members - .fetch(interaction.targetUser.id) - .catch(() => interaction.targetUser); - - if (ParsedUser instanceof GuildMember) { - // User status and is client device parsing - let UserStatus = "Unknown"; - if (ParsedUser.presence && ParsedUser.presence["status"]) { - const ClientStatus = `Desktop: ${ParsedUser.presence.clientStatus?.desktop || "N/A"} | Mobile: ${ - ParsedUser.presence.clientStatus?.mobile || "N/A" - } | Web: ${ParsedUser.presence.clientStatus?.web || "N/A"}`; - if (ParsedUser.presence.status === "dnd") { - UserStatus = `do not disturb - ${ClientStatus}`; - } else { - UserStatus = `${ParsedUser.presence.status} - ${ClientStatus}`; - } - } - - Embed.setDescription(String(ParsedUser)) - .setTitle(ParsedUser.user.tag) - .setAuthor({ - name: "Guild member", - url: `https://discordapp.com/users/${ParsedUser.user.id}`, - }) - .setThumbnail(ParsedUser.user.displayAvatarURL()) - .setColor(ParsedUser.displayColor ? ParsedUser.displayColor : [0, 153, 255]) - .addFields([ - { name: "Status", value: UserStatus }, - { name: "UserId", value: ParsedUser.user.id }, - { - name: "Joined Discord at", - value: `\n${ - ParsedUser.user.createdAt - }\n()`, - }, - ]); - - if (ParsedUser?.joinedTimestamp) { - Embed.addFields([ - { - name: "Joined this server at", - value: `\n()`, - }, - ]); - } - - if (ParsedUser?.premiumSince && ParsedUser?.premiumSinceTimestamp) { - Embed.addFields([ - { - name: "Server Nitro Booster", - value: `${ - ParsedUser.premiumSince - ? `Booster since ()` - : "Not a booster" - }`, - }, - ]); - } - - Embed.addFields([ - { - name: `Roles (${ - ParsedUser.roles.cache.filter((role) => role.name !== "@everyone").size - } in total without @everyone)`, - value: ParsedUser.roles.cache.filter((role) => role.name !== "@everyone").size - ? (() => - ParsedUser.roles.cache - .filter((role) => role.name !== "@everyone") - .sort((role1, role2) => role2.rawPosition - role1.rawPosition) - .map((role) => role) - .join(", "))() - : "———", - }, - ]); - } else if (ParsedUser instanceof User) { - Embed.setDescription(String(ParsedUser)) - .setTitle(ParsedUser.tag) - .setAuthor({ - name: "User", - url: `https://discordapp.com/users/${ParsedUser.id}`, - }) - .setThumbnail(ParsedUser.displayAvatarURL()) - .addFields([ - { name: "UserId", value: ParsedUser.id }, - { - name: "Joined Discord at", - value: `\n${ - ParsedUser.createdAt - }\n()`, - }, - ]); - } - - return await interaction.reply({ embeds: [Embed], ephemeral: true }); - }, -}; diff --git a/src/events/eventsEntry.ts b/src/events/eventsEntry.ts new file mode 100644 index 0000000..890a398 --- /dev/null +++ b/src/events/eventsEntry.ts @@ -0,0 +1,13 @@ +import type { Awaitable, ClientEvents } from "discord.js"; +import type MeteoriumClient from "../classes/client.js"; + +export * as ReadyInit from "./readyInit.js"; +export * as InteractionHandler from "./interactionHandler.js"; +export * as PresenceResumption from "./presenceResumption.js"; +export * as GuildDataSetup from "./guildDataSetup.js"; + +export type MeteoriumEvent = { + event: EventName; + callback(client: MeteoriumClient, ...args: ClientEvents[EventName]): Awaitable; + once: boolean; +}; diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts deleted file mode 100644 index 7c16fb7..0000000 --- a/src/events/guildCreate.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { MeteoriumEvent } from "."; - -export const Event: MeteoriumEvent<"guildCreate"> = { - async Callback(client, guild) { - const guildCreateNS = client.Logging.GetNamespace("Events/guildCreate"); - if ( - client.Database.guild.findUnique({ - where: { GuildId: guild.id }, - }) === null - ) { - guildCreateNS.verbose(`Creating new guild in database for ${guild.id}`); - client.Database.guild.create({ data: { GuildId: guild.id } }); - } - return; - }, -}; diff --git a/src/events/guildDataSetup.ts b/src/events/guildDataSetup.ts new file mode 100644 index 0000000..96e82d5 --- /dev/null +++ b/src/events/guildDataSetup.ts @@ -0,0 +1,14 @@ +import type { MeteoriumEvent } from "./eventsEntry.js"; + +export const Event: MeteoriumEvent<"guildCreate"> = { + event: "guildCreate", + async callback(client, guild) { + await client.db.guild.upsert({ + where: { GuildId: guild.id }, + create: { GuildId: guild.id }, + update: {}, + }); + return; + }, + once: false, +}; diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts deleted file mode 100644 index d98774e..0000000 --- a/src/events/guildMemberAdd.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { MeteoriumEvent } from "."; -import { MeteoriumEmbedBuilder } from "../util/MeteoriumEmbedBuilder"; -import { GenerateFormattedTime } from "../util/Utilities"; - -export const Event: MeteoriumEvent<"guildMemberAdd"> = { - async Callback(client, member) { - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: member.guild.id } }))!; - if (!GuildSchema) return; - if (GuildSchema.JoinLeaveLogChannelId == "") return; - - const LogEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ - name: member.user.username, - iconURL: member.user.displayAvatarURL({ extension: "png" }), - }) - .setTitle("Welcome!") - .setDescription(`<@${member.user.id}> \`\`${member.user.username}\`\` (${member.user.id})`) - .setFields([ - { name: "Created the account at", value: GenerateFormattedTime(member.user.createdAt) }, - { name: "Joined the server at", value: GenerateFormattedTime(member.joinedAt!) }, - ]) - .setColor("Green"); - - const JLLChannel = await member.guild.channels.fetch(GuildSchema.JoinLeaveLogChannelId); - if (JLLChannel && JLLChannel.isTextBased()) await JLLChannel.send({ embeds: [LogEmbed] }); - - const VLChannel = await member.guild.channels.fetch(GuildSchema.LoggingChannelId); - if (VLChannel && VLChannel.isTextBased()) await VLChannel.send({ embeds: [LogEmbed] }); - - return; - }, -}; diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts deleted file mode 100644 index 5cb5908..0000000 --- a/src/events/guildMemberRemove.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { MeteoriumEvent } from "."; -import { MeteoriumEmbedBuilder } from "../util/MeteoriumEmbedBuilder"; -import { GenerateFormattedTime } from "../util/Utilities"; - -export const Event: MeteoriumEvent<"guildMemberRemove"> = { - async Callback(client, member) { - const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: member.guild.id } }))!; - if (!GuildSchema) return; - if (GuildSchema.JoinLeaveLogChannelId == "") return; - - const LogEmbed = new MeteoriumEmbedBuilder() - .setAuthor({ - name: member.user.username, - iconURL: member.user.displayAvatarURL({ extension: "png" }), - }) - .setTitle("Left!") - .setDescription(`<@${member.user.id}> \`\`${member.user.username}\`\` (${member.user.id})`) - .setFields([ - { name: "Created the account at", value: GenerateFormattedTime(member.user.createdAt) }, - { name: "Joined the server at", value: GenerateFormattedTime(member.joinedAt!) }, - ]) - .setColor("Red"); - - const JLLChannel = await member.guild.channels.fetch(GuildSchema.JoinLeaveLogChannelId); - if (JLLChannel && JLLChannel.isTextBased()) await JLLChannel.send({ embeds: [LogEmbed] }); - - const VLChannel = await member.guild.channels.fetch(GuildSchema.LoggingChannelId); - if (VLChannel && VLChannel.isTextBased()) await VLChannel.send({ embeds: [LogEmbed] }); - - return; - }, -}; diff --git a/src/events/index.ts b/src/events/index.ts index ba49990..edfe7ca 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,14 +1,44 @@ -import type { Awaitable, ClientEvents } from "discord.js"; -import type { MeteoriumClient } from "../util/MeteoriumClient"; - -export * as ready from "./ready"; -export * as interactionCreate from "./interactionCreate"; -export * as guildCreate from "./guildCreate"; -export * as shardResume from "./shardResume"; -export * as guildMemberAdd from "./guildMemberAdd"; -export * as guildMemberRemove from "./guildMemberRemove"; - -export type MeteoriumEvent = { - Callback(client: MeteoriumClient, ...args: ClientEvents[EventName]): Awaitable; - Once?: true; -}; +import { Collection } from "discord.js"; +import type MeteoriumClient from "../classes/client.js"; +import type { LoggingNamespace } from "../classes/logging.js"; + +import * as events from "./eventsEntry.js"; + +export default class MeteoriumEventManager { + public events: Collection>; + public client: MeteoriumClient; + public logging: LoggingNamespace; + public eventNS: LoggingNamespace; + + public constructor(client: MeteoriumClient) { + this.events = new Collection(); + this.client = client; + this.logging = client.logging.registerNamespace("EventManager"); + this.eventNS = this.logging.registerNamespace("Event"); + } + + public register() { + const regNS = this.logging.getNamespace("Registration"); + + regNS.info("Registering events"); + for (const [Name, { Event }] of Object.entries(events)) { + regNS.verbose(`Registering -> ${Name}${Event.once ? " (Once)" : ""}`); + this.events.set(Name, Event); + } + + return; + } + + public hook() { + const hookNS = this.logging.registerNamespace("Hooking"); + + hookNS.info("Hooking events to client"); + for (const [Name, Event] of this.events) { + hookNS.verbose(`Hooking -> ${Name}`); + if (Event.once) this.client.once(Event.event, (...args) => Event.callback(this.client, ...args)); + else this.client.on(Event.event, (...args) => Event.callback(this.client, ...args)); + } + + return; + } +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts deleted file mode 100644 index d24507b..0000000 --- a/src/events/interactionCreate.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { inspect } from "util"; -import { ApplicationCommandType, codeBlock } from "discord.js"; -import type { MeteoriumEvent } from "."; -import { MeteoriumEmbedBuilder } from "../util/MeteoriumEmbedBuilder"; - -export const Event: MeteoriumEvent<"interactionCreate"> = { - async Callback(client, interaction) { - if (!interaction.inCachedGuild()) return; - - // Slash command interaction handling - if (interaction.isChatInputCommand()) { - const commandHandlerNS = client.Logging.GetNamespace("Events/interactionCreate/SlashCommandHandler"); - - const Command = client.Commands.get(interaction.commandName); - if (Command == undefined) - return commandHandlerNS.error( - `Unexpected behavior when handling slash command interaction: ${interaction.commandName} doesn't exist on client.Commands.`, - ); - - let GuildExistInDb = await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }); - if (GuildExistInDb == null) - GuildExistInDb = await client.Database.guild.create({ data: { GuildId: interaction.guildId } }); - - if (GuildExistInDb && GuildExistInDb.LoggingChannelId != "") { - client.channels - .fetch(GuildExistInDb.LoggingChannelId) - .then(async (channel) => { - if (channel != null && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Command executed") - .setFields([ - { name: "Command name", value: interaction.commandName }, - { - name: "Executor", - value: `${interaction.user.username} (${interaction.user.id}) (<@${interaction.user.id}>)`, - }, - ]) - .setNormalColor(), - ], - }); - }) - .catch(() => null); - } - - try { - await Command.Callback(interaction, client); - } catch (err) { - commandHandlerNS.error("Slash command callback error:\n" + err); - const ErrorEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Error occurred while the command callback was running") - .setDescription(codeBlock(inspect(err))) - .setErrorColor(); - try { - if (interaction.deferred) { - await interaction.editReply({ - content: "Error occurred (if you don't see anything below, you have embeds disabled)", - embeds: [ErrorEmbed], - }); - } else { - await interaction.reply({ - content: "Error occurred (if you don't see anything below, you have embeds disabled)", - embeds: [ErrorEmbed], - ephemeral: true, - }); - } - } catch (err) { - commandHandlerNS.error(`Could not send interaction error reply!\n${codeBlock(inspect(err))}`); - } - } - return; - } - - // Context menu command interaction handling - if (interaction.isContextMenuCommand()) { - const commandHandlerNS = client.Logging.GetNamespace("Events/interactionCreate/ContextMenuActionHandler"); - - const Command = - client.UserContextMenuActions.get(interaction.commandName) || - client.MessageContextMenuActions.get(interaction.commandName); - if (Command == undefined) - return commandHandlerNS.error( - `Unexpected behavior when handling context menu interaction: ${interaction.commandName} doesn't exist on client.ContextMenuActions.`, - ); - - let GuildExistInDb = await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }); - if (GuildExistInDb == null) - GuildExistInDb = await client.Database.guild.create({ data: { GuildId: interaction.guildId } }); - - if (GuildExistInDb && GuildExistInDb.LoggingChannelId != "") { - client.channels - .fetch(GuildExistInDb.LoggingChannelId) - .then(async (channel) => { - if (channel != null && channel.isTextBased()) - await channel.send({ - embeds: [ - new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Context menu command executed") - .setFields([ - { name: "Command name", value: interaction.commandName }, - { - name: "Executor", - value: `${interaction.user.username} (${interaction.user.id}) (<@${interaction.user.id}>)`, - }, - ]) - .setNormalColor(), - ], - }); - }) - .catch(() => null); - } - - try { - if (interaction.isUserContextMenuCommand() && Command.Type == ApplicationCommandType.User) - await Command.Callback(interaction, client); - else if (interaction.isMessageContextMenuCommand() && Command.Type == ApplicationCommandType.Message) - await Command.Callback(interaction, client); - else throw "Invalid command object/interaction data"; - } catch (err) { - commandHandlerNS.error("Slash command callback error:\n" + err); - const ErrorEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) - .setTitle("Error occurred while the context menu callback was running") - .setDescription(codeBlock(inspect(err))) - .setErrorColor(); - try { - if (interaction.deferred) { - await interaction.editReply({ - content: "Error occurred (if you don't see anything below, you have embeds disabled)", - embeds: [ErrorEmbed], - }); - } else { - await interaction.reply({ - content: "Error occurred (if you don't see anything below, you have embeds disabled)", - embeds: [ErrorEmbed], - ephemeral: true, - }); - } - } catch (err) { - commandHandlerNS.error(`Could not send interaction error reply!\n${codeBlock(inspect(err))}`); - } - } - } - - // Autocomplete interaction handling - if (interaction.isAutocomplete()) { - const autocompleteHandlerNS = client.Logging.GetNamespace("Events/interactionCreate/AutocompleteHandler"); - - const Command = client.Commands.get(interaction.commandName); - if (Command == undefined) - return autocompleteHandlerNS.error( - `Unexpected behavior when handling autocomplete interaction: ${interaction.commandName} doesn't exist on client.Commands.`, - ); - if (!Command.Autocomplete) return; - - let GuildExistInDb = await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }); - if (GuildExistInDb == null) - GuildExistInDb = await client.Database.guild.create({ data: { GuildId: interaction.guildId } }); - - try { - Command.Autocomplete!(interaction, client); - } catch (err) { - autocompleteHandlerNS.error( - `Caught error while handling autocomplete for ${interaction.commandName} in ${ - interaction.guildId - }:\n${codeBlock(inspect(err))}`, - ); - } - - return; - } - - return; - }, -}; diff --git a/src/events/interactionHandler.ts b/src/events/interactionHandler.ts new file mode 100644 index 0000000..ae65fee --- /dev/null +++ b/src/events/interactionHandler.ts @@ -0,0 +1,10 @@ +import type { MeteoriumEvent } from "./eventsEntry.js"; + +export const Event: MeteoriumEvent<"interactionCreate"> = { + event: "interactionCreate", + async callback(client, interaction) { + await client.interactions.dispatchInteraction(interaction); + return; + }, + once: false, +}; diff --git a/src/events/shardResume.ts b/src/events/presenceResumption.ts similarity index 50% rename from src/events/shardResume.ts rename to src/events/presenceResumption.ts index 5581c0a..b5f40ee 100644 --- a/src/events/shardResume.ts +++ b/src/events/presenceResumption.ts @@ -1,12 +1,16 @@ import { ActivityType } from "discord.js"; -import type { MeteoriumEvent } from "."; +import type { MeteoriumEvent } from "./eventsEntry.js"; export const Event: MeteoriumEvent<"shardResume"> = { - async Callback(client) { + event: "shardResume", + async callback(client) { + const resumeNS = client.events.eventNS.getNamespace("PresenceResumption"); + resumeNS.info("Setting client presence"); client.user.setPresence({ status: "idle", activities: [{ name: "no", type: ActivityType.Playing }], }); return; }, + once: false, }; diff --git a/src/events/ready.ts b/src/events/ready.ts deleted file mode 100644 index 1d7e263..0000000 --- a/src/events/ready.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { MeteoriumEvent } from "."; -import { - ActivityType, - RESTPostAPIChatInputApplicationCommandsJSONBody, - RESTPostAPIContextMenuApplicationCommandsJSONBody, - codeBlock, -} from "discord.js"; -import moment from "moment"; -import { inspect } from "util"; -import { MeteoriumEmbedBuilder } from "../util/MeteoriumEmbedBuilder"; - -export const Event: MeteoriumEvent<"ready"> = { - async Callback(client) { - const readyNS = client.Logging.GetNamespace("Events/ready"); - const StartTime = moment().format("DD-MM-YYYY hh:mm:ss:SSS A Z"); - - const MergedApplicationCommands: Array< - RESTPostAPIChatInputApplicationCommandsJSONBody | RESTPostAPIContextMenuApplicationCommandsJSONBody - > = []; - client.Commands.forEach((command) => MergedApplicationCommands.push(command.InteractionData.toJSON())); - client.UserContextMenuActions.forEach((contextMenuAction) => - MergedApplicationCommands.push(contextMenuAction.InteractionData.toJSON()), - ); - client.MessageContextMenuActions.forEach((contextMenuAction) => - MergedApplicationCommands.push(contextMenuAction.InteractionData.toJSON()), - ); - - readyNS.info("Registering global application commands at Discord"); - await client.application.commands.set(MergedApplicationCommands); // Global application commands - - readyNS.info("Registering guild application commands at Discord"); - client.Config.InteractionFirstDeployGuildIds.forEach(async (guildId) => { - const Guild = await client.guilds.fetch(guildId).catch(() => null); - if (!Guild) return readyNS.error(`Cannot register guild application command for ${guildId}`); - readyNS.info(`Registering guild application commands -> ${Guild.name} (${guildId})`); - await Guild.commands.set(MergedApplicationCommands); - }); - - readyNS.verbose("Setting user presence"); - client.user.setPresence({ - status: "idle", - activities: [{ name: "no", type: ActivityType.Playing }], - }); - - readyNS.info("Bot ready"); - - async function ExitHandler() { - readyNS.info("Bot is shutting down!"); - const Promises: Promise[] = []; - const ShutdownEmbed = new MeteoriumEmbedBuilder() - .setTitle("Bot shutting down") - .setDescription("The bot is now shutting down...") - .addFields([ - { name: "Start time", value: StartTime }, - { name: "Shut down time", value: moment().format("DD-MM-YYYY hh:mm:ss:SSS A Z") }, - ]) - .setColor("Red"); - for (const ChannelId of client.Config.RuntimeLogChannelIds) { - const Channel = await client.channels.fetch(ChannelId).catch(() => null); - - if (Channel && Channel.isTextBased()) - Promises.push( - Channel.send({ - embeds: [ShutdownEmbed], - }), - ); - } - await Promise.all(Promises); - client.destroy(); - process.exit(0); - } - - process.on("SIGTERM", ExitHandler); - process.on("SIGINT", ExitHandler); - - process.on("uncaughtException", async (err) => { - const Promises: Promise[] = []; - const ErrorEmbed = new MeteoriumEmbedBuilder() - .setTitle("Uncaught exception occured") - .setDescription(codeBlock(inspect(err).substring(0, 4500))) - .setColor("Red"); - - for (const ChannelId of client.Config.RuntimeLogChannelIds) { - const Channel = await client.channels.fetch(ChannelId).catch(() => null); - if (Channel && Channel.isTextBased()) - Promises.push( - Channel.send({ - embeds: [ErrorEmbed], - }), - ); - } - await Promise.all(Promises); - }); - - process.on("unhandledRejection", async (err) => { - const Promises: Promise[] = []; - const ErrorEmbed = new MeteoriumEmbedBuilder() - .setTitle("Unhandled rejection occured") - .setDescription(codeBlock(inspect(err).substring(0, 4500))) - .setColor("Red"); - - for (const ChannelId of client.Config.RuntimeLogChannelIds) { - const Channel = await client.channels.fetch(ChannelId).catch(() => null); - if (Channel && Channel.isTextBased()) - Promises.push( - Channel.send({ - embeds: [ErrorEmbed], - }), - ); - } - await Promise.all(Promises); - }); - - const ReadyEmbedPromises: Promise[] = []; - const ReadyEmbed = new MeteoriumEmbedBuilder() - .setTitle("Bot online") - .setDescription("The bot is now online") - .addFields([{ name: "Start time", value: StartTime }]) - .setColor("Green"); - - for (const ChannelId of client.Config.RuntimeLogChannelIds) { - const Channel = await client.channels.fetch(ChannelId).catch(() => null); - if (Channel && Channel.isTextBased()) - ReadyEmbedPromises.push( - Channel.send({ - embeds: [ReadyEmbed], - }), - ); - } - - await Promise.all(ReadyEmbedPromises); - - return; - }, -}; diff --git a/src/events/readyInit.ts b/src/events/readyInit.ts new file mode 100644 index 0000000..592c29a --- /dev/null +++ b/src/events/readyInit.ts @@ -0,0 +1,130 @@ +import process from "node:process"; +import util from "node:util"; +import { ActivityType, codeBlock, time } from "discord.js"; +import MeteoriumEmbedBuilder from "../classes/embedBuilder.js"; +import type { MeteoriumEvent } from "./eventsEntry.js"; +import moment from "moment"; + +export const Event: MeteoriumEvent<"ready"> = { + event: "ready", + async callback(client) { + const readyNS = client.events.eventNS.getNamespace("ReadyInit"); + const runtimeNS = client.logging.getNamespace("Runtime"); + const startTime = new Date(); + const interJson = client.interactions.generateAppsJsonData(); + + // Register to global interactions registry + readyNS.info("Registering to global interactions registry"); + await client.application.commands.set(interJson); + + // Register to guild interactions registry + readyNS.info("Registering guild interactions registry"); + client.config.ApplicationDeployGuildIds.forEach(async (guildId) => { + const guild = await client.guilds.fetch(guildId).catch(() => null); + if (!guild) return readyNS.error(`Cannot get guild ${guildId} for registering guild interactions registry`); + readyNS.verbose(`Registering guild interactions registry -> ${guildId}`); + await guild.commands.set(interJson); + }); + + // Set user presence + readyNS.info("Setting client presence"); + client.user.setPresence({ + status: "idle", + activities: [{ name: "no", type: ActivityType.Playing }], + }); + + // Hook to error events + const uncaughtExceptionNS = runtimeNS.getNamespace("UncaughtException"); + const unhandledRejectionNS = runtimeNS.getNamespace("UnhandledRejection"); + process.on("uncaughtException", async (err) => { + const inspected = util.inspect(err); + const runtimeLogPromises: Promise[] = []; + uncaughtExceptionNS.error(inspected); + + const embed = new MeteoriumEmbedBuilder() + .setTitle("Uncaught exception occurred") + .setDescription(codeBlock(inspected.substring(0, 4500))) + .setErrorColor(); + + client.config.RuntimeLogChannelIds.forEach(async (channelId) => { + const channel = await client.channels.fetch(channelId).catch(() => null); + if (!channel || !channel.isTextBased()) + return uncaughtExceptionNS.warn(`could not send log to ${channelId}`); + return runtimeLogPromises.push(channel.send({ embeds: [embed] })); + }); + + await Promise.all(runtimeLogPromises); + await client.destroy(); + return process.exit(1); + }); + process.on("unhandledRejection", async (err: Error | any) => { + const inspected = util.inspect(err); + const runtimeLogPromises: Promise[] = []; + unhandledRejectionNS.error(inspected); + + const embed = new MeteoriumEmbedBuilder() + .setTitle("Unhandled rejection occurred") + .setDescription(codeBlock(inspected.substring(0, 4500))) + .setErrorColor(); + + client.config.RuntimeLogChannelIds.forEach(async (channelId) => { + const channel = await client.channels.fetch(channelId).catch(() => null); + if (!channel || !channel.isTextBased()) + return unhandledRejectionNS.warn(`could not send log to ${channelId}`); + return runtimeLogPromises.push(channel.send({ embeds: [embed] })); + }); + + await Promise.all(runtimeLogPromises); + await client.destroy(); + return process.exit(1); + }); + + // Exit handling + const exitNS = runtimeNS.getNamespace("Exit"); + async function exitHandler() { + exitNS.info("Bot shutting down!"); + const runtimeLogPromises: Promise[] = []; + const currentTime = new Date(); + const embed = new MeteoriumEmbedBuilder() + .setTitle("Bot is shutting down") + .setDescription("The bot is now shutting down... (SIGTERM/SIGINT)") + .addFields([ + { name: "Started at", value: `${time(startTime, "F")} (${time(startTime, "R")})` }, + { name: "Shutted down at", value: `${time(currentTime, "F")} (${time(currentTime, "R")})` }, + ]) + .setErrorColor(); + + client.config.RuntimeLogChannelIds.forEach(async (channelId) => { + const channel = await client.channels.fetch(channelId).catch(() => null); + if (!channel || !channel.isTextBased()) return exitNS.warn(`could not send log to ${channelId}`); + return runtimeLogPromises.push(channel.send({ embeds: [embed] })); + }); + + await Promise.all(runtimeLogPromises); + await client.destroy(); + return process.exit(0); + } + process.on("SIGTERM", exitHandler); + process.on("SIGINT", exitHandler); + + // Send ready message + readyNS.info("Bot ready"); + const runtimeLogPromises: Promise[] = []; + const embed = new MeteoriumEmbedBuilder() + .setTitle("Bot is online") + .setDescription("The bot is now online") + .addFields([{ name: "Started at", value: `${time(startTime, "F")} (${time(startTime, "R")})` }]) + .setNormalColor(); + + client.config.RuntimeLogChannelIds.forEach(async (channelId) => { + const channel = await client.channels.fetch(channelId).catch(() => null); + if (!channel || !channel.isTextBased()) return readyNS.warn(`could not send log to ${channelId}`); + return runtimeLogPromises.push(channel.send({ embeds: [embed] })); + }); + + await Promise.all(runtimeLogPromises); + + return; + }, + once: true, +}; diff --git a/src/index.ts b/src/index.ts index 2415731..a32c9c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,14 @@ -import { MeteoriumClient } from "./util/MeteoriumClient"; +import MeteoriumClient from "./classes/client.js"; import { IntentsBitField } from "discord.js"; -const Intents = new IntentsBitField(); -Intents.add( - IntentsBitField.Flags.Guilds, - IntentsBitField.Flags.GuildMembers, - IntentsBitField.Flags.GuildVoiceStates, - IntentsBitField.Flags.GuildInvites, - IntentsBitField.Flags.GuildEmojisAndStickers, - IntentsBitField.Flags.GuildMessages, - IntentsBitField.Flags.GuildMessageReactions, - IntentsBitField.Flags.GuildPresences, - IntentsBitField.Flags.GuildModeration, -); - -const Client = new MeteoriumClient({ - intents: Intents, +const client = new MeteoriumClient({ + intents: [ + IntentsBitField.Flags.Guilds, + IntentsBitField.Flags.GuildMembers, + IntentsBitField.Flags.GuildMessages, + IntentsBitField.Flags.GuildModeration, + IntentsBitField.Flags.GuildVoiceStates, + ], }); -Client.login(); +await client.login(); diff --git a/src/installUtils/README.md b/src/installUtils/README.md new file mode 100644 index 0000000..16e6419 --- /dev/null +++ b/src/installUtils/README.md @@ -0,0 +1 @@ +This folder contains installation/database utilities (e.g. fixing something, filling missing data), all the scripts here are entry point scripts. diff --git a/src/installUtils/enableGuildFeature.ts b/src/installUtils/enableGuildFeature.ts new file mode 100644 index 0000000..47f22f1 --- /dev/null +++ b/src/installUtils/enableGuildFeature.ts @@ -0,0 +1,20 @@ +import process from "node:process"; +import MeteoriumClient from "../classes/client.js"; +import { IntentsBitField } from "discord.js"; +import { GuildFeatures } from "@prisma/client"; + +const client = new MeteoriumClient({ + intents: [IntentsBitField.Flags.Guilds], +}); +const egfNS = client.logging.registerNamespace("InstallUtils").registerNamespace("EnableGuildFeature"); +await client.login(); + +const guildId = process.env.METEORIUM_EGF_GUILDID; +const feature = GuildFeatures[process.env.METEORIUM_EGF_FEATURENAME as "Moderation"]; +if (guildId && feature) await client.guildFeatures.enableFeature(guildId, feature); +else egfNS.error(`Configuration is incorrect, detected:\nGuild id: ${guildId}\nFeature: ${feature}`); + +await client.destroy(); + +egfNS.info("Done"); +process.exit(0); diff --git a/src/installUtils/fillMissingGuildData.ts b/src/installUtils/fillMissingGuildData.ts new file mode 100644 index 0000000..1edfa83 --- /dev/null +++ b/src/installUtils/fillMissingGuildData.ts @@ -0,0 +1,30 @@ +import process from "node:process"; +import MeteoriumClient from "../classes/client.js"; +import { IntentsBitField } from "discord.js"; + +const client = new MeteoriumClient({ + intents: [IntentsBitField.Flags.Guilds], +}); +const missingGDNS = client.logging.registerNamespace("InstallUtils").registerNamespace("FillMissingGuildData"); +await client.login(); + +const guilds = await client.guilds.fetch({ limit: 200 }); +let totalCount = 0; // wheres my ``Array.count``?? +const data: Array<{ GuildId: string }> = guilds.map((guild) => { + totalCount += 1; + return { GuildId: guild.id }; +}); + +missingGDNS.info(`Inserting the following guild ids to the Guild table: (${totalCount})`); + +const result = await client.db.guild.createMany({ + data: data, + skipDuplicates: true, +}); + +missingGDNS.info(`Finished, inserted rows: ${result.count}`); + +await client.destroy(); + +missingGDNS.info("Done"); +process.exit(0); diff --git a/src/interactions/commands/index.ts b/src/interactions/commands/index.ts new file mode 100644 index 0000000..ad8ca47 --- /dev/null +++ b/src/interactions/commands/index.ts @@ -0,0 +1,17 @@ +// Tests +export * as Test from "./tests/test.js"; +export * as TestError from "./tests/error.js"; +export * as TestDefferedError from "./tests/deferredError.js"; + +// Moderation +export * as Case from "./moderation/case.js"; +export * as Cases from "./moderation/cases.js"; +export * as RemoveCase from "./moderation/removeCase.js"; +export * as EditCase from "./moderation/editCase.js"; +export * as CreateCase from "./moderation/createCase.js"; +export * as Warn from "./moderation/warn.js"; +export * as Mute from "./moderation/mute.js"; +export * as Kick from "./moderation/kick.js"; +export * as TempBan from "./moderation/tempBan.js"; +export * as Ban from "./moderation/ban.js"; +export * as UnBan from "./moderation/unban.js"; diff --git a/src/interactions/commands/moderation/ban.ts b/src/interactions/commands/moderation/ban.ts new file mode 100644 index 0000000..95faabb --- /dev/null +++ b/src/interactions/commands/moderation/ban.ts @@ -0,0 +1,150 @@ +import util from "node:util"; +import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { GuildFeatures, ModerationAction } from "@prisma/client"; +import ms from "../../../classes/ms.js"; +import type { MeteoriumChatCommand } from "../../index.js"; +import MeteoriumEmbedBuilder from "../../../classes/embedBuilder.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("ban") + .setDescription("Creates a ban punishment") + .addUserOption((option) => + option.setName("user").setDescription("The user that will be punished").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .addBooleanOption((option) => + option.setName("notappealable").setDescription("Is this ban appealable?").setRequired(false), + ) + .addStringOption((option) => + option + .setName("delmsghistory") + .setDescription("Delete message history time") + .setRequired(false) + .setAutocomplete(true), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const user = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + const proof = interaction.options.getAttachment("proof", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const notAppealable = interaction.options.getBoolean("notappealable", false); + const delMsgHistoryTime = interaction.options.getString("delmsghistory", false); + const moderator = interaction.user; + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + if (user.bot) return await interaction.reply({ content: "Moderating bots aren't possible.", ephemeral: true }); + + // Defer reply + await interaction.deferReply({ ephemeral: true }); + + // Create case + const { caseId, embed, fullEmbed } = await client.dbUtils.createModerationCase( + { + Action: ModerationAction.Ban, + GuildId: interaction.guildId, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof?.url, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: moderationAttach?.url, + NotAppealable: notAppealable || undefined, + }, + async function (caseDb) { + // Get guild settings + const guildSettings = await client.db.guild.findUnique({ where: { GuildId: interaction.guildId } }); + if (!guildSettings) throw new Error(`no guild settings for guild ${interaction.guildId}`); + + // Get guild member object + const member = await interaction.guild.members.fetch(user); + if (!member) throw new Error(`could not get guild member object for ${user.id}`); + + // Case embed + const embed = await client.dbUtils.generateCaseEmbedFromData( + { + ...caseDb, + PublicLogMsgId: "", + Removed: false, + }, + undefined, + false, + false, + ); + + // Appeal embed + const appealEmbed = new MeteoriumEmbedBuilder() + .setTitle(notAppealable ? "You cannot appeal your ban." : "Your ban is appealable.") + .setDescription( + notAppealable + ? "Your ban was marked unappealable, you have been permanently banned." + : guildSettings.BanAppealLink != "" + ? "You can appeal your ban, use the following link below to appeal." + : "You can appeal your ban, contact a server moderator.", + ) + .setErrorColor(); + if (!notAppealable && guildSettings.BanAppealLink != "") + appealEmbed.addFields([{ name: "Ban appeal link", value: guildSettings.BanAppealLink }]); + + // Send + try { + const DirectMessageChannnel = await user.createDM(); + await DirectMessageChannnel.send({ embeds: [embed, appealEmbed] }); + } catch (err) { + client.logging + .getNamespace("Moderation") + .getNamespace("Ban") + .error(`Could not send direct message to ${user.id}:\n${util.inspect(err)}`); + } + + // Ban + const delMsgSeconds = delMsgHistoryTime ? ms(delMsgHistoryTime) * 1000 : undefined; + await member.ban({ + reason: `Case #${caseDb.CaseId} from ${moderator.username} (${moderator.id}): ${reason}`, + deleteMessageSeconds: delMsgSeconds + ? delMsgSeconds >= 604800 + ? 604800 + : delMsgSeconds + : undefined, + }); + return; + }, + ); + + return await interaction.editReply({ content: `Case #${caseId} created.`, embeds: [embed, fullEmbed] }); + }, + async autocomplete(interaction, _) { + const focus = interaction.options.getFocused(true); + if (focus.name == "delmsghistory") + return await interaction.respond([ + { name: "1 day", value: "1d" }, + { name: "2 days", value: "2d" }, + { name: "3 days", value: "3d" }, + { name: "4 days", value: "4d" }, + { name: "5 days", value: "5d" }, + { name: "6 days", value: "6d" }, + { name: "7 days", value: "7d" }, + ]); + return await interaction.respond([]); + }, +}; diff --git a/src/interactions/commands/moderation/case.ts b/src/interactions/commands/moderation/case.ts new file mode 100644 index 0000000..ecad5d6 --- /dev/null +++ b/src/interactions/commands/moderation/case.ts @@ -0,0 +1,53 @@ +import { PermissionFlagsBits, SlashCommandBuilder, userMention, time } from "discord.js"; +import { GuildFeatures } from "@prisma/client"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("case") + .setDescription("Gives information about a recorded moderation case") + .addNumberOption((option) => + option.setName("caseid").setDescription("The id of the case you want to view").setRequired(true), + ) + .addNumberOption((option) => + option + .setName("historylevel") + .setDescription("The history level, set to 0 to view the original case") + .setRequired(false), + ) + .addBooleanOption((option) => + option + .setName("ephemeral") + .setDescription("If true, response will be shown only to you") + .setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ViewAuditLog) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const caseId = interaction.options.getNumber("caseid", true); + const historyLevel = interaction.options.getNumber("historylevel", false) || undefined; + const ephemeral = interaction.options.getBoolean("ephemeral", false) || false; + + // Defer the reply + await interaction.deferReply({ ephemeral: ephemeral }); + + // Get case data + const embed = await client.dbUtils.generateCaseEmbedFromCaseId( + interaction.guildId, + caseId, + interaction.user, + true, + historyLevel, + ); + if (!embed) return await interaction.editReply("The case you specified doesn't exist."); + + // Logging + await client.dbUtils.sendGuildLog(interaction.guildId, { + content: `Case #${caseId} viewed.`, + embeds: [embed], + }); + + return await interaction.editReply({ embeds: [embed] }); + }, +}; diff --git a/src/interactions/commands/moderation/cases.ts b/src/interactions/commands/moderation/cases.ts new file mode 100644 index 0000000..eaf4114 --- /dev/null +++ b/src/interactions/commands/moderation/cases.ts @@ -0,0 +1,178 @@ +import { PermissionFlagsBits, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { ModerationAction, GuildFeatures } from "@prisma/client"; +import type { MeteoriumChatCommand } from "../../index.js"; +import MeteoriumEmbedBuilder from "../../../classes/embedBuilder.js"; +import { CaseData } from "../../../classes/dbUtils.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("cases") + .setDescription("Gives information about a recorded moderation case") + .addUserOption((option) => + option + .setName("user") + .setDescription("The target user you want to check for recorded cases") + .setRequired(false), + ) + .addBooleanOption((option) => + option + .setName("inclremoved") + .setDescription("If true, removed cases will also be included") + .setRequired(false), + ) + .addBooleanOption((option) => + option + .setName("ephemeral") + .setDescription("If true, response will be shown only to you") + .setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ViewAuditLog) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const user = interaction.options.getUser("user", false) || undefined; + const ephemeral = interaction.options.getBoolean("ephemeral", false) || false; + + // Defer reply + const sentReplyMsg = await interaction.deferReply({ ephemeral: ephemeral, fetchReply: true }); + + // Get cases + const cases = await client.dbUtils.getCasesWithLatestHistory(interaction.guildId, user?.id); + + // Check if theres no cases to show + if (cases.length == 0) + return interaction.editReply({ + embeds: [ + new MeteoriumEmbedBuilder(interaction.user) + .setAuthor({ + name: `Cases | ${user ? `${user.username} (${user.id})` : interaction.guild.name}`, + iconURL: user + ? user.displayAvatarURL({ extension: "png", size: 256 }) + : interaction.guild.iconURL({ extension: "png", size: 256 }) || undefined, + }) + .setDescription( + user + ? "This user has no recorded moderation cases" + : "This server has no recorded moderation cases", + ), + ], + }); + + // Create paged cases array + const pagedCases: Array> = []; + let warns = 0; + let mutes = 0; + let kicks = 0; + let tempBans = 0; + let bans = 0; + let unbans = 0; + pagedCases.push([]); + for (let index = 0; index < cases.length; index++) { + const caseData = cases[index]; + if ((index + 1) % 10 == 0) pagedCases.push([]); + pagedCases.at(-1)!.push(caseData); + + switch (caseData.Action) { + case ModerationAction.Warn: { + warns += 1; + break; + } + case ModerationAction.Mute: { + mutes += 1; + break; + } + case ModerationAction.Kick: { + kicks += 1; + break; + } + case ModerationAction.TempBan: { + tempBans += 1; + break; + } + case ModerationAction.Ban: { + bans += 1; + break; + } + case ModerationAction.Unban: { + unbans += 1; + break; + } + default: { + throw new Error("impossible switch statement reach"); + } + } + } + + // Function to create a embed containing cases based on current pagination index + function createPageEmbed(index: number) { + if (!pagedCases[index]) throw new Error(`invalid page index ${index}`); + return new MeteoriumEmbedBuilder(interaction.user) + .setAuthor({ + name: `Cases | ${user ? `${user.username} (${user.id})` : interaction.guild.name}`, + iconURL: user + ? user.displayAvatarURL({ extension: "png", size: 256 }) + : interaction.guild.iconURL({ extension: "png", size: 256 }) || undefined, + }) + .setFields( + pagedCases[index].map((caseData) => ({ + name: `Case ${caseData.CaseId} - ${caseData.Action}${caseData.Removed ? " - [R]" : ""}`, + value: caseData.Reason, + })), + ) + .setFooter({ + text: `Page ${index + 1}/${pagedCases.length} | Warned: ${warns} | Muted: ${mutes} | Kicked: ${kicks} | Banned: ${bans} | TempBanned: ${tempBans} | Unbanned: ${unbans}`, + }); + } + + // Function to create action row containing buttons to control the index + function createActionRow(index: number) { + return new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setCustomId(`${index - 1}`) + .setLabel("Previous") + .setEmoji({ name: "◀️" }) + .setStyle(ButtonStyle.Primary) + .setDisabled(index <= 0), + new ButtonBuilder() + .setCustomId(`${index + 1}`) + .setLabel("Next") + .setEmoji({ name: "▶️" }) + .setStyle(ButtonStyle.Primary) + .setDisabled(index < 0 || index == pagedCases.length - 1), + ]); + } + + // Function to create message option based on current index + function createMessageOption(index: number) { + return { + embeds: [createPageEmbed(index)], + components: pagedCases.length > 1 ? [createActionRow(index)] : undefined, + }; + } + + // Set to starting point + await interaction.editReply(createMessageOption(0)); + + // Collector + const collector = sentReplyMsg.createMessageComponentCollector({ idle: 150000 }); + collector.on("collect", async (collectInteraction) => { + if (collectInteraction.user.id != interaction.user.id) { + await collectInteraction.reply({ + content: "Only the user who requested this command can control the page index.", + ephemeral: true, + }); + return; + } + + await interaction.editReply(createMessageOption(Number(collectInteraction.customId))); + return; + }); + collector.on("end", async () => { + if (pagedCases.length == 1) return; + await interaction.editReply({ components: [createActionRow(-1)] }); + return; + }); + + return; + }, +}; diff --git a/src/interactions/commands/moderation/createCase.ts b/src/interactions/commands/moderation/createCase.ts new file mode 100644 index 0000000..93bde91 --- /dev/null +++ b/src/interactions/commands/moderation/createCase.ts @@ -0,0 +1,284 @@ +import { PermissionFlagsBits, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import type { MeteoriumChatCommand } from "../../index.js"; +import { CaseData } from "../../../classes/dbUtils.js"; +import { ModerationAction, GuildFeatures } from "@prisma/client"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("createcase") + .setDescription("Creates a new case (for actions taken when bot was offline)") + .addStringOption((option) => + option + .setName("action") + .setDescription("The action moderator took") + .setRequired(true) + .addChoices( + { name: "ban", value: "ban" }, + { name: "unban", value: "unban" }, + { name: "kick", value: "kick" }, + { name: "mute", value: "mute" }, + { name: "warn", value: "warn" }, + { name: "tempban", value: "tempban" }, + ), + ) + .addUserOption((option) => + option.setName("user").setDescription("The user that received moderation").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(true), + ) + .addUserOption((option) => + option + .setName("moderator") + .setDescription("The moderator who took moderated the user, if empty defaults to you") + .setRequired(false), + ) + .addNumberOption((option) => + option + .setName("relcid") + .setDescription("Related case id (in unban, this is the ban case id)") + .setRequired(false), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("duration").setDescription("The duration of the mute/temp-ban").setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .addBooleanOption((option) => + option.setName("notappealable").setDescription("Is this ban appealable?").setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const actionStr = interaction.options.getString("action", true); + const user = interaction.options.getUser("user", true); + const moderator = interaction.options.getUser("moderator", false) || interaction.user; + const relatedCaseId = interaction.options.getNumber("relcid", false); + const reason = interaction.options.getString("reason", true); + const proof = interaction.options.getAttachment("proof", false); + const duration = interaction.options.getString("duration", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const notAppealable = interaction.options.getBoolean("notappealable", false); + + // Determine action + let action: ModerationAction; + switch (actionStr) { + case "ban": { + action = ModerationAction.Ban; + break; + } + case "tempban": { + action = ModerationAction.TempBan; + break; + } + case "kick": { + action = ModerationAction.Kick; + break; + } + case "mute": { + action = ModerationAction.Mute; + break; + } + case "warn": { + action = ModerationAction.Warn; + break; + } + case "unban": { + action = ModerationAction.Unban; + break; + } + default: { + throw new Error("CreateCase action switch reached impossible conclusion"); + } + } + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + if (user.bot) return await interaction.reply({ content: "Moderating bots aren't possible.", ephemeral: true }); + if (action == ModerationAction.Unban && relatedCaseId == null) + return await interaction.reply({ + content: "Unban cases require a related ban moderation case.", + ephemeral: true, + }); + if ((action == ModerationAction.Mute || action == ModerationAction.TempBan) && duration == null) + return await interaction.reply({ content: "Missing duration field.", ephemeral: true }); + if (action == ModerationAction.Ban && notAppealable == null) + return await interaction.reply({ + content: "Not appealable field must be explicitly set.", + ephemeral: true, + }); + + // Defer reply + const sentReplyMsg = await interaction.deferReply({ ephemeral: true, fetchReply: true }); + + // Get guild settings + const guildSettings = await client.db.guild.findUnique({ where: { GuildId: interaction.guildId } }); + if (!guildSettings) throw new Error(`no guild settings for guild ${interaction.guildId}`); + + // Get related case id + const relatedCaseData: CaseData | null = relatedCaseId + ? (await client.dbUtils.getCaseData(interaction.guildId, relatedCaseId)) || null + : null; + if (action == ModerationAction.Unban && relatedCaseData == null) + return await interaction.editReply({ content: "Could not fetch related case data." }); + if (action == ModerationAction.Unban && relatedCaseData!.Action != ModerationAction.Ban) + return await interaction.editReply({ content: "The related case is not a ban case." }); + if (action == ModerationAction.Unban && !relatedCaseData!.Active) + return await interaction.editReply({ content: "This related ban case is no longer active." }); + if (action == ModerationAction.Unban && relatedCaseData!.TargetUserId != user.id) + return await interaction.editReply({ + content: "The user id does not match between the data specified here and the related case data.", + }); + + // Active ban case check + const prevBanCase = await client.db.moderationCase.findFirst({ + where: { + OR: [ + { GuildId: interaction.guildId, TargetUserId: user.id, Action: ModerationAction.Ban }, + { GuildId: interaction.guildId, TargetUserId: user.id, Action: ModerationAction.TempBan }, + ], + }, + orderBy: { + GlobalCaseId: "desc", + }, + }); + if (prevBanCase && prevBanCase.Active) + return await interaction.editReply({ + content: "This user currently has a active ban/tempban case.", + }); + + // Generate embed + const embed = await client.dbUtils.generateCaseEmbedFromData( + { + GlobalCaseId: -1, + CaseId: -1, + Action: action, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Active: true, + RelatedCaseId: relatedCaseId || -1, + PublicLogMsgId: "", + CreatedAt: new Date(), + + Reason: reason, + AttachmentProof: proof ? proof.url : "", + Duration: duration || "0", + ModeratorNote: moderationNote || "", + ModeratorAttachment: moderationAttach ? moderationAttach.url : "", + NotAppealable: notAppealable || false, + Removed: false, + }, + interaction.user, + true, + ); + + // Action row buttons + const actionRow = new ActionRowBuilder().addComponents([ + new ButtonBuilder().setLabel("No").setCustomId("no").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setLabel("Yes").setCustomId("yes").setStyle(ButtonStyle.Danger), + ]); + + // Edit the reply + await interaction.editReply({ + content: + "Are you sure you want to create a new case with the following data?\n(Note that some data may change after you press yes, mainly dates and ids)", + embeds: [embed], + components: [actionRow], + }); + + // Collector + const collector = sentReplyMsg.createMessageComponentCollector({ idle: 150000 }); + collector.on("collect", async (collectInteraction) => { + switch (collectInteraction.customId) { + case "yes": { + // Push into db + const data = await client.db.moderationCase.create({ + data: { + GuildId: interaction.guildId, + CaseId: + (await client.db.moderationCase.count({ where: { GuildId: interaction.guildId } })) + 1, + Action: action, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof ? proof.url : "", + Duration: + action == ModerationAction.Mute || action == ModerationAction.TempBan + ? duration! + : undefined, + ModeratorNote: moderationNote || "", + ModeratorAttachment: moderationAttach ? moderationAttach.url : "", + NotAppealable: action == ModerationAction.Ban ? notAppealable! : undefined, + RelatedCaseId: action == ModerationAction.Unban ? relatedCaseId! : undefined, + }, + }); + + // Update related ban case if unban + if (action == ModerationAction.Unban) + await client.db.moderationCase.update({ + where: { GlobalCaseId: relatedCaseData!.GlobalCaseId }, + data: { + Active: false, + RelatedCaseId: data.CaseId, + }, + }); + + // Generate new embed based on db data + const dbDataEmbedFull = await client.dbUtils.generateCaseEmbedFromCaseId( + interaction.guildId, + data.CaseId, + interaction.user, + true, + ); + + const dbDataEmbedPublic = await client.dbUtils.generateCaseEmbedFromCaseId( + interaction.guildId, + data.CaseId, + interaction.user, + false, + ); + + // Edit reply + await interaction.editReply({ + content: `Created new case #${data.CaseId}.`, + embeds: dbDataEmbedFull ? [dbDataEmbedFull] : [], // TODO: Handle this better? + components: [], + }); + + // Post in public moderation log + if (dbDataEmbedPublic) + await client.dbUtils.sendGuildPubLog(interaction.guildId, { embeds: [dbDataEmbedPublic] }); + + // Post in internal server log + await client.dbUtils.sendGuildLog(interaction.guildId, { + content: `New case manually created: #${data.CaseId}`, + embeds: dbDataEmbedFull ? [dbDataEmbedFull] : [], + }); + + break; + } + case "no": { + await interaction.editReply({ content: "Case creation cancelled.", embeds: [], components: [] }); + break; + } + } + }); + collector.on("end", async () => { + await interaction.editReply({ content: "Timed out.", embeds: [], components: [] }); + return; + }); + }, +}; diff --git a/src/interactions/commands/moderation/editCase.ts b/src/interactions/commands/moderation/editCase.ts new file mode 100644 index 0000000..0741b47 --- /dev/null +++ b/src/interactions/commands/moderation/editCase.ts @@ -0,0 +1,179 @@ +import { PermissionFlagsBits, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import type { MeteoriumChatCommand } from "../../index.js"; +import { CaseData } from "../../../classes/dbUtils.js"; +import { GuildFeatures } from "@prisma/client"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("editcase") + .setDescription("Edits a case") + .addNumberOption((option) => + option.setName("caseid").setDescription("The id of the case you want to view").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(false), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("duration").setDescription("The duration of the mute/temp-ban").setRequired(false), + ) + .addBooleanOption((option) => + option.setName("notappealable").setDescription("Is this ban appealable?").setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .addBooleanOption((option) => + option + .setName("remproof") + .setDescription("Remove the attachment proof (this takes priority)") + .setRequired(false), + ) + .addBooleanOption((option) => + option + .setName("remmodattach") + .setDescription("Remove the moderation attachment (this takes priority)") + .setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const caseId = interaction.options.getNumber("caseid", true); + const reason = interaction.options.getString("reason", false); + const proof = interaction.options.getAttachment("proof", false); + const duration = interaction.options.getString("duration", false); + const notAppealable = interaction.options.getBoolean("notappealable", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const removeProof = interaction.options.getBoolean("remproof", false) || false; + const removeModAttach = interaction.options.getBoolean("remmodattach", false) || false; + + // Check if any field is modified + if ( + !reason && + !proof && + !duration && + typeof notAppealable != "boolean" && + !moderationNote && + !moderationAttach && + !removeProof && + !removeModAttach + ) + return interaction.reply({ content: "There are no modified fields.", ephemeral: true }); + + // Defer reply + const sentReplyMsg = await interaction.deferReply({ ephemeral: true, fetchReply: true }); + + // Case data + const caseData = await client.dbUtils.getCaseData(interaction.guildId, caseId); + if (!caseData) return await interaction.editReply({ content: `Case #${caseId} does not exist.` }); + if (caseData.Removed) return await interaction.editReply({ content: `Case #${caseId} is a removed case.` }); + + // New case data + const newCaseData: CaseData = { + GlobalCaseId: caseData.GlobalCaseId, + CaseId: caseData.CaseId, + Action: caseData.Action, + TargetUserId: caseData.TargetUserId, + ModeratorUserId: caseData.ModeratorUserId, + Active: caseData.Active, + RelatedCaseId: caseData.RelatedCaseId, + PublicLogMsgId: caseData.PublicLogMsgId, + CreatedAt: caseData.CreatedAt, + + Reason: reason ? reason : caseData.Reason, + AttachmentProof: removeProof ? "" : proof ? proof.url : caseData.AttachmentProof, + Duration: duration ? duration : caseData.Duration, + ModeratorNote: moderationNote ? moderationNote : caseData.ModeratorNote, + ModeratorAttachment: removeModAttach + ? "" + : moderationAttach + ? moderationAttach.url + : caseData.ModeratorAttachment, + NotAppealable: typeof notAppealable == "boolean" ? notAppealable : caseData.NotAppealable, + Removed: caseData.Removed, + }; + + // Embed previews + const oldDataEmbed = await client.dbUtils.generateCaseEmbedFromData(caseData, interaction.user, true); + const newDataEmbed = await client.dbUtils.generateCaseEmbedFromData(newCaseData, interaction.user, true); + + // Action row buttons + const actionRow = new ActionRowBuilder().addComponents([ + new ButtonBuilder().setLabel("No").setCustomId("no").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setLabel("Yes").setCustomId("yes").setStyle(ButtonStyle.Danger), + ]); + + // Edit the reply + await interaction.editReply({ + content: "Are you sure you want to remove this edit case? (new top, old bottom)", + embeds: [newDataEmbed, oldDataEmbed], + components: [actionRow], + }); + + // Collector + const collector = sentReplyMsg.createMessageComponentCollector({ idle: 150000 }); + collector.on("collect", async (collectInteraction) => { + switch (collectInteraction.customId) { + case "yes": { + // Create patch + await client.db.moderationCaseHistory.create({ + data: { + GlobalCaseId: caseData.GlobalCaseId, + EditorUserId: interaction.user.id, + Reason: reason || undefined, + AttachmentProof: removeProof ? "" : proof ? proof.url : undefined, + Duration: duration || undefined, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: removeModAttach + ? "" + : moderationAttach + ? moderationAttach.url + : undefined, + NotAppealable: typeof notAppealable == "boolean" ? notAppealable : undefined, + Removed: undefined, + }, + }); + + // Edit the reply + const editCount = await client.db.moderationCaseHistory.count({ where: { GlobalCaseId: caseId } }); + await interaction.editReply({ + content: `Case #${caseId} edited. (Edit #${editCount})`, + embeds: [newDataEmbed], + components: [], + }); + + // Logging + await client.dbUtils.sendGuildLog(interaction.guildId, { + content: `Case #${caseId} edited. (Edit #${editCount}) (new top, previous bottom)`, + embeds: [ + await client.dbUtils.generateCaseEmbedFromData(newCaseData, interaction.user, true, true), + await client.dbUtils.generateCaseEmbedFromData(caseData, interaction.user, true, true), + ], + }); + + break; + } + case "no": { + await interaction.editReply({ content: "Case editing cancelled.", embeds: [], components: [] }); + break; + } + } + + return; + }); + collector.on("end", async () => { + await interaction.editReply({ content: "Timed out.", embeds: [], components: [] }); + return; + }); + }, +}; diff --git a/src/interactions/commands/moderation/kick.ts b/src/interactions/commands/moderation/kick.ts new file mode 100644 index 0000000..026a409 --- /dev/null +++ b/src/interactions/commands/moderation/kick.ts @@ -0,0 +1,68 @@ +import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { GuildFeatures, ModerationAction } from "@prisma/client"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("kick") + .setDescription("Creates a kick punishment") + .addUserOption((option) => + option.setName("user").setDescription("The user that will be punished").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.KickMembers) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const user = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + const proof = interaction.options.getAttachment("proof", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const moderator = interaction.user; + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + if (user.bot) return await interaction.reply({ content: "Moderating bots aren't possible.", ephemeral: true }); + + // Defer reply + await interaction.deferReply({ ephemeral: true }); + + // Create case + const { caseId, embed, fullEmbed } = await client.dbUtils.createModerationCase( + { + Action: ModerationAction.Kick, + GuildId: interaction.guildId, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof?.url, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: moderationAttach?.url, + }, + async function (caseDb) { + const member = await interaction.guild.members.fetch(user); + if (!member) throw new Error(`could not get guild member object for ${user.id}`); + await member.kick(`Case #${caseDb.CaseId} from ${moderator.username} (${moderator.id}): ${reason}`); + return; + }, + ); + + return await interaction.editReply({ content: `Case #${caseId} created.`, embeds: [embed, fullEmbed] }); + }, +}; diff --git a/src/interactions/commands/moderation/mute.ts b/src/interactions/commands/moderation/mute.ts new file mode 100644 index 0000000..22d611f --- /dev/null +++ b/src/interactions/commands/moderation/mute.ts @@ -0,0 +1,77 @@ +import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { GuildFeatures, ModerationAction } from "@prisma/client"; +import ms from "../../../classes/ms.js"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("mute") + .setDescription("Creates a mute (timeout) punishment") + .addUserOption((option) => + option.setName("user").setDescription("The user that will be punished").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(true), + ) + .addStringOption((option) => + option.setName("duration").setDescription("The duration of the mute").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const user = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + const duration = interaction.options.getString("duration", true); + const proof = interaction.options.getAttachment("proof", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const moderator = interaction.user; + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + if (user.bot) return await interaction.reply({ content: "Moderating bots aren't possible.", ephemeral: true }); + + // Defer reply + await interaction.deferReply({ ephemeral: true }); + + // Create case + const { caseId, embed, fullEmbed } = await client.dbUtils.createModerationCase( + { + Action: ModerationAction.Mute, + GuildId: interaction.guildId, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof?.url, + Duration: duration, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: moderationAttach?.url, + }, + async function (caseDb) { + const member = await interaction.guild.members.fetch(user); + if (!member) throw new Error(`could not get guild member object for ${user.id}`); + await member.timeout( + ms(duration), + `Case #${caseDb.CaseId} from ${moderator.username} (${moderator.id}): ${reason}`, + ); + return; + }, + ); + + return await interaction.editReply({ content: `Case #${caseId} created.`, embeds: [embed, fullEmbed] }); + }, +}; diff --git a/src/interactions/commands/moderation/removeCase.ts b/src/interactions/commands/moderation/removeCase.ts new file mode 100644 index 0000000..8273a58 --- /dev/null +++ b/src/interactions/commands/moderation/removeCase.ts @@ -0,0 +1,139 @@ +import { PermissionFlagsBits, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { ModerationAction, GuildFeatures } from "@prisma/client"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("removecase") + .setDescription("Removes a moderation case") + .addNumberOption((option) => + option.setName("caseid").setDescription("The id of the case you want to remove").setRequired(true), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const caseId = interaction.options.getNumber("caseid", true); + + // Defer reply + const sentReplyMsg = await interaction.deferReply({ ephemeral: true, fetchReply: true }); + + // Get case data + const caseData = await client.dbUtils.getCaseData(interaction.guildId, caseId); + if (!caseData) return await interaction.editReply({ content: `Case #${caseId} does not exist.` }); + if (caseData.Removed) + return await interaction.editReply({ content: `Case #${caseId} has been already marked as removed.` }); + + // Get guild settings + const guildSettings = await client.db.guild.findUnique({ where: { GuildId: interaction.guildId } }); + if (!guildSettings) throw new Error(`no guild settings for guild ${interaction.guildId}`); + + // Case embed + const caseEmbed = await client.dbUtils.generateCaseEmbedFromData(caseData, interaction.user, true, false); + + // Action row buttons + const actionRow = new ActionRowBuilder().addComponents([ + new ButtonBuilder().setLabel("No").setCustomId("no").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setLabel("Yes").setCustomId("yes").setStyle(ButtonStyle.Danger), + ]); + + // Edit the reply + await interaction.editReply({ + content: "Are you sure you want to remove this case?", + embeds: [caseEmbed], + components: [actionRow], + }); + + // Collector + const collector = sentReplyMsg.createMessageComponentCollector({ idle: 150000 }); + collector.on("collect", async (collectInteraction) => { + switch (collectInteraction.customId) { + case "yes": { + // Mark case deleted + await client.db.moderationCaseHistory.create({ + data: { + GlobalCaseId: caseData.GlobalCaseId, + EditorUserId: interaction.user.id, + Removed: true, + }, + }); + + // Reply content + let replyContent = `Case #${caseData.CaseId} removed.`; + + // Untimeout/unban + if (caseData.Action == ModerationAction.Mute) { + const targetGuildMember = await interaction.guild.members + .fetch(caseData.TargetUserId) + .catch(() => null); + if (targetGuildMember) + targetGuildMember.timeout( + null, + `Case #${caseData.CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, + ); + else + replyContent += + "(Warning: could not remove the timeout. You will have to remove the timeout manually.)"; + } else if (caseData.Action == ModerationAction.TempBan) { + const targetUser = await client.users.fetch(caseData.TargetUserId).catch(() => null); + if (targetUser) + interaction.guild.members.unban( + targetUser, + `Case #${caseData.CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, + ); + else + replyContent += + "(Warning: could not remove the ban. You will have to remove the ban manually.)"; + + await client.db.activeTempBans.delete({ + where: { GlobalCaseId: caseData.GlobalCaseId }, + }); + } else if (caseData.Action == ModerationAction.Ban) { + const targetUser = await client.users.fetch(caseData.TargetUserId).catch(() => null); + if (targetUser) + interaction.guild.members.unban( + targetUser, + `Case #${caseData.CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, + ); + else + replyContent += + "(Warning: could not remove the ban. You will have to remove the ban manually.)"; + } + + // Delete public mod log message + const publogChannel = await interaction.guild.channels + .fetch(guildSettings.PublicModLogChannelId) + .catch(() => null); + const publogMessage = + publogChannel && publogChannel.isTextBased() + ? await publogChannel.messages.fetch(caseData.PublicLogMsgId).catch(() => null) + : null; + if (publogMessage && publogMessage.deletable) await publogMessage.delete(); + + // Edit interaction reply + await interaction.editReply({ content: replyContent, embeds: [], components: [] }); + + // Logging + await client.dbUtils.sendGuildLog(interaction.guildId, { + content: `Case #${caseId} removed.`, + embeds: [ + await client.dbUtils.generateCaseEmbedFromData(caseData, interaction.user, true, true), + ], + }); + + break; + } + case "no": { + await interaction.editReply({ content: "Case removal cancelled.", embeds: [], components: [] }); + break; + } + } + + return; + }); + collector.on("end", async () => { + await interaction.editReply({ content: "Timed out.", embeds: [], components: [] }); + return; + }); + }, +}; diff --git a/src/interactions/commands/moderation/tempBan.ts b/src/interactions/commands/moderation/tempBan.ts new file mode 100644 index 0000000..0cf887c --- /dev/null +++ b/src/interactions/commands/moderation/tempBan.ts @@ -0,0 +1,184 @@ +import util from "node:util"; +import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { GuildFeatures, ModerationAction } from "@prisma/client"; +import ms from "../../../classes/ms.js"; +import type { MeteoriumChatCommand } from "../../index.js"; +import MeteoriumEmbedBuilder from "../../../classes/embedBuilder.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("tempban") + .setDescription("Creates a temporary ban punishment") + .addUserOption((option) => + option.setName("user").setDescription("The user that will be punished").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(true), + ) + .addStringOption((option) => + option.setName("duration").setDescription("The duration of the temporary ban").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .addStringOption((option) => + option + .setName("delmsghistory") + .setDescription("Delete message history time") + .setRequired(false) + .setAutocomplete(true), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const user = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + const duration = interaction.options.getString("duration", true); + const proof = interaction.options.getAttachment("proof", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const delMsgHistoryTime = interaction.options.getString("delmsghistory", false); + const moderator = interaction.user; + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + if (user.bot) return await interaction.reply({ content: "Moderating bots aren't possible.", ephemeral: true }); + + // Defer reply + await interaction.deferReply({ ephemeral: true }); + + // Create case + const { caseId, embed, fullEmbed } = await client.dbUtils.createModerationCase( + { + Action: ModerationAction.TempBan, + GuildId: interaction.guildId, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof?.url, + Duration: duration, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: moderationAttach?.url, + }, + async function (caseDb) { + // Get guild settings + const guildSettings = await client.db.guild.findUnique({ where: { GuildId: interaction.guildId } }); + if (!guildSettings) throw new Error(`no guild settings for guild ${interaction.guildId}`); + + // Get guild member object + const member = await interaction.guild.members.fetch(user); + if (!member) throw new Error(`could not get guild member object for ${user.id}`); + + // Case embed + const embed = await client.dbUtils.generateCaseEmbedFromData( + { + ...caseDb, + PublicLogMsgId: "", + Removed: false, + }, + undefined, + false, + false, + ); + + // Send + try { + const DirectMessageChannnel = await user.createDM(); + await DirectMessageChannnel.send({ embeds: [embed] }); + } catch (err) { + client.logging + .getNamespace("Moderation") + .getNamespace("TempBan") + .error(`Could not send direct message to ${user.id}:\n${util.inspect(err)}`); + } + + // Ban + const delMsgSeconds = delMsgHistoryTime ? ms(delMsgHistoryTime) * 1000 : undefined; + await member.ban({ + reason: `Case #${caseDb.CaseId} from ${moderator.username} (${moderator.id}): ${reason}`, + deleteMessageSeconds: delMsgSeconds + ? delMsgSeconds >= 604800 + ? 604800 + : delMsgSeconds + : undefined, + }); + + // Create active temp ban entry + await client.db.activeTempBans.create({ data: { GlobalCaseId: caseDb.GlobalCaseId } }); + + return; + }, + ); + + return await interaction.editReply({ content: `Case #${caseId} created.`, embeds: [embed, fullEmbed] }); + }, + async autocomplete(interaction, _) { + const focus = interaction.options.getFocused(true); + if (focus.name == "delmsghistory") + return await interaction.respond([ + { name: "1 day", value: "1d" }, + { name: "2 days", value: "2d" }, + { name: "3 days", value: "3d" }, + { name: "4 days", value: "4d" }, + { name: "5 days", value: "5d" }, + { name: "6 days", value: "6d" }, + { name: "7 days", value: "7d" }, + ]); + return await interaction.respond([]); + }, + async initialize(client) { + const logNS = client.logging.getNamespace("Moderation").getNamespace("TempBan"); + setInterval(async () => { + await client.db.$transaction(async (tx) => { + const active = await tx.activeTempBans.findMany({ include: { Case: true } }); + const promises = active.map(async (active) => { + const current = new Date(); + const expiresAt = new Date(Number(active.Case.CreatedAt) + ms(active.Case.Duration)); + if (expiresAt <= current) { + logNS.verbose(`Processing unban for ${active.Case.TargetUserId} in ${active.Case.GuildId}`); + + // Unban + const guild = await client.guilds.fetch(active.Case.GuildId); + await guild.members.unban( + active.Case.TargetUserId, + `Case #${active.Case.CaseId}: Automatic unban from temporary ban.`, + ); + await tx.activeTempBans.delete({ where: { ActiveTempBanId: active.ActiveTempBanId } }); + await tx.moderationCase.update({ + where: { GlobalCaseId: active.GlobalCaseId }, + data: { Active: false }, + }); + + // Log + const logEmbed = await client.dbUtils.generateCaseEmbedFromCaseId( + active.Case.GuildId, + active.Case.CaseId, + undefined, + true, + ); + if (!logEmbed) + throw new Error( + `could not generate embed for case #${active.Case.CaseId}@${active.Case.GuildId} (${active.Case.GlobalCaseId})`, + ); + logEmbed.setTitle("Automatic unban"); + await client.dbUtils.sendGuildLog(active.Case.GuildId, { embeds: [logEmbed] }); + } + return; + }); + await Promise.all(promises); + return; + }); + }, 10000); + }, +}; diff --git a/src/interactions/commands/moderation/unban.ts b/src/interactions/commands/moderation/unban.ts new file mode 100644 index 0000000..98a6bfe --- /dev/null +++ b/src/interactions/commands/moderation/unban.ts @@ -0,0 +1,115 @@ +import { PermissionFlagsBits, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { GuildFeatures, ModerationAction } from "@prisma/client"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("unban") + .setDescription("Deactivate a ban punishment") + .addNumberOption((option) => + option.setName("relcid").setDescription("The ban case id that will be deactivated").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was unbanned").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user got unbanned") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const relatedCaseId = interaction.options.getNumber("relcid", true); + const reason = interaction.options.getString("reason", true); + const proof = interaction.options.getAttachment("proof", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const moderator = interaction.user; + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + + // Defer reply + const sentReplyMsg = await interaction.deferReply({ ephemeral: true, fetchReply: true }); + + // Get related ban case and validate it + const banCase = await client.dbUtils.getCaseData(interaction.guildId, relatedCaseId); + if (!banCase) return await interaction.editReply({ content: `Case #${relatedCaseId} doesn't exist.` }); + if (banCase.Action != ModerationAction.Ban) + return await interaction.editReply({ content: `Case #${relatedCaseId} isn't a ban case.` }); + + // Ban case embed + const banEmbed = await client.dbUtils.generateCaseEmbedFromData(banCase, interaction.user, true, false); + + // Action row buttons + const actionRow = new ActionRowBuilder().addComponents([ + new ButtonBuilder().setLabel("No").setCustomId("no").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setLabel("Yes").setCustomId("yes").setStyle(ButtonStyle.Danger), + ]); + + // Edit reply + await interaction.editReply({ + content: "Are you sure you want to deactivate (unban) the following case?", + embeds: [banEmbed], + components: [actionRow], + }); + + // Collector + const collector = sentReplyMsg.createMessageComponentCollector({ idle: 150000 }); + collector.on("collect", async (collectInteraction) => { + switch (collectInteraction.customId) { + case "yes": { + // Create case + const { caseId, embed } = await client.dbUtils.createModerationCase({ + Action: ModerationAction.Unban, + GuildId: interaction.guildId, + TargetUserId: banCase.TargetUserId, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof?.url, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: moderationAttach?.url, + RelatedCaseId: relatedCaseId, + }); + + await client.db.moderationCase.update({ + where: { GlobalCaseId: banCase.GlobalCaseId }, + data: { + Active: false, + RelatedCaseId: caseId, + }, + }); + + // Unban + await interaction.guild.members.unban(banCase.TargetUserId, `Case #${caseId}: ${reason}`); + + // Edit reply + await interaction.editReply({ + content: `Deactivated ban in case #${relatedCaseId}.`, + embeds: [embed], + components: [], + }); + break; + } + case "no": { + await interaction.editReply({ content: "Unban action cancelled.", embeds: [], components: [] }); + break; + } + } + }); + collector.on("end", async () => { + await interaction.editReply({ content: "Timed out.", embeds: [], components: [] }); + return; + }); + }, +}; diff --git a/src/interactions/commands/moderation/warn.ts b/src/interactions/commands/moderation/warn.ts new file mode 100644 index 0000000..c5c59af --- /dev/null +++ b/src/interactions/commands/moderation/warn.ts @@ -0,0 +1,60 @@ +import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { GuildFeatures, ModerationAction } from "@prisma/client"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("warn") + .setDescription("Creates a warning punishment") + .addUserOption((option) => + option.setName("user").setDescription("The user that will be punished").setRequired(true), + ) + .addStringOption((option) => + option.setName("reason").setDescription("The reason why this user was moderated").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("The attachment proof on why this user needed to be moderated") + .setRequired(false), + ) + .addStringOption((option) => + option.setName("modnote").setDescription("Internal moderation note").setRequired(false), + ) + .addAttachmentOption((option) => + option.setName("modattach").setDescription("Internal moderation attachment").setRequired(false), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .setDMPermission(false), + requiredFeature: GuildFeatures.Moderation, + async callback(interaction, client) { + const user = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason", true); + const proof = interaction.options.getAttachment("proof", false); + const moderationNote = interaction.options.getString("modnote", false); + const moderationAttach = interaction.options.getAttachment("modattach", false); + const moderator = interaction.user; + + // Sanity checks + if (moderator.bot) + return await interaction.reply({ content: "The moderator can't be a bot.", ephemeral: true }); + if (user.bot) return await interaction.reply({ content: "Moderating bots aren't possible.", ephemeral: true }); + + // Defer reply + await interaction.deferReply({ ephemeral: true }); + + // Create case + const { caseId, embed, fullEmbed } = await client.dbUtils.createModerationCase({ + Action: ModerationAction.Warn, + GuildId: interaction.guildId, + TargetUserId: user.id, + ModeratorUserId: moderator.id, + Reason: reason, + AttachmentProof: proof?.url, + ModeratorNote: moderationNote || undefined, + ModeratorAttachment: moderationAttach?.url, + }); + + return await interaction.editReply({ content: `Case #${caseId} created.`, embeds: [embed, fullEmbed] }); + }, +}; diff --git a/src/interactions/commands/tests/deferredError.ts b/src/interactions/commands/tests/deferredError.ts new file mode 100644 index 0000000..9631849 --- /dev/null +++ b/src/interactions/commands/tests/deferredError.ts @@ -0,0 +1,13 @@ +import { SlashCommandBuilder } from "discord.js"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("deferrortest") + .setDescription("Error handling during a command dispatching test with deferred reply") + .setDMPermission(true), + async callback(interaction, _) { + await interaction.deferReply(); + throw new Error("Intentional error"); + }, +}; diff --git a/src/interactions/commands/tests/error.ts b/src/interactions/commands/tests/error.ts new file mode 100644 index 0000000..8077f7c --- /dev/null +++ b/src/interactions/commands/tests/error.ts @@ -0,0 +1,12 @@ +import { SlashCommandBuilder } from "discord.js"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("errortest") + .setDescription("Error handling during command dispatching test") + .setDMPermission(true), + async callback() { + throw new Error("Intentional error"); + }, +}; diff --git a/src/interactions/commands/tests/test.ts b/src/interactions/commands/tests/test.ts new file mode 100644 index 0000000..2f7a6c7 --- /dev/null +++ b/src/interactions/commands/tests/test.ts @@ -0,0 +1,12 @@ +import { SlashCommandBuilder } from "discord.js"; +import type { MeteoriumChatCommand } from "../../index.js"; + +export const Command: MeteoriumChatCommand = { + interactionData: new SlashCommandBuilder() + .setName("test") + .setDescription("Test command for checking command handling and dispatching") + .setDMPermission(true), + async callback(interaction, _) { + return interaction.reply(`OK - DM: ${interaction.channel?.isDMBased() ? "Yes" : "No"}`); + }, +}; diff --git a/src/interactions/index.ts b/src/interactions/index.ts new file mode 100644 index 0000000..3789ec7 --- /dev/null +++ b/src/interactions/index.ts @@ -0,0 +1,311 @@ +import util from "node:util"; + +import { + ChatInputCommandInteraction, + AutocompleteInteraction, + MessageContextMenuCommandInteraction, + UserContextMenuCommandInteraction, + SlashCommandBuilder, + ContextMenuCommandBuilder, + RESTPostAPIChatInputApplicationCommandsJSONBody, + RESTPostAPIContextMenuApplicationCommandsJSONBody, + ApplicationCommandType, + Awaitable, + Collection, + Interaction, + codeBlock, + userMention, + User, +} from "discord.js"; +import { GuildFeatures } from "@prisma/client"; +import type MeteoriumClient from "../classes/client.js"; +import type { LoggingNamespace } from "../classes/logging.js"; + +import * as commands from "./commands/index.js"; +import * as userContextMenuActions from "./userContextMenuActions/index.js"; +import * as messageContextMenuActions from "./messageContextMenuActions/index.js"; +import MeteoriumEmbedBuilder from "../classes/embedBuilder.js"; + +export type MeteoriumChatCommand = { + interactionData: Pick & Pick; + requiredFeature?: GuildFeatures; + callback(interaction: ChatInputCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; + autocomplete?(interaction: AutocompleteInteraction<"cached">, client: MeteoriumClient): Awaitable; + initialize?(client: MeteoriumClient): Awaitable; +}; + +export type MeteoriumUserContextMenuAction = { + interactionData: ContextMenuCommandBuilder; + requiredFeature?: GuildFeatures; + callback(interaction: UserContextMenuCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; + initialize?(client: MeteoriumClient): Awaitable; +}; + +export type MeteoriumMessageContextMenuAction = { + interactionData: ContextMenuCommandBuilder; + requiredFeature?: GuildFeatures; + callback(interaction: MessageContextMenuCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; + initialize?(client: MeteoriumClient): Awaitable; +}; + +export default class MeteoriumInteractionManager { + public chatInputInteractions: Collection; + public userContextMenuActionInteractions: Collection; + public messageContextMenuActionInteractions: Collection; + public client: MeteoriumClient; + public loggingNS: LoggingNamespace; + + public constructor(client: MeteoriumClient) { + this.chatInputInteractions = new Collection(); + this.userContextMenuActionInteractions = new Collection(); + this.messageContextMenuActionInteractions = new Collection(); + this.client = client; + this.loggingNS = client.logging.registerNamespace("InteractionManager"); + } + + public registerChatInputInteractions() { + const regChatInputNS = this.loggingNS.getNamespace("Registration/ChatInput"); + + this.chatInputInteractions.clear(); + regChatInputNS.info("Registering chat input interactions"); + + for (const [Name, { Command }] of Object.entries(commands)) { + if (this.client.config.DontRegisterTestInteractions && Name.toLowerCase().startsWith("test")) continue; + regChatInputNS.verbose(`Registering -> ${Name} (${Command.interactionData.name})`); + this.chatInputInteractions.set(Name, Command); + if (Command.initialize) { + regChatInputNS.verbose(`Init -> ${Name} (${Command.interactionData.name})`); + Command.initialize(this.client); + } + } + + return; + } + + public registerUserContextMenuActionInteractions() { + const regUserContextMenuActionNS = this.loggingNS.getNamespace("Registration/UserContextMenuAction"); + + this.userContextMenuActionInteractions.clear(); + regUserContextMenuActionNS.info("Registering user context menu actions"); + + for (const [Name, { UserContextMenuAction }] of Object.entries(userContextMenuActions)) { + if (this.client.config.DontRegisterTestInteractions && Name.toLowerCase().startsWith("test")) continue; + regUserContextMenuActionNS.verbose( + `Registering -> ${Name} (${UserContextMenuAction.interactionData.name})`, + ); + if (UserContextMenuAction.interactionData.type != ApplicationCommandType.User) + throw new Error( + `invalid context menu action type for ${Name} (expected User, got ${UserContextMenuAction.interactionData.type})`, + ); + this.userContextMenuActionInteractions.set(Name, UserContextMenuAction); + if (UserContextMenuAction.initialize) { + regUserContextMenuActionNS.verbose(`Init -> ${Name} (${UserContextMenuAction.interactionData.name})`); + UserContextMenuAction.initialize(this.client); + } + } + + return; + } + + public registerMessageContextMenuActionInteractions() { + const regMessageContextMenuActionNS = this.loggingNS.getNamespace("Registration/MessageContextMenuAction"); + + this.messageContextMenuActionInteractions.clear(); + regMessageContextMenuActionNS.info("Registering message context menu actions"); + + for (const [Name, { MessageContextMenuAction }] of Object.entries(messageContextMenuActions)) { + if (this.client.config.DontRegisterTestInteractions && Name.toLowerCase().startsWith("test")) continue; + regMessageContextMenuActionNS.verbose( + `Registering -> ${Name} (${MessageContextMenuAction.interactionData.name})`, + ); + if (MessageContextMenuAction.interactionData.type != ApplicationCommandType.Message) + throw new Error( + `invalid context menu action type for ${Name} (expected Message, got ${MessageContextMenuAction.interactionData.type})`, + ); + this.messageContextMenuActionInteractions.set(Name, MessageContextMenuAction); + if (MessageContextMenuAction.initialize) { + regMessageContextMenuActionNS.verbose( + `Init -> ${Name} (${MessageContextMenuAction.interactionData.name})`, + ); + MessageContextMenuAction.initialize(this.client); + } + } + + return; + } + + public registerAllInteractions() { + this.registerChatInputInteractions(); + this.registerUserContextMenuActionInteractions(); + this.registerMessageContextMenuActionInteractions(); + return; + } + + public getInteractionData(type: ApplicationCommandType, name: string) { + const collection = + type == ApplicationCommandType.ChatInput + ? this.chatInputInteractions + : type == ApplicationCommandType.User + ? this.userContextMenuActionInteractions + : type == ApplicationCommandType.Message + ? this.messageContextMenuActionInteractions + : undefined; + if (!collection) throw new Error(`invalid interaction type ${type}`); + + for (const [_, data] of collection) { + if (data.interactionData.name == name) return data; + } + + return undefined; + } + + public generateAppsJsonData() { + const merged: Array< + RESTPostAPIChatInputApplicationCommandsJSONBody | RESTPostAPIContextMenuApplicationCommandsJSONBody + > = []; + + // json hell lmao + this.chatInputInteractions.forEach(({ interactionData }) => merged.push(interactionData.toJSON())); + this.userContextMenuActionInteractions.forEach(({ interactionData }) => merged.push(interactionData.toJSON())); + this.messageContextMenuActionInteractions.forEach(({ interactionData }) => + merged.push(interactionData.toJSON()), + ); + + return merged; + } + + private async dispatchInteractionOccurredLog(name: string, type: string, guildId: string, dispatcher: User) { + const guildDb = await this.client.db.guild.findUnique({ where: { GuildId: guildId } }); + const channel = + guildDb && guildDb.LoggingChannelId != "" + ? await this.client.channels.fetch(guildDb.LoggingChannelId).catch(() => null) + : null; + if (!guildDb) throw new Error(`no guild data for ${guildId}`); + if (!channel || !channel.isTextBased()) return; + + const embed = new MeteoriumEmbedBuilder().setTitle("Interaction dispatched").addFields([ + { name: "Dispatcher", value: `${userMention(dispatcher.id)} (${dispatcher.username} - ${dispatcher.id})` }, + { name: "Interaction name", value: name }, + { name: "Interaction type", value: type }, + ]); + + return await channel.send({ embeds: [embed] }); + } + + public async dispatchInteraction(interaction: Interaction) { + if (!interaction.inCachedGuild()) return; + + // Get logging namespace + const dispatchNS = this.loggingNS.getNamespace("Dispatch"); + + // Interaction type + const interactionType = interaction.isChatInputCommand() + ? ApplicationCommandType.ChatInput + : interaction.isAutocomplete() + ? ApplicationCommandType.ChatInput + : interaction.isUserContextMenuCommand() + ? ApplicationCommandType.User + : interaction.isMessageContextMenuCommand() + ? ApplicationCommandType.Message + : undefined; + const interactionName = + interaction.isCommand() || interaction.isAutocomplete() ? interaction.commandName : undefined; + const isAutocomplete = interaction.isAutocomplete(); + if (!interactionType || !interactionName) return; + + // Get interaction data + const data = this.getInteractionData(interactionType, interactionName); + if (!data) + return dispatchNS.error( + `could not find interaction data for ${interactionName}? ignoring this interaction (${interaction.id})`, + ); + + // Required feature check + if ( + data.requiredFeature && + !(await this.client.guildFeatures.hasFeatureEnabled(interaction.guildId, data.requiredFeature)) + ) { + if (interaction.isRepliable()) + await interaction.reply({ + content: `This command requires the guild feature ${data.requiredFeature} to be enabled.`, + ephemeral: true, + }); + return; + } + + // Logging to guild internal logs + if (!isAutocomplete) + this.dispatchInteractionOccurredLog( + interactionName, + interactionType == ApplicationCommandType.ChatInput + ? "Chat command" + : `${interactionType == ApplicationCommandType.User ? "User" : "Message"} context menu action`, + interaction.guildId, + interaction.user, + ).catch((e) => + dispatchNS.warn( + `could not send interaction dispatch log for ${interaction.id} (guild ${interaction.guildId}):\n${util.inspect(e)}`, + ), + ); + + // Execute callback + try { + if (isAutocomplete) { + const dataTyped = data as MeteoriumChatCommand; + if (!dataTyped.autocomplete) + return dispatchNS.error( + `autocomplete interaction running on a command that doesn't have a autocomplete callback set (${interactionName})`, + ); + await dataTyped.autocomplete(interaction, this.client); + } else { + switch (interactionType) { + case ApplicationCommandType.ChatInput: { + const dataTyped = data as MeteoriumChatCommand; + await dataTyped.callback(interaction as ChatInputCommandInteraction<"cached">, this.client); + break; + } + case ApplicationCommandType.User: { + const dataTyped = data as MeteoriumUserContextMenuAction; + await dataTyped.callback( + interaction as UserContextMenuCommandInteraction<"cached">, + this.client, + ); + break; + } + case ApplicationCommandType.Message: { + const dataTyped = data as MeteoriumMessageContextMenuAction; + await dataTyped.callback( + interaction as MessageContextMenuCommandInteraction<"cached">, + this.client, + ); + break; + } + default: { + throw new Error("invalid interaction type"); + } + } + } + } catch (e) { + const inspected = util.inspect(e); + dispatchNS.error(`error occurred when during interaction dispatch (${interactionName}):\n${inspected}`); + + const errEmbed = new MeteoriumEmbedBuilder(interaction.user) + .setTitle("Error occurred during interaction dispatch") + .setDescription(codeBlock(inspected.substring(0, 4500))) + .setErrorColor(); + + try { + if (interaction.isChatInputCommand() || interaction.isContextMenuCommand()) { + if (interaction.deferred) await interaction.editReply({ embeds: [errEmbed] }); + else await interaction.reply({ embeds: [errEmbed] }); + } + } catch (e) { + dispatchNS.warn( + `could not send interaction error for ${interaction.id} (${interactionName}):\n${util.inspect(e)}`, + ); + } + } + + return; + } +} diff --git a/src/interactions/messageContextMenuActions/index.ts b/src/interactions/messageContextMenuActions/index.ts new file mode 100644 index 0000000..29029a1 --- /dev/null +++ b/src/interactions/messageContextMenuActions/index.ts @@ -0,0 +1 @@ +export * as Test from "./test.js"; diff --git a/src/interactions/messageContextMenuActions/test.ts b/src/interactions/messageContextMenuActions/test.ts new file mode 100644 index 0000000..cf3d4fc --- /dev/null +++ b/src/interactions/messageContextMenuActions/test.ts @@ -0,0 +1,12 @@ +import { ContextMenuCommandBuilder, ApplicationCommandType } from "discord.js"; +import type { MeteoriumMessageContextMenuAction } from "../index.js"; + +export const MessageContextMenuAction: MeteoriumMessageContextMenuAction = { + interactionData: new ContextMenuCommandBuilder() + .setName("test") + .setType(ApplicationCommandType.Message) + .setDMPermission(true), + async callback(interaction, _) { + return interaction.reply(`OK - DM: ${interaction.channel?.isDMBased() ? "Yes" : "No"}`); + }, +}; diff --git a/src/interactions/userContextMenuActions/index.ts b/src/interactions/userContextMenuActions/index.ts new file mode 100644 index 0000000..29029a1 --- /dev/null +++ b/src/interactions/userContextMenuActions/index.ts @@ -0,0 +1 @@ +export * as Test from "./test.js"; diff --git a/src/interactions/userContextMenuActions/test.ts b/src/interactions/userContextMenuActions/test.ts new file mode 100644 index 0000000..0437b91 --- /dev/null +++ b/src/interactions/userContextMenuActions/test.ts @@ -0,0 +1,12 @@ +import { ContextMenuCommandBuilder, ApplicationCommandType } from "discord.js"; +import type { MeteoriumUserContextMenuAction } from "../index.js"; + +export const UserContextMenuAction: MeteoriumUserContextMenuAction = { + interactionData: new ContextMenuCommandBuilder() + .setName("test") + .setType(ApplicationCommandType.User) + .setDMPermission(true), + async callback(interaction, _) { + return interaction.reply(`OK - DM: ${interaction.channel?.isDMBased() ? "Yes" : "No"}`); + }, +}; diff --git a/src/util/MeteoriumClient.ts b/src/util/MeteoriumClient.ts deleted file mode 100644 index a5a8d9e..0000000 --- a/src/util/MeteoriumClient.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ApplicationCommandType, Client, Collection } from "discord.js"; -import { config } from "dotenv"; -import { Player } from "discord-player"; -import { HolodexApiClient } from "holodex.js"; -import { lyricsExtractor } from "@discord-player/extractor"; -import { PrismaClient } from "@prisma/client"; - -import * as Commands from "../commands"; -import * as Events from "../events"; -import * as ContextMenuActions from "../contextmenu"; -import { MeteoriumLogging } from "./MeteoriumLogging"; - -const ParseDotEnvConfig = () => { - if (!process.env.METEORIUMBOTTOKEN) { - config({ path: "./.ENV" }); - } - const InteractionFirstDeployGuildIds = String(process.env.DEPLOYGUILDIDS).split(","); - const RuntimeLogChannelIds = String(process.env.RUNTIMELOGCHANNELID).split(","); - return { - MongoDB_URI: String(process.env.METEORIUMMONGODBURI), - DiscordToken: String(process.env.METEORIUMBOTTOKEN), - DiscordApplicationId: String(process.env.METEORIUMAPPLICATIONID), - InteractionFirstDeployGuildIds: InteractionFirstDeployGuildIds, - HolodexAPIKey: String(process.env.METEORIUMHOLODEXTOKEN), - RatelimitMaxLimit: Number(process.env.RATELIMITMAXLIMIT), - RatelimitMaxLimitTime: Number(process.env.RATELIMITMAXLIMITTIME), - GeniusAPIKey: String(process.env.GENIUSAPIKEY), - RuntimeLogChannelIds: RuntimeLogChannelIds, - }; -}; - -export class MeteoriumClient extends Client { - public Config = ParseDotEnvConfig(); - public Commands = new Collection(); - public UserContextMenuActions = new Collection(); - public MessageContextMenuActions = new Collection(); - public Database = new PrismaClient(); - public DiscordPlayer = new Player(this); - public LyricsExtractor = lyricsExtractor(this.Config.GeniusAPIKey); - public HolodexClient = new HolodexApiClient({ - apiKey: this.Config.HolodexAPIKey, - }); - public Logging = new MeteoriumLogging("Meteorium"); - public override async login() { - const loginNS = this.Logging.RegisterNamespace("init", true); - - loginNS.info("Loading discord-player default extractors"); - this.DiscordPlayer.extractors.loadDefault(); - - loginNS.info("Registering events"); - for (const [Name, { Event }] of Object.entries(Events)) { - loginNS.debug(`Registering event -> ${Name} ${Event}`); - if (Event.Once) { - // @ts-ignore - this.once(Name, (...args) => Event.Callback(this, ...args)); - } else { - // @ts-ignore - this.on(Name, (...args) => Event.Callback(this, ...args)); - } - } - - loginNS.info("Registering commands"); - this.Commands.clear(); - for (const [Name, { Command }] of Object.entries(Commands)) { - loginNS.debug(`Registering command -> ${Name} ${Command}`); - if (Command.Init) { - loginNS.debug(`Running command init -> ${Name} ${Command}`); - await Command.Init(this); - } - this.Commands.set(Name, Command); - } - - loginNS.info("Registering context menu actions"); - this.UserContextMenuActions.clear(); - this.MessageContextMenuActions.clear(); - for (const [Name, { ContextMenuAction }] of Object.entries(ContextMenuActions)) { - loginNS.debug( - `Registering context menu action -> ${Name} (${ContextMenuAction.Name}) ${ContextMenuAction}`, - ); - if (ContextMenuAction.Init) { - loginNS.debug(`Running command init -> ${Name} (${ContextMenuAction.Name}) ${ContextMenuAction}`); - await ContextMenuAction.Init(this); - } - if (ContextMenuAction.Type == ApplicationCommandType.User) { - this.UserContextMenuActions.set(ContextMenuAction.Name, ContextMenuAction); - } else { - this.MessageContextMenuActions.set(ContextMenuAction.Name, ContextMenuAction); - } - } - - // Shard logging - const shardNS = this.Logging.RegisterNamespace("Sharding", true); - super.on("shardDisconnect", (event, id) => shardNS.warn(`Disconnected from shard ${id} (code ${event.code}).`)); - super.on("shardError", (err, id) => shardNS.error(`Shard ${id} websocket error occured:\n${err}`)); - super.on("shardResume", (id, re) => shardNS.info(`Shard ${id} reconnected successfully. (ReplayEvents ${re})`)); - super.on("shardReconnecting", (id) => shardNS.info(`Attempting to reconnect to shard ${id}.`)); - - loginNS.info("Logging into Discord"); - return super.login(this.Config.DiscordToken); - } -} diff --git a/src/util/MeteoriumEmbedBuilder.ts b/src/util/MeteoriumEmbedBuilder.ts deleted file mode 100644 index 614498e..0000000 --- a/src/util/MeteoriumEmbedBuilder.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { APIEmbed, EmbedBuilder, EmbedData, User } from "discord.js"; - -export class MeteoriumEmbedBuilder extends EmbedBuilder { - constructor(data?: EmbedData | APIEmbed | undefined, user?: User, dontsetinitialdata?: boolean) { - super(data); - if (!dontsetinitialdata) { - this.setColor([0, 153, 255]) - .setFooter({ - text: "Meteorium | Developed by RadiatedExodus (RealEthanPlayzDev)", - }) - .setTimestamp(); - if (user) { - this.setFooter({ - text: - "Requested by " + - user.tag + - " (" + - user.id + - ") | Meteorium | Developed by RadiatedExodus (RealEthanPlayzDev)", - iconURL: user.avatarURL() || user.defaultAvatarURL, - }); - } - } - } - public setNormalColor() { - this.setColor([0, 153, 255]); - return this; - } - public setErrorColor() { - this.setColor([255, 0, 0]); - return this; - } -} diff --git a/src/util/MeteoriumLogging.ts b/src/util/MeteoriumLogging.ts deleted file mode 100644 index 22ed898..0000000 --- a/src/util/MeteoriumLogging.ts +++ /dev/null @@ -1,63 +0,0 @@ -import chalk from "chalk"; -import moment from "moment"; - -const RED = chalk.red; -const YELLOW = chalk.yellow; -const CYAN = chalk.cyan; -const MAGENTA = chalk.magenta; -const GRAY = chalk.gray; - -function getCurrentDTStr() { - return moment().format("DD-MM-YYYY hh:mm:ss:SSS A Z"); -} - -export class MeteoriumLogging { - public namespaces: Array; - public name: string; - constructor(name: string) { - this.name = name; - this.namespaces = new Array(); - } - RegisterNamespace(name: string, verbose: boolean) { - const Namespace = new MeteoriumLoggingNamespace(name, this, verbose); - this.namespaces.push(Namespace); - return Namespace; - } - GetNamespace(name: string) { - for (const namespace of this.namespaces) { - if (namespace.name == name) return namespace; - } - return this.RegisterNamespace(name, true); - } -} - -export class MeteoriumLoggingNamespace { - public name: string; - public loggy: MeteoriumLogging; - public verboseMode: boolean; - constructor(name: string, root: MeteoriumLogging, verbose: boolean) { - this.name = name; - this.loggy = root; - this.verboseMode = verbose; - } - write(color: chalk.Chalk, msg: string, ...data: string[]) { - return console.log(color(msg), ...data); - } - verbose(msg: string, ...data: string[]) { - if (!this.verboseMode) return; - return this.write(GRAY, `[${getCurrentDTStr()}] [VRB] [${this.loggy.name}/${this.name}] ${msg}`, ...data); - } - debug(msg: string, ...data: string[]) { - if (!this.verboseMode) return; - return this.write(MAGENTA, `[${getCurrentDTStr()}] [DBG] [${this.loggy.name}/${this.name}] ${msg}`, ...data); - } - info(msg: string, ...data: string[]) { - return this.write(CYAN, `[${getCurrentDTStr()}] [INF] [${this.loggy.name}/${this.name}] ${msg}`, ...data); - } - warn(msg: string, ...data: string[]) { - return this.write(YELLOW, `[${getCurrentDTStr()}] [WRN] [${this.loggy.name}/${this.name}] ${msg}`, ...data); - } - error(msg: string, ...data: string[]) { - return this.write(RED, `[${getCurrentDTStr()}] [ERR] [${this.loggy.name}/${this.name}] ${msg}`, ...data); - } -} diff --git a/src/util/MojangAPI/Proto.ts b/src/util/MojangAPI/Proto.ts deleted file mode 100644 index a9390a7..0000000 --- a/src/util/MojangAPI/Proto.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface Textures { - timestamp: string; - profileId: string; - profileName: string; - signatureRequired?: true; - textures: { - SKIN?: { - url: string; - metadata?: { model: string }; - }; - CAPE?: { url: string }; - }; -} - -export interface ProfileResponse { - id: string; - name: string; - properties: [ - { - name: "textures"; - value: string; - signature?: string; - }, - ]; - profileActions: string[]; - legacy?: true; -} - -export interface UUIDFromNameResponse { - name: string; - id: string; - legacy?: true; - demo?: true; -} diff --git a/src/util/MojangAPI/index.ts b/src/util/MojangAPI/index.ts deleted file mode 100644 index 00df30d..0000000 --- a/src/util/MojangAPI/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import axios from "axios"; - -import * as proto from "./Proto"; - -export async function getUUIDFromName(name: string) { - const res = await axios.get(`https://api.mojang.com/users/profiles/minecraft/${name}`); - return { code: res.status, data: res.data as proto.UUIDFromNameResponse }; -} - -export async function getProfile(uuid: string) { - const res = await axios.get(`https://sessionserver.mojang.com/session/minecraft/profile/${encodeURIComponent(uuid)}`); - return { code: res.status, data: res.data as proto.ProfileResponse }; -} - -export function decodeTexturesB64(texturesb64: string) { - const bf = Buffer.from(texturesb64, "base64"); - return JSON.parse(bf.toString()) as proto.Textures; -} - -export function decodeDefaultSkin(uuid: string): "steve" | "alex" { - const chars = uuid.split(""); - // @ts-ignore - const lsbs_even = parseInt(chars[ 7], 16) ^ parseInt(chars[15], 16) ^ parseInt(chars[23], 16) ^ parseInt(chars[31], 16); - return lsbs_even ? "alex" : "steve"; -} \ No newline at end of file diff --git a/src/util/Utilities.ts b/src/util/Utilities.ts deleted file mode 100644 index f43fb38..0000000 --- a/src/util/Utilities.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { time } from "discord.js"; - -export function GenerateFormattedTime(inputTime: Date | number = new Date()) { - const date = typeof inputTime === "number" ? new Date(Math.floor(inputTime)) : inputTime; - return `${time(date)} (${time(date, "R")})`; -} diff --git a/tsconfig.json b/tsconfig.json index 72e4124..ccce624 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,106 +1,110 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": [ - "ES2022" - ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - "useDefineForClassFields": true /* Emit ECMAScript-standard-compliant class fields. */, - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "Node16" /* Specify what module code is generated. */, - "rootDir": "src" /* Specify the root folder within your source files. */, - "moduleResolution": "Node16" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "resolveJsonModule": true /* Enable importing .json files. */, - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Modules */ + "module": "NodeNext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true /* Create source map files for emitted JavaScript files. */, - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "dist" /* Specify an output folder for all emitted files. */, - "removeComments": true /* Disable emitting comments. */, - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - "verbatimModuleSyntax": false /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */, - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "inlineSources": true /* Include source code in the sourcemaps inside the emitted JavaScript. */, - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - "newLine": "lf" /* Set the newline character for emitting files. */, - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Interop Constraints */ + "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */, - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */, - "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */, - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, - "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, - "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - "allowUnusedLabels": false /* Disable error reporting for unused labels. */, - "allowUnreachableCode": false /* Disable error reporting for unreachable code. */, + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["./src/**/*.ts", "./src/strings/*.json"] + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"] } diff --git a/yarn.lock b/yarn.lock index 48f2948..492d5e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,42 +9,6 @@ dependencies: regenerator-runtime "^0.14.0" -"@discord-player/equalizer@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@discord-player/equalizer/-/equalizer-0.2.3.tgz#a5a00150479db71d28d85ed162cf4954341e325a" - integrity sha512-71UAepYMbHTg2QQLXQAgyuXYHrgAYpJDxjg9dRWfTUNf+zfOAlyJEiRRk/WFhQyGu6m23iLR/H/JxgF4AW8Csg== - -"@discord-player/extractor@^4.4.6": - version "4.4.6" - resolved "https://registry.yarnpkg.com/@discord-player/extractor/-/extractor-4.4.6.tgz#87eb9dc198f44a77cceb1b7f8dbbe0b6605c6772" - integrity sha512-jo511D+YOnBptfC+mp775kVXKuCfoWulqOhk82PBmRmwNLPfzBEyPQM+vOlzrNsmoRD1p3mBBXEOnFnuyE9o3w== - dependencies: - file-type "^16.5.4" - genius-lyrics "^4.4.6" - isomorphic-unfetch "^4.0.2" - node-html-parser "^6.1.4" - reverbnation-scraper "^2.0.0" - soundcloud.ts "^0.5.2" - spotify-url-info "^3.2.6" - youtube-sr "^4.3.9" - -"@discord-player/ffmpeg@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@discord-player/ffmpeg/-/ffmpeg-0.1.0.tgz#3c8f1fc7dfdb033dc8ecd18a9e82fde2a3867196" - integrity sha512-0kW6q4gMQN2B4Z4EzmUgXrKQSXXmyhjdZBBZ/6jSHZ9fh814oOu+JXP01VvtWHwTylI7qJHIctEWtSyjEubCJg== - -"@discord-player/opus@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@discord-player/opus/-/opus-0.1.2.tgz#f097cdeb0fb0478f56ff586e26e57cdfea5dc63d" - integrity sha512-yF0m+pW7H9RCbRcgk/i6vv47tlWxaCHjp6/F0W4GXZMGZ0pcvZaxk8ic7aFPc3+IoDvrAHvWNomLq+JeFzdncA== - -"@discord-player/utils@^0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@discord-player/utils/-/utils-0.2.2.tgz#ece599124472ea8a5fc28e81adaee38cf4ad5a0c" - integrity sha512-UklWUT7BcZEkBgywM9Cmpo2nwj3SQ9Wmhu6ml1uy/YRQnY8IRdZEHD84T2kfjOg4LVZek0ej1VerIqq7a9PAHQ== - dependencies: - "@discordjs/collection" "^1.1.0" - "@discordjs/builders@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.7.0.tgz#e2478c7e55b0f4c40837edb8f102bce977323a37" @@ -58,7 +22,7 @@ ts-mixer "^6.0.3" tslib "^2.6.2" -"@discordjs/collection@1.5.3", "@discordjs/collection@^1.1.0": +"@discordjs/collection@1.5.3": version "1.5.3" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.3.tgz#5a1250159ebfff9efa4f963cfa7e97f1b291be18" integrity sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ== @@ -150,45 +114,45 @@ integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== "@prisma/client@^5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.9.1.tgz#d92bd2f7f006e0316cb4fda9d73f235965cf2c64" - integrity sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ== - -"@prisma/debug@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.9.1.tgz#906274e73d3267f88b69459199fa3c51cd9511a3" - integrity sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA== - -"@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64": - version "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz#54d2164f28d23e09d41cf9eb0bddbbe7f3aaa660" - integrity sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ== - -"@prisma/engines@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.9.1.tgz#767539afc6f193a182d0495b30b027f61f279073" - integrity sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ== - dependencies: - "@prisma/debug" "5.9.1" - "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - "@prisma/fetch-engine" "5.9.1" - "@prisma/get-platform" "5.9.1" - -"@prisma/fetch-engine@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz#5d3b2c9af54a242e37b3f9561b59ab72f8e92818" - integrity sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA== - dependencies: - "@prisma/debug" "5.9.1" - "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - "@prisma/get-platform" "5.9.1" - -"@prisma/get-platform@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.9.1.tgz#a66bb46ab4d30db786c84150ef074ab0aad4549e" - integrity sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg== - dependencies: - "@prisma/debug" "5.9.1" + version "5.13.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.13.0.tgz#b9f1d0983d714e982675201d8222a9ecb4bdad4a" + integrity sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg== + +"@prisma/debug@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.13.0.tgz#d88b0f6fafa0c216e20e284ed9fc30f1cbe45786" + integrity sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ== + +"@prisma/engines-version@5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b": + version "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz#a72a4fb83ba1fd01ad45f795aa55168f60d34723" + integrity sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A== + +"@prisma/engines@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.13.0.tgz#8994ebf7b4e35aee7746a8465ec22738379bcab6" + integrity sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw== + dependencies: + "@prisma/debug" "5.13.0" + "@prisma/engines-version" "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b" + "@prisma/fetch-engine" "5.13.0" + "@prisma/get-platform" "5.13.0" + +"@prisma/fetch-engine@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz#9b6945c7b38bb59e840f8905b20ea7a3d059ca55" + integrity sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA== + dependencies: + "@prisma/debug" "5.13.0" + "@prisma/engines-version" "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b" + "@prisma/get-platform" "5.13.0" + +"@prisma/get-platform@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.13.0.tgz#99ef909a52b9d79b64d72d2d3d8210c4892b6572" + integrity sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw== + dependencies: + "@prisma/debug" "5.13.0" "@sapphire/async-queue@^1.5.0": version "1.5.2" @@ -213,16 +177,6 @@ resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.3.tgz#0c102aa2ec5b34f806e9bc8625fc6a5e1d0a0c6a" integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== -"@tokenizer/token@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" - integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== - -"@types/ms@^0.7.32": - version "0.7.34" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" - integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== - "@types/node@*": version "20.11.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.17.tgz#cdd642d0e62ef3a861f88ddbc2b61e32578a9292" @@ -230,6 +184,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.11.17": + version "20.12.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== + dependencies: + undici-types "~5.26.4" + "@types/ws@8.5.9": version "8.5.9" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.9.tgz#384c489f99c83225a53f01ebc3eddf3b8e202a8c" @@ -237,7 +198,7 @@ dependencies: "@types/node" "*" -"@types/ws@^8.5.5", "@types/ws@^8.5.9": +"@types/ws@^8.5.9": version "8.5.10" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== @@ -367,15 +328,6 @@ axios@^0.21.4: dependencies: follow-redirects "^1.14.0" -axios@^1.6.3: - version "1.6.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" - integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== - dependencies: - follow-redirects "^1.15.4" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -406,7 +358,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -bufferutil@^4.0.1: +bufferutil@^4.0.1, bufferutil@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== @@ -418,14 +370,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" - integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -434,6 +378,11 @@ chalk@^4.0.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + cheerio-select@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" @@ -490,7 +439,7 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -559,11 +508,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - debounce-fn@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7" @@ -605,36 +549,6 @@ discord-api-types@0.37.61: resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.61.tgz#9dd8e58c624237e6f1b23be2d29579af268b8c5b" integrity sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw== -discord-api-types@^0.37.50: - version "0.37.69" - resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.69.tgz#4c78a03ef247dd8d6b95e4eaf199ed8d000a3955" - integrity sha512-c0rHc5YGNIXQkI+V7QwP8y77wxo74ITNeZmMwxtKC/l01aIF/gKBG/U2MKhUt2iaeRH9XwAt9PT3AI9JQVvKVA== - -discord-player@^6.6.7: - version "6.6.7" - resolved "https://registry.yarnpkg.com/discord-player/-/discord-player-6.6.7.tgz#01e645f34712fa0fb459a1e1e0dd3f6b69b72da2" - integrity sha512-8y5TBJTF3vut4WJZFfrc8m7yXIkOr0WNbdKiU5I3MADWZQUlRaMBTGS4NO03Vq13nZZfK0c4XVUZuqtydIKp4w== - dependencies: - "@discord-player/equalizer" "^0.2.3" - "@discord-player/ffmpeg" "^0.1.0" - "@discord-player/utils" "^0.2.2" - discord-voip "^0.1.3" - ip "^1.1.8" - libsodium-wrappers "^0.7.10" - -discord-voip@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/discord-voip/-/discord-voip-0.1.3.tgz#6823ad81b924314f6a9aa536cd5bb43fb1631a80" - integrity sha512-9DWY5/BLPXeldVwPr8/ggGjggTYOTw77aGQc3+4n5K54bRbbiJ9DUJc+mJzDiSLoHN3f286eRGACJYtrUu27xA== - dependencies: - "@discord-player/ffmpeg" "^0.1.0" - "@discord-player/opus" "^0.1.2" - "@types/ws" "^8.5.5" - discord-api-types "^0.37.50" - prism-media "^1.3.5" - tslib "^2.6.1" - ws "^8.13.0" - discord.js@^14.14.1: version "14.14.1" resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.14.1.tgz#9a2bea23bba13819705ab87612837610abce9ee3" @@ -692,10 +606,10 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" -dotenv@^16.0.3: - version "16.4.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11" - integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ== +dotenv@^16.4.2: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== ecc-jsbn@~0.1.1: version "0.1.2" @@ -783,28 +697,11 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - figlet@^1.4.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.7.0.tgz#46903a04603fd19c3e380358418bb2703587a72e" integrity sha512-gO8l3wvqo0V7wEFLXPbkX83b7MVjRrk1oRLfYlZXol8nEpb/ON9pcKLI4qpBv5YtOTfrINtqb7b40iYY2FTWFg== -file-type@^16.5.4: - version "16.5.4" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd" - integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw== - dependencies: - readable-web-to-node-stream "^3.0.0" - strtok3 "^6.2.4" - token-types "^4.1.1" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -812,7 +709,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -follow-redirects@^1.14.0, follow-redirects@^1.15.4: +follow-redirects@^1.14.0: version "1.15.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== @@ -822,15 +719,6 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -840,13 +728,6 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -874,14 +755,6 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" -genius-lyrics@^4.4.6: - version "4.4.6" - resolved "https://registry.yarnpkg.com/genius-lyrics/-/genius-lyrics-4.4.6.tgz#b3d755152dc64008f4cbfb039e1ada2c740adfc4" - integrity sha512-TeSF4qXwLm+Nl8wUX+WUTJlEhPBanMw9EWpIHE2a/Qs4y2NBK99AHYfZJc73H1HVkZj4zPfscuGWlkQbbh0pDA== - dependencies: - node-html-parser "^6.1.9" - undici "^5.24.0" - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -929,16 +802,6 @@ has-unicode@^2.0.1: resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -himalaya@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/himalaya/-/himalaya-1.1.0.tgz#31724ae9d35714cd7c6f4be94888953f3604606a" - integrity sha512-LLase1dHCRMel68/HZTFft0N0wti0epHr3nNY7ynpLbyZpmrKMQ8YIpiOV77TM97cNpC8Wb2n6f66IRggwdWPw== - holodex.js@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/holodex.js/-/holodex.js-2.0.5.tgz#5be9ecb618ac1f6aeb9306b0561ec3a5c27253d0" @@ -978,11 +841,6 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -996,11 +854,6 @@ inherits@2, inherits@^2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ip@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" - integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -1021,14 +874,6 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== -isomorphic-unfetch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-4.0.2.tgz#5fc04eeb1053b7b702278e2cf7a3f246cb3a9214" - integrity sha512-1Yd+CF/7al18/N2BDbsLBcp6RO3tucSW+jcLq24dqdX5MNbCNTw1z4BsGsp4zNmjr/Izm2cs/cEqZPp4kvWSCA== - dependencies: - node-fetch "^3.2.0" - unfetch "^5.0.0" - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -1074,18 +919,6 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" -libsodium-wrappers@^0.7.10: - version "0.7.13" - resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz#83299e06ee1466057ba0e64e532777d2929b90d3" - integrity sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw== - dependencies: - libsodium "^0.7.13" - -libsodium@^0.7.13: - version "0.7.13" - resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.13.tgz#230712ec0b7447c57b39489c48a4af01985fb393" - integrity sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw== - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -1111,14 +944,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -m3u8stream@^0.8.4, m3u8stream@^0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/m3u8stream/-/m3u8stream-0.8.6.tgz#0d6de4ce8ee69731734e6b616e7b05dd9d9a55b1" - integrity sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA== - dependencies: - miniget "^4.2.2" - sax "^1.2.4" - magic-bytes.js@^1.5.0: version "1.8.0" resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz#8362793c60cd77c2dd77db6420be727192df68e2" @@ -1153,11 +978,6 @@ mimic-fn@^3.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== -miniget@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/miniget/-/miniget-4.2.3.tgz#3707a24c7c11c25d359473291638ab28aab349bd" - integrity sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA== - minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1190,7 +1010,7 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.29.4: +moment@^2.30.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== @@ -1205,10 +1025,10 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +nan@^2.18.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" + integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== next-tick@^1.1.0: version "1.1.0" @@ -1233,40 +1053,18 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" -node-fetch@^3.2.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - node-gyp-build@^4.3.0: version "4.8.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== -node-html-parser@^6.1.4, node-html-parser@^6.1.9: - version "6.1.12" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.12.tgz#6138f805d0ad7a6b5ef415bcd91bca07374bf575" - integrity sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA== - dependencies: - css-select "^5.1.0" - he "1.2.0" - nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -1359,11 +1157,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -peek-readable@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" - integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -1376,39 +1169,22 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -play-audio@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/play-audio/-/play-audio-0.5.2.tgz#191dd45b95ff64ae20bbd718671da4e987d4edd1" - integrity sha512-ZAqHUKkQLix2Iga7pPbsf1LpUoBjcpwU93F1l3qBIfxYddQLhxS6GKmS0d3jV8kSVaUbr6NnOEcEMFvuX93SWQ== - -play-dl@^1.9.7: - version "1.9.7" - resolved "https://registry.yarnpkg.com/play-dl/-/play-dl-1.9.7.tgz#885beb66ad3b450632733240faeb65040a43b30f" - integrity sha512-KpgerWxUCY4s9Mhze2qdqPhiqd8Ve6HufpH9mBH3FN+vux55qSh6WJKDabfie8IBHN7lnrAlYcT/UdGax58c2A== - dependencies: - play-audio "^0.5.2" - -prettier@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" - integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== prism-media@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.5.tgz#ea1533229f304a1b774b158de40e98c765db0aa6" integrity sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA== -prisma@^5.2.0: - version "5.9.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.9.1.tgz#baa3dd635fbf71504980978f10f55ea11068f6aa" - integrity sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ== +prisma@^5.9.1: + version "5.13.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.13.0.tgz#1f06e20ccfb6038ad68869e6eacd3b346f9d0851" + integrity sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg== dependencies: - "@prisma/engines" "5.9.1" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + "@prisma/engines" "5.13.0" psl@^1.1.28: version "1.9.0" @@ -1434,13 +1210,6 @@ readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-web-to-node-stream@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" - integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== - dependencies: - readable-stream "^3.6.0" - regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -1499,13 +1268,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -reverbnation-scraper@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/reverbnation-scraper/-/reverbnation-scraper-2.0.0.tgz#c17539aba218cc29033a63e732ba3e4953cb5bd5" - integrity sha512-t1Mew5QC9QEVEry5DXyagvci2O+TgXTGoMHbNoW5NRz6LTOzK/DLHUpnrQwloX8CVX5z1a802vwHM3YgUVOvKg== - dependencies: - node-fetch "^2.6.0" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1523,11 +1285,6 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@^1.1.3, sax@^1.2.4: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" - integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== - semver@^6.0.0: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -1557,35 +1314,6 @@ signalr-client@0.0.20: dependencies: websocket "^1.0.28" -soundcloud-scraper@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/soundcloud-scraper/-/soundcloud-scraper-5.0.3.tgz#90c45f13353a2c88749d3dcabef9100fda2f56ba" - integrity sha512-AmS9KmK7mMaPVzHzBk40rANpAttZila3+iAet6EA47EeiTBUzVwjq4B+1LCOLtgPmzDSGk0qn+LZOEd5UhnZTQ== - dependencies: - cheerio "^1.0.0-rc.10" - m3u8stream "^0.8.4" - node-fetch "^2.6.1" - -soundcloud.ts@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/soundcloud.ts/-/soundcloud.ts-0.5.2.tgz#60660b889afb31c0e504f089002228fbf576b298" - integrity sha512-/pc72HWYJpSpup+mJBE9pT31JsrMcxJGBlip3Vem+0Fsscg98xh1/7I2nCpAKuMAeV6MVyrisI8TfjO6T7qKJg== - dependencies: - undici "^5.22.1" - -spotify-uri@~4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/spotify-uri/-/spotify-uri-4.0.1.tgz#15a853883aca59a05338db03ea55d6ad37067ded" - integrity sha512-dEt8UN5fSsZpcPk8HOEHkv29U71kefKjcYt2dopsShrkIZhXtDXe9Xse4xq0GW6831LnEZFry5mpzm1HV/TNLw== - -spotify-url-info@^3.2.13, spotify-url-info@^3.2.6: - version "3.2.13" - resolved "https://registry.yarnpkg.com/spotify-url-info/-/spotify-url-info-3.2.13.tgz#c0f96fb733bfb9e73becd61f70ed6b8e33505d10" - integrity sha512-b1D4n4vnSHf8/HkLT7SIwBsj21t5AV8uhWvzU6c1v8JHS34Ocdb1SsPlannRChCuRAWMKbOEntSn/sP3RhsDfQ== - dependencies: - himalaya "~1.1.0" - spotify-uri "~4.0.0" - sshpk@^1.7.0: version "1.18.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" @@ -1629,14 +1357,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strtok3@^6.2.4: - version "6.3.0" - resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0" - integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw== - dependencies: - "@tokenizer/token" "^0.3.0" - peek-readable "^4.1.0" - supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1672,14 +1392,6 @@ terminal-link@^3.0.0: ansi-escapes "^5.0.0" supports-hyperlinks "^2.2.0" -token-types@^4.1.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753" - integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ== - dependencies: - "@tokenizer/token" "^0.3.0" - ieee754 "^1.2.1" - tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -1698,7 +1410,7 @@ ts-mixer@^6.0.3: resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.3.tgz#69bd50f406ff39daa369885b16c77a6194c7cae6" integrity sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ== -tslib@2.6.2, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -1737,10 +1449,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^5.1.6: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typescript@^5.3.3: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== undici-types@~5.26.4: version "5.26.5" @@ -1754,18 +1466,6 @@ undici@5.27.2: dependencies: "@fastify/busboy" "^2.0.0" -undici@^5.22.1, undici@^5.24.0: - version "5.28.3" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b" - integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA== - dependencies: - "@fastify/busboy" "^2.0.0" - -unfetch@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-5.0.0.tgz#8a5b6e5779ebe4dde0049f7d7a81d4a1af99d142" - integrity sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg== - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -1780,6 +1480,13 @@ utf-8-validate@^5.0.2: dependencies: node-gyp-build "^4.3.0" +utf-8-validate@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" + integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA== + dependencies: + node-gyp-build "^4.3.0" + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -1799,11 +1506,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -web-streams-polyfill@^3.0.3: - version "3.3.2" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz#32e26522e05128203a7de59519be3c648004343b" - integrity sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -1855,7 +1557,7 @@ ws@8.14.2: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== -ws@^8.13.0, ws@^8.14.2: +ws@^8.14.2: version "8.16.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== @@ -1893,16 +1595,9 @@ yargs@^17.2.0: y18n "^5.0.5" yargs-parser "^21.1.1" -youtube-sr@^4.3.10, youtube-sr@^4.3.9: - version "4.3.10" - resolved "https://registry.yarnpkg.com/youtube-sr/-/youtube-sr-4.3.10.tgz#6e0fdc5c1c1c23b6476cb480825b3df8eff0f895" - integrity sha512-YTpIWy2c1XLN4VpsUuZTDNXpJ2sLZQyG0kX1vq3nymHhDKro2SoeODey3pZazW+6AjfmNxoSnI8pCSzPrEa3jw== - -ytdl-core@^4.11.5: - version "4.11.5" - resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.11.5.tgz#8cc3dc9e4884e24e8251250cfb56313a300811f0" - integrity sha512-27LwsW4n4nyNviRCO1hmr8Wr5J1wLLMawHCQvH8Fk0hiRqrxuIu028WzbJetiYH28K8XDbeinYW4/wcHQD1EXA== +zlib-sync@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/zlib-sync/-/zlib-sync-0.1.9.tgz#7075cc257e4551f5d9fc74e24cf2c11e39c7a0d1" + integrity sha512-DinB43xCjVwIBDpaIvQqHbmDsnYnSt6HJ/yiB2MZQGTqgPcwBSZqLkimXwK8BvdjQ/MaZysb5uEenImncqvCqQ== dependencies: - m3u8stream "^0.8.6" - miniget "^4.2.2" - sax "^1.1.3" + nan "^2.18.0"