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"