├── .eslintrc.json
├── .github
└── ISSUE_TEMPLATE
│ └── feature_request.md
├── .gitignore
├── .prettierrc.json
├── .travis.yml
├── LICENSE
├── README.md
├── package.json
└── src
├── commands
├── Information
│ ├── adddevice.ts
│ ├── info.ts
│ ├── listdevices.ts
│ ├── removedevice.ts
│ ├── serverinfo.ts
│ ├── stats.ts
│ ├── userinfo.ts
│ ├── warnpoints.ts
│ ├── xp.ts
│ └── xptop.ts
├── Moderation
│ ├── ban.ts
│ ├── cases.ts
│ ├── clem.ts
│ ├── filter.ts
│ ├── filterlist.ts
│ ├── kick.ts
│ ├── liftwarn.ts
│ ├── lock.ts
│ ├── mute.ts
│ ├── offlineping.ts
│ ├── purge.ts
│ ├── unban.ts
│ ├── unmute.ts
│ └── warn.ts
└── Voice
│ ├── loop.ts
│ ├── nowplaying.ts
│ ├── pause.ts
│ ├── play.ts
│ ├── queue.ts
│ ├── skip.ts
│ ├── stop.ts
│ └── volume.ts
├── config.ts
├── events
├── guildMemberAdd.ts
├── guildMemberRemove.ts
├── guildMemberUpdate.ts
├── messageDelete.ts
├── messageUpdate.ts
└── voiceStateUpdate.ts
├── finalizers
└── delete.ts
├── index.ts
├── lib
└── client.ts
├── monitors
├── experience.ts
├── filter.ts
├── invites.ts
├── spoilers.ts
└── tweaks.ts
├── providers
└── postgresql.js
├── serializers
├── case.ts
└── filteredword.ts
├── tasks
└── unmute.ts
├── tsconfig.json
└── util
├── case.ts
├── dispatcher.ts
├── filteredWord.ts
└── queue.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": ["plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint", "plugin:prettier/recommended"],
4 | "parserOptions": {
5 | "ecmaVersion": 2020,
6 | "sourceType": "module"
7 | },
8 | "rules": {
9 | "@typescript-eslint/explicit-function-return-type": "off"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | src/config.js
3 | yarn.lock
4 | .env
5 | bwd
6 | dist
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "tabWidth": 4
7 | }
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "10"
5 | - "13"
6 | cache:
7 | directories:
8 | - node_modules
9 | jobs:
10 | include:
11 | - stage: "ESLint"
12 | script: yarn eslint
13 | script:
14 | - yarn build
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rjbBot
2 | Bot for the community discord of r/jailbreak
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "janet",
3 | "version": "1.0.0",
4 | "description": "A bot for the r/jb discord",
5 | "main": "dist/index.js",
6 | "repository": "https://github.com/Emy/janet",
7 | "author": "Emy",
8 | "license": "AGPL-3.0-or-later",
9 | "scripts": {
10 | "build": "tsc -b src",
11 | "start": "node dist/index.js",
12 | "lint": "eslint \"src/**\" --fix",
13 | "dev": "yarn lint && yarn build && yarn start"
14 | },
15 | "dependencies": {
16 | "discord.js": "discordjs/discord.js",
17 | "dotenv": "^8.2.0",
18 | "fold-to-ascii": "^5.0.0",
19 | "ipswme": "^2.2.1",
20 | "klasa": "^0.5.0",
21 | "moment": "^2.24.0",
22 | "node-fetch": "^2.6.0",
23 | "pg": "^8.0.0",
24 | "shoukaku": "Deivu/Shoukaku"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^14.0.4",
28 | "@types/node-fetch": "^2.5.5",
29 | "@typescript-eslint/eslint-plugin": "^2.25.0",
30 | "@typescript-eslint/parser": "^2.25.0",
31 | "eslint": "^6.8.0",
32 | "eslint-config-prettier": "^6.10.1",
33 | "eslint-plugin-prettier": "^3.1.2",
34 | "klasa-decorators": "^0.0.1",
35 | "prettier": "^2.0.2",
36 | "typescript": "^4.0.3"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/commands/Information/adddevice.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaClient, KlasaMessage, TextPrompt, Usage } from 'klasa';
2 | import { IDevice, getDevices, getDevice } from 'ipswme';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | promptLimit: 2,
8 | promptTime: 30 * 1000,
9 | runIn: ['text'],
10 | requiredPermissions: ['MANAGE_NICKNAMES'],
11 | usage: '(device:device)',
12 | });
13 |
14 | this.createCustomResolver('device', async (arg: string) => {
15 | if (!arg) throw `Missing device`;
16 | const devices = await getDevices();
17 |
18 | const exists = devices.find(
19 | (x) =>
20 | x.name
21 | .replace(/\(.*\)$/, '')
22 | .trim()
23 | .toLowerCase() === arg.toLowerCase() ||
24 | x.name
25 | .replace(/\(.*\)$/, '')
26 | .replace(' Plus', '+')
27 | .trim()
28 | .toLowerCase() === arg.toLowerCase(),
29 | );
30 | if (exists) return exists;
31 | throw `Device doesn't exist`;
32 | });
33 | }
34 |
35 | async run(msg: KlasaMessage, [device]: [IDevice]) {
36 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
37 | if (!msg.guild.settings.get('channels.botspam')) return;
38 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
39 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
40 | }
41 | }
42 |
43 | if (msg.member!.nickname && /^.+ \[.+\,.+\]$/.test(msg.member!.nickname!)) {
44 | return msg.reply('device already set') as Promise;
45 | }
46 |
47 | const firmwares = await getDevice(device.identifier);
48 |
49 | const usage = new Usage(msg.client as KlasaClient, '(version:version)', ' ');
50 | usage.createCustomResolver('version', (arg: string) => {
51 | const exists = firmwares.firmwares!.some((x) => x.version === arg);
52 | if (exists) return arg;
53 | throw `Version doesn't exist`;
54 | });
55 |
56 | const prompt = new TextPrompt(msg, usage, { limit: 3 });
57 |
58 | const response = await prompt.run('Please enter version');
59 |
60 | const deviceName = device.name
61 | .replace(/\(.*\)$/, '')
62 | .replace(' Plus', '+')
63 | .replace('Pro Max', 'PM')
64 | .trim();
65 | const nickname = `${msg.member!.displayName} [${deviceName},${response[0]}]`;
66 |
67 | if (nickname.length > 32) return msg.reply('nickname too long') as Promise;
68 |
69 | msg.member!.setNickname(nickname);
70 |
71 | return msg.reply(`nickname set to \`${nickname}\``) as Promise;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/commands/Information/info.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 |
3 | export default class extends Command {
4 | constructor(store: CommandStore, file: string[], dir: string) {
5 | super(store, file, dir, {
6 | enabled: false,
7 | aliases: ['details', 'what'],
8 | guarded: true,
9 | description: (language) => language.get('COMMAND_INFO_DESCRIPTION'),
10 | });
11 | }
12 |
13 | async run(message: KlasaMessage) {
14 | return message.sendLocale('COMMAND_INFO');
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/commands/Information/listdevices.ts:
--------------------------------------------------------------------------------
1 | import { Command, KlasaMessage, RichDisplay } from 'klasa';
2 | import { MessageEmbed } from 'discord.js';
3 | import { getDevices, IDevice } from 'ipswme';
4 |
5 | interface CategorizedDevices {
6 | iPhone: IDevice[];
7 | iPod: IDevice[];
8 | iPad: IDevice[];
9 | AppleTV: IDevice[];
10 | AppleWatch: IDevice[];
11 | HomePod: IDevice[];
12 | }
13 |
14 | type DeviceCategory = 'iPhone' | 'iPod' | 'iPad' | 'AppleTV' | 'AppleWatch' | 'HomePod';
15 |
16 | const chunkSize = 20;
17 |
18 | export default class extends Command {
19 | description = 'List devices';
20 | aliases = ['list'];
21 |
22 | async run(msg: KlasaMessage) {
23 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
24 | if (!msg.guild.settings.get('channels.botspam')) return;
25 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
26 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
27 | }
28 | }
29 |
30 | const waitMsg = await msg.reply('Please wait...');
31 | const display = new RichDisplay();
32 | display.setFooterSuffix(` - Requested by ${msg.author.tag}`);
33 |
34 | const allDevices = await getDevices();
35 |
36 | const categorized = this.categorize(allDevices);
37 |
38 | for (const category in categorized) {
39 | const chunks = chunk(categorized[category as DeviceCategory], chunkSize);
40 |
41 | for (const i in chunks) {
42 | const embed = new MessageEmbed().setTimestamp();
43 |
44 | embed.setTitle(`${category} [${Number(i) + 1}/${chunks.length}]`);
45 |
46 | chunks[i].forEach((x) => embed.addField(x.name, x.identifier));
47 |
48 | display.addPage(embed);
49 | }
50 | }
51 |
52 | display.run(waitMsg as KlasaMessage);
53 |
54 | return null;
55 | }
56 |
57 | private categorize(devices: IDevice[]): CategorizedDevices {
58 | return devices.reduce((prev: any, val) => {
59 | if (val.name.startsWith('iPhone')) {
60 | if (!prev.iPhone) prev.iPhone = [];
61 | prev.iPhone.push(val);
62 | } else if (val.name.startsWith('iPod')) {
63 | if (!prev.iPod) prev.iPod = [];
64 | prev.iPod.push(val);
65 | } else if (val.name.startsWith('iPad')) {
66 | if (!prev.iPad) prev.iPad = [];
67 | prev.iPad.push(val);
68 | } else if (val.name.startsWith('Apple TV')) {
69 | if (!prev.AppleTV) prev.AppleTV = [];
70 | prev.AppleTV.push(val);
71 | } else if (val.name.startsWith('Apple Watch')) {
72 | if (!prev.AppleWatch) prev.AppleWatch = [];
73 | prev.AppleWatch.push(val);
74 | } else if (val.name.startsWith('HomePod')) {
75 | if (!prev.HomePod) prev.HomePod = [];
76 | prev.HomePod.push(val);
77 | }
78 |
79 | return prev;
80 | }, {});
81 | }
82 | }
83 |
84 | function chunk(arr: T[], len: number): T[][] {
85 | const chunks = [];
86 | for (let i = 0; i < arr.length; i += len) {
87 | chunks.push(arr.slice(i, i + len));
88 | }
89 | return chunks;
90 | }
91 |
--------------------------------------------------------------------------------
/src/commands/Information/removedevice.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 |
3 | export default class extends Command {
4 | constructor(store: CommandStore, file: string[], dir: string) {
5 | super(store, file, dir, {
6 | runIn: ['text'],
7 | requiredPermissions: ['MANAGE_NICKNAMES'],
8 | });
9 | }
10 |
11 | async run(msg: KlasaMessage) {
12 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
13 | if (!msg.guild.settings.get('channels.botspam')) return;
14 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
15 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
16 | }
17 | }
18 |
19 | if (!/^.+ \[.+\,.+\]$/.test(msg.member!.nickname!)) {
20 | return msg.reply('no device set') as Promise;
21 | }
22 |
23 | const nickname = msg.member!.nickname!.replace(/ \[.+,.+\]/, '');
24 |
25 | if (nickname.length > 32) return msg.reply('nickname too long') as Promise;
26 |
27 | msg.member!.setNickname(nickname);
28 |
29 | return msg.reply(`nickname set to \`${nickname}\``) as Promise;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/commands/Information/serverinfo.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, Timestamp } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text'],
9 | requiredPermissions: [],
10 | });
11 | }
12 |
13 | async run(msg: KlasaMessage) {
14 | const timestamp = new Timestamp('LLL');
15 |
16 | const embed = new MessageEmbed()
17 | .setTitle('Server Information')
18 | .setThumbnail(msg.guild.iconURL({ format: 'png', dynamic: true }))
19 | .setColor(msg.guild.roles.highest.color)
20 | .setDescription(`**${msg.guild.name}**`)
21 | .addField('Region', msg.guild.region, true)
22 | .addField('Boost Tier', msg.guild.premiumTier, true)
23 | .addField('Users', msg.guild.memberCount, true)
24 | .addField('Channels', msg.guild.channels.cache.size, true)
25 | .addField('Roles', msg.guild.roles.cache.size, true)
26 | .addField('Owner', msg.guild.owner, true)
27 | .addField('Verified', msg.guild.verified, true)
28 | .addField('Created', timestamp.display(msg.guild.createdAt))
29 | .setFooter(msg.guild.id)
30 | .setTimestamp();
31 |
32 | msg.send(embed);
33 | return null;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/commands/Information/stats.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, Duration, KlasaMessage } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | guarded: true,
9 | description: (language) => language.get('COMMAND_STATS_DESCRIPTION'),
10 | });
11 | }
12 |
13 | async run(msg: KlasaMessage) {
14 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
15 | if (!msg.guild.settings.get('channels.botspam')) return;
16 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
17 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
18 | }
19 | }
20 | let [users, memory] = [0, 0];
21 |
22 | if (this.client.shard) {
23 | const results = await this.client.shard.broadcastEval(
24 | `[this.users.cache.size, (process.memoryUsage().heapUsed / 1024 / 1024)]`,
25 | );
26 | for (const result of results) {
27 | users += result[0];
28 | memory += result[3];
29 | }
30 | }
31 |
32 | const embed = new MessageEmbed()
33 | .setTitle('Statistics')
34 | .setThumbnail(this.client.user.avatarURL({ format: 'jpg' }))
35 | .setColor('GREEN')
36 | .addField('Users', users || this.client.users.cache.size, true)
37 | .addField('Memory', `${(memory || process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`, true)
38 | .addField('Uptime', Duration.toNow(Date.now() - process.uptime() * 1000), true)
39 | .setTimestamp();
40 | msg.send(embed);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/commands/Information/userinfo.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, Timestamp } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text'],
9 | requiredPermissions: [],
10 | aliases: ['info'],
11 | description: 'Displays info of requested user.',
12 | extendedHelp: '!userinfo [optional: ]',
13 | usage: '[member:member]',
14 | });
15 | }
16 |
17 | async run(msg: KlasaMessage, [member]: [GuildMember]) {
18 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
19 | if (!msg.guild.settings.get('channels.botspam')) return;
20 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
21 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
22 | }
23 | }
24 | if (!member) member = msg.member;
25 |
26 | let roles = '';
27 | member.roles.cache.map((r) => (r.name != '@everyone' ? (roles += `${r} `) : ''));
28 |
29 | const timestamp = new Timestamp('LLL');
30 |
31 | const joined = timestamp.display(member.joinedAt);
32 | const created = timestamp.display(member.user.createdAt);
33 |
34 | const embed = new MessageEmbed()
35 | .setTitle('User Information')
36 | .setColor(member.roles.highest.color)
37 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
38 | .addField('Username', `${member.user.tag} (${member.user})`, true)
39 | .addField('Level', member.user.settings.get('level'), true)
40 | .addField('XP', member.user.settings.get('xp'), true)
41 | .addField('Roles', roles ? roles : 'No roles.')
42 | .addField('Joined', joined ? joined : 'N/A', true)
43 | .addField('Created', created ? created : 'N/A', true)
44 | .setFooter(member.user.id)
45 | .setTimestamp();
46 | msg.send(embed);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/commands/Information/warnpoints.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text'],
9 | requiredPermissions: ['EMBED_LINKS', 'SEND_MESSAGES'],
10 | requiredSettings: [],
11 | guarded: false,
12 | permissionLevel: 5,
13 | description: 'Views total warnpoints of member.',
14 | extendedHelp: '!warnpoints [optional: ]',
15 | usage: '[user:user]',
16 | });
17 | }
18 |
19 | async run(msg: KlasaMessage, [user]: [KlasaUser]) {
20 | if (!user) user = msg.author;
21 |
22 | const embed = new MessageEmbed()
23 | .setTitle('Warn Points')
24 | .setColor('ORANGE')
25 | .setThumbnail(user.avatarURL({ format: 'jpg' }))
26 | .addField('Member', `${user.tag} (<@${user.id}>)`)
27 | .addField('Warn Points', user.settings.get('warnPoints'))
28 | .setFooter(user.id)
29 | .setTimestamp();
30 |
31 | return msg.send(embed);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/commands/Information/xp.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text'],
9 | requiredPermissions: ['EMBED_LINKS', 'SEND_MESSAGES'],
10 | requiredSettings: [],
11 | aliases: ['xpstats'],
12 | guarded: false,
13 | permissionLevel: 0,
14 | description: 'Views xp of self or member.',
15 | extendedHelp: '!xp [optional: ',
16 | usage: '[user:user]',
17 | });
18 | }
19 |
20 | async run(msg: KlasaMessage, [user]: [KlasaUser]) {
21 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
22 | if (!msg.guild.settings.get('channels.botspam')) return;
23 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
24 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
25 | }
26 | }
27 | if (!user) user = msg.author;
28 |
29 | const leaderboard = this.client.users.cache
30 | .sort((a, b) => {
31 | if (a.settings.get('xp') > b.settings.get('xp')) return -1;
32 | if (a.settings.get('xp') < b.settings.get('xp')) return 1;
33 | return 0;
34 | })
35 | .array();
36 |
37 | let rank = 0;
38 | leaderboard.some((u) => {
39 | rank++;
40 | return u.id === user.id;
41 | });
42 |
43 | const embed = new MessageEmbed()
44 | .setTitle('Level Statistics')
45 | .setColor('GREEN')
46 | .setThumbnail(user.avatarURL({ format: 'jpg' }))
47 | .addField('Member', `${user.tag} (<@${user.id}>)`)
48 | .addField('Level', user.settings.get('level'), true)
49 | .addField(
50 | 'XP',
51 | `${user.settings.get('xp')}/${this.getRemainingXPForNextLevel(user.settings.get('level') + 1)}`,
52 | true,
53 | )
54 | .addField('Rank', `${rank} / ${leaderboard.length}`, true)
55 | .setFooter(user.id)
56 | .setTimestamp();
57 |
58 | return msg.send(embed);
59 | }
60 |
61 | getRemainingXPForNextLevel(levelToReach: number) {
62 | let level = 0;
63 | let xp = 0;
64 |
65 | for (let e = 0; e < levelToReach; e++) {
66 | xp = xp + 45 * level * (Math.floor(level / 10) + 1);
67 | level++;
68 | }
69 |
70 | return xp;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/commands/Information/xptop.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text'],
9 | requiredPermissions: ['EMBED_LINKS', 'SEND_MESSAGES'],
10 | requiredSettings: [],
11 | aliases: [],
12 | description: 'Views the Top 10 members with the most XP on server.',
13 | extendedHelp: 'No extended help available.',
14 | });
15 | }
16 |
17 | async run(msg: KlasaMessage) {
18 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
19 | if (!msg.guild.settings.get('channels.botspam')) return;
20 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
21 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
22 | }
23 | }
24 | const leaderboard = this.client.users.cache
25 | .sort((a, b) => {
26 | if (a.settings.get('xp') > b.settings.get('xp')) return -1;
27 | if (a.settings.get('xp') < b.settings.get('xp')) return 1;
28 | return 0;
29 | })
30 | .array()
31 | .slice(0, 10);
32 | let counter = 1;
33 | const embed = new MessageEmbed()
34 | .setTitle('Leaderboard')
35 | .setDescription(`${msg.guild.name}'s Leaderboard`)
36 | .setColor('GREEN')
37 | .setTimestamp();
38 | leaderboard.forEach((user) => {
39 | embed.addField(`#${counter++} - Level ${user.settings.get('level')}`, `<@${user.id}>`);
40 | });
41 |
42 | return msg.send(embed);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/commands/Moderation/ban.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | runIn: ['text'],
10 | requiredPermissions: ['BAN_MEMBERS'],
11 | guarded: true,
12 | permissionLevel: 5,
13 | description: 'Bans a member from the server.',
14 | extendedHelp: '!ban [optional: reason]',
15 | usage: ' [reason:...string]',
16 | usageDelim: ' ',
17 | });
18 | }
19 |
20 | async run(msg: KlasaMessage, [member, reason]: [GuildMember, string]) {
21 | if (member.id === this.client.user.id) return msg.send('I cannot ban myself.');
22 | if (member.id === msg.author.id) return msg.send('You cannot ban yourself.');
23 | if (member.roles.highest.position >= msg.member.roles.highest.position)
24 | return msg.send('Your highest role is even or lower than the target users role.');
25 | if (!member.bannable) return msg.send('The target is not bannable.');
26 | await member.ban({ days: 1, reason: reason ? reason : 'No reason.' });
27 | const c = await this.buildCase(msg, reason, member.user);
28 | this.sendEmbed(msg, member, reason, c);
29 | }
30 |
31 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser) {
32 | const c = new Case({
33 | id: this.client.settings.get('caseID'),
34 | type: 'BAN',
35 | date: Date.now(),
36 | until: undefined,
37 | modID: msg.author.id,
38 | modTag: msg.author.tag,
39 | reason: reason,
40 | punishment: 'PERMANENT',
41 | currentWarnPoints: user.settings.get('warnPoints'),
42 | });
43 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
44 | await user.settings.update('cases', c, { action: 'add' });
45 | return c;
46 | }
47 |
48 | sendEmbed(msg: KlasaMessage, member: GuildMember, reason: string, c: Case) {
49 | const channelID = msg.guild.settings.get('channels.public');
50 | if (!channelID) return;
51 | const embed = new MessageEmbed()
52 | .setTitle('Member Banned')
53 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
54 | .setColor('BLUE')
55 | .addField('Member', `${member.user.tag} (<@${member.id}>)`)
56 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`)
57 | .addField('Reason', reason ? reason : 'No reason.')
58 | .setFooter(`Case #${c.id} | ${member.id}`)
59 | .setTimestamp();
60 |
61 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
62 | channel.send(embed);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/commands/Moderation/cases.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser, RichDisplay } from 'klasa';
3 | import moment from 'moment';
4 | import Case from '../../util/case';
5 |
6 | export default class extends Command {
7 | constructor(store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: [],
12 | requiredSettings: [],
13 | aliases: ['caselog', 'caselogs'],
14 | autoAliases: true,
15 | bucket: 1,
16 | cooldown: 0,
17 | promptLimit: 0,
18 | promptTime: 30000,
19 | deletable: false,
20 | guarded: false,
21 | nsfw: false,
22 | permissionLevel: 5,
23 | description: 'Views cases performed on a member.',
24 | extendedHelp: '!cases ',
25 | usage: '',
26 | usageDelim: undefined,
27 | quotedStringSupport: false,
28 | subcommands: false,
29 | });
30 | }
31 |
32 | async run(msg: KlasaMessage, [user]: [KlasaUser]) {
33 | const display = new RichDisplay();
34 | let counter = 0;
35 | let embed = new MessageEmbed();
36 | await user.settings.sync();
37 | user.settings.get('cases').forEach((c: Case) => {
38 | embed.addField(
39 | `#${c.id} ${c.type} - Mod: ${c.modTag} Reason: ${c.reason} Punishment: ${c.punishment}`,
40 | `At: ${moment(new Date(c.date).toISOString()).format('LL')}`,
41 | );
42 | counter++;
43 | if (counter % 10 == 0) {
44 | display.addPage(embed);
45 | embed = new MessageEmbed();
46 | }
47 | });
48 | if (counter % 10 !== 0) display.addPage(embed);
49 |
50 | display.run(msg);
51 |
52 | return null;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/commands/Moderation/clem.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | runIn: ['text'],
10 | requiredPermissions: [],
11 | requiredSettings: [],
12 | aliases: [],
13 | autoAliases: true,
14 | bucket: 1,
15 | cooldown: 0,
16 | promptLimit: 0,
17 | promptTime: 30000,
18 | deletable: false,
19 | guarded: false,
20 | nsfw: false,
21 | permissionLevel: 7,
22 | description: 'Puts a member on Clem Protocol (Server and Bot Owner only)',
23 | extendedHelp: 'No extended help available.',
24 | usage: ' [reason:...string]',
25 | usageDelim: ' ',
26 | });
27 | }
28 |
29 | async run(msg: KlasaMessage, [member, reason]: [GuildMember, string]) {
30 | await member.user.settings.update('clem', true);
31 | await member.user.settings.update('xpFrozen', true);
32 | await member.user.settings.update('xp', 0);
33 | await member.user.settings.update('level', 0);
34 | const warnPointDiff = 599 - member.user.settings.get('warnPoints');
35 | await member.user.settings.update('warnPoints', 599);
36 | const c = await this.buildCase(msg, reason, member.user, warnPointDiff);
37 |
38 | this.sendEmbed(msg, member, reason, c);
39 |
40 | return null;
41 | }
42 |
43 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser, warnPointDiff: number) {
44 | const c = new Case({
45 | id: this.client.settings.get('caseID'),
46 | type: 'CLEM',
47 | date: Date.now(),
48 | until: undefined,
49 | modID: msg.author.id,
50 | modTag: msg.author.tag,
51 | reason: reason,
52 | punishment: warnPointDiff,
53 | currentWarnPoints: user.settings.get('warnPoints'),
54 | });
55 |
56 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
57 | await user.settings.update('cases', c, { action: 'add' });
58 | return c;
59 | }
60 |
61 | sendEmbed(msg: KlasaMessage, member: GuildMember, reason: string, c: Case) {
62 | const channelID = msg.guild.settings.get('channels.private');
63 | if (!channelID) return;
64 | const embed = new MessageEmbed()
65 | .setTitle('Member Clemed')
66 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
67 | .setColor('RED')
68 | .addField('Member', `${member.user.tag} (${member})`, true)
69 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`, true)
70 | .addField('Warn Points', member.user.settings.get('warnPoints'))
71 | .addField('Reason', reason ? reason : 'No reason.')
72 | .setFooter(`Case #${c.id} | ${member.user.id}`)
73 | .setTimestamp();
74 |
75 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
76 | channel.send(embed);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/commands/Moderation/filter.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import FilteredWord from '../../util/filteredWord';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text'],
9 | requiredPermissions: [],
10 | requiredSettings: [],
11 | aliases: [],
12 | autoAliases: true,
13 | bucket: 1,
14 | cooldown: 0,
15 | promptLimit: 0,
16 | promptTime: 30000,
17 | deletable: false,
18 | guarded: false,
19 | nsfw: false,
20 | permissionLevel: 6,
21 | description: 'Adds word to filter list with silent or reportable delete.',
22 | extendedHelp:
23 | '!filter True = Should Report, False = Silent Delete',
24 | usage: ' ',
25 | usageDelim: ' ',
26 | quotedStringSupport: false,
27 | subcommands: false,
28 | });
29 | }
30 |
31 | async run(msg: KlasaMessage, [notify, bypass, word]: [boolean, number, string]) {
32 | const fw = new FilteredWord({
33 | notify: notify,
34 | bypass: bypass,
35 | word: word,
36 | });
37 | await msg.guild.settings.update('filter.words', fw, { action: 'add' });
38 |
39 | msg.send(`Added ${word} ${notify ? 'with' : 'without'} notifications and bypass level ${bypass}.`);
40 |
41 | return null;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/commands/Moderation/filterlist.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import FilteredWord from '../../util/filteredWord';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | runIn: ['text', 'dm'],
9 | requiredPermissions: [],
10 | requiredSettings: [],
11 | aliases: [],
12 | autoAliases: true,
13 | bucket: 1,
14 | cooldown: 0,
15 | promptLimit: 0,
16 | promptTime: 30000,
17 | deletable: false,
18 | guarded: false,
19 | nsfw: false,
20 | permissionLevel: 4,
21 | description: 'Views the list of filtered words.',
22 | extendedHelp: '[show notify] [show only this bypass level]',
23 | usage: '[notify:boolean] [bypass:integer]',
24 | usageDelim: ' ',
25 | quotedStringSupport: false,
26 | subcommands: false,
27 | });
28 | }
29 |
30 | async run(msg: KlasaMessage, [notify, bypass]: [boolean, number]) {
31 | let content = 'Filterwords: ';
32 | msg.guild.settings.get('filter.words').forEach((fw: FilteredWord) => {
33 | const output = `**${fw.word}** [${fw.notify} - ${fw.bypass}], `;
34 |
35 | if (notify == undefined) content += output;
36 |
37 | if (notify == fw.notify && bypass == undefined) content += output;
38 | if (notify == fw.notify && bypass == fw.bypass) content += output;
39 | });
40 |
41 | return msg.send(content);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/commands/Moderation/kick.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | runIn: ['text'],
10 | requiredPermissions: ['KICK_MEMBERS'],
11 | guarded: true,
12 | permissionLevel: 5,
13 | description: 'Kicks a member from the server.',
14 | extendedHelp: ' [optional: reason]',
15 | usage: ' [reason:...string]',
16 | usageDelim: ' ',
17 | });
18 | }
19 |
20 | async run(msg: KlasaMessage, [member, reason]) {
21 | if (member.id === this.client.user.id) return msg.send('I cannot kick myself.');
22 | if (member.id === msg.author.id) return msg.send('You cannot kick yourself.');
23 | if (member.roles.highest.position >= msg.member.roles.highest.position)
24 | return msg.send('Your highest role is even or lower than the target users role.');
25 | if (!member.kickable) return msg.send('The target is not kickable.');
26 | await member.kick(reason);
27 | const c = await this.buildCase(msg, reason, member.user);
28 | this.sendEmbed(msg, member, reason, c);
29 | }
30 |
31 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser) {
32 | const c = new Case({
33 | id: this.client.settings.get('caseID'),
34 | type: 'KICK',
35 | date: Date.now(),
36 | until: undefined,
37 | modID: msg.author.id,
38 | modTag: msg.author.tag,
39 | reason: reason,
40 | punishment: undefined,
41 | currentWarnPoints: user.settings.get('warnPoints'),
42 | });
43 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
44 | await user.settings.update('cases', c, { action: 'add' });
45 | return c;
46 | }
47 |
48 | sendEmbed(msg: KlasaMessage, member: GuildMember, reason: string, c: Case) {
49 | const channelID = msg.guild.settings.get('channels.public');
50 | if (!channelID) return 'logchannel';
51 | const embed = new MessageEmbed()
52 | .setTitle('Member Kicked')
53 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
54 | .setColor('DARK_GREEN')
55 | .addField('Member', `${member.user.tag} (<@${member.id}>)`, true)
56 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`, true)
57 | .addField('Reason', reason ? reason : 'No reason.')
58 | .setFooter(`Case #${c.id} | ${member.id}`)
59 | .setTimestamp();
60 |
61 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
62 | channel.send(embed);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/commands/Moderation/liftwarn.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: false,
9 | runIn: ['text'],
10 | requiredPermissions: [],
11 | requiredSettings: [],
12 | aliases: [],
13 | autoAliases: true,
14 | bucket: 1,
15 | cooldown: 0,
16 | promptLimit: 0,
17 | promptTime: 30000,
18 | deletable: false,
19 | guarded: false,
20 | nsfw: false,
21 | permissionLevel: 5,
22 | description: 'Removes warn points from a previous case.',
23 | extendedHelp: ' [optional: reason]',
24 | usage: ' [reason:...string]',
25 | usageDelim: undefined,
26 | quotedStringSupport: false,
27 | subcommands: false,
28 | });
29 | }
30 |
31 | async run(msg: KlasaMessage, [member, points, reason]: [GuildMember, number, string]) {
32 | if (member.user.settings.get('warnPoints') < points) points = member.user.settings.get('warnPoints');
33 | await member.user.settings.update('warnPoints', points * -1);
34 | this.buildCase(msg, reason, points, member.user);
35 |
36 | return null;
37 | }
38 |
39 | async buildCase(msg: KlasaMessage, reason: string, points: number, user: KlasaUser) {
40 | const c = new Case({
41 | id: this.client.settings.get('caseID'),
42 | type: 'LIFTWARN',
43 | date: Date.now(),
44 | until: undefined,
45 | modID: msg.author.id,
46 | modTag: msg.author.tag,
47 | reason: reason,
48 | punishment: points,
49 | currentWarnPoints: user.settings.get('warnPoints'),
50 | });
51 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
52 | await user.settings.update('cases', c, { action: 'add' });
53 | return c;
54 | }
55 | }
56 | // .setColor('DARK_ORANGE')
57 |
--------------------------------------------------------------------------------
/src/commands/Moderation/lock.ts:
--------------------------------------------------------------------------------
1 | import { TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage } from 'klasa';
3 |
4 | export default class extends Command {
5 | constructor(store: CommandStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: false,
8 | runIn: ['text'],
9 | requiredPermissions: [],
10 | requiredSettings: [],
11 | aliases: [],
12 | autoAliases: true,
13 | bucket: 1,
14 | cooldown: 0,
15 | promptLimit: 0,
16 | promptTime: 30000,
17 | deletable: false,
18 | guarded: false,
19 | nsfw: false,
20 | permissionLevel: 7,
21 | description: 'Locks the channel by blocking @everyone from sending messages.',
22 | extendedHelp: 'No extended help available.',
23 | usage: '',
24 | usageDelim: undefined,
25 | quotedStringSupport: false,
26 | subcommands: false,
27 | });
28 | }
29 |
30 | async run(msg: KlasaMessage) {
31 | const everyone = msg.guild.roles.cache.first();
32 | const channel = msg.channel as TextChannel;
33 | const isLocked = channel.permissionsFor(everyone).has('SEND_MESSAGES');
34 |
35 | await channel.updateOverwrite(everyone, { SEND_MESSAGES: !isLocked });
36 |
37 | return msg.send(`Channel ${isLocked ? 'locked' : 'unlocked'}.`);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/commands/Moderation/mute.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import moment from 'moment';
4 | import Case from '../../util/case';
5 |
6 | export default class extends Command {
7 | constructor(store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: ['MANAGE_ROLES'],
12 | requiredSettings: [],
13 | guarded: true,
14 | permissionLevel: 5,
15 | description: 'Mutes a member with optional time and/or reason.',
16 | extendedHelp:
17 | ' [optional: duration (m = minutes, h = hours, d = days)] [optional: reason]',
18 | usage: ' [duration:time] [reason:...string]',
19 | usageDelim: ' ',
20 | });
21 | }
22 |
23 | async run(msg: KlasaMessage, [member, duration, reason]: [GuildMember, Date, string]) {
24 | if (member.id === this.client.user.id) return msg.send('I cannot mute myself.');
25 | if (member.id === msg.author.id) return msg.send('You cannot mute yourself.');
26 | if (member.roles.highest.position >= msg.member.roles.highest.position)
27 | return msg.send('Your highest role is even or lower than the target users role.');
28 | if (member.roles.cache.has(msg.guild.settings.get('roles.muted'))) return msg.send('Target is already muted.');
29 |
30 | await member.roles.add(msg.guild.settings.get('roles.muted'));
31 | await member.user.settings.update('isMuted', true);
32 |
33 | const c = await this.buildCase(msg, reason, member.user, duration);
34 |
35 | if (duration) {
36 | await this.client.schedule.create('unmute', duration, {
37 | data: {
38 | guildID: msg.guild.id,
39 | memberID: member.id,
40 | },
41 | catchUp: true,
42 | });
43 | }
44 |
45 | this.sendEmbed(msg, member, reason, duration, c);
46 | }
47 |
48 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser, duration: Date) {
49 | const c = new Case({
50 | id: this.client.settings.get('caseID'),
51 | type: 'MUTE',
52 | date: Date.now(),
53 | until: duration,
54 | modID: msg.author.id,
55 | modTag: msg.author.tag,
56 | reason: reason,
57 | punishment: duration ? moment().to(duration.toISOString(), true) : 'PERMANENT',
58 | currentWarnPoints: user.settings.get('warnPoints'),
59 | });
60 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
61 | await user.settings.update('cases', c, { action: 'add' });
62 | return c;
63 | }
64 |
65 | sendEmbed(msg: KlasaMessage, member: GuildMember, reason: string, duration: Date, c: Case) {
66 | const channelID = msg.guild.settings.get('channels.public');
67 | if (!channelID) return 'logchannel';
68 | const embed = new MessageEmbed()
69 | .setTitle('Member Muted')
70 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
71 | .setColor('LUMINOUS_VIVID_PINK')
72 | .addField('Member', `${member.user.tag} (<@${member.id}>)`, true)
73 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`, true)
74 | .addField('Duration', duration ? moment().to(duration.toISOString(), true) : 'PERMANENT')
75 | .addField('Reason', reason ? reason : 'No reason.')
76 | .setFooter(`Case #${c.id} | ${member.id}`)
77 | .setTimestamp();
78 |
79 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
80 | return channel.send(embed);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/commands/Moderation/offlineping.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 |
3 | export default class extends Command {
4 | constructor(store: CommandStore, file: string[], dir: string) {
5 | super(store, file, dir, {
6 | enabled: true,
7 | runIn: ['text'],
8 | requiredPermissions: [],
9 | requiredSettings: [],
10 | aliases: [],
11 | autoAliases: true,
12 | bucket: 1,
13 | cooldown: 0,
14 | promptLimit: 0,
15 | promptTime: 30000,
16 | deletable: false,
17 | guarded: false,
18 | nsfw: false,
19 | permissionLevel: 5,
20 | description: 'Option to be pinged of reportable actions when offline. (True will ping when offline.)',
21 | extendedHelp: '!offlineping ',
22 | usage: '',
23 | usageDelim: undefined,
24 | quotedStringSupport: false,
25 | subcommands: false,
26 | });
27 | }
28 |
29 | async run(msg: KlasaMessage, [bool]: [boolean]) {
30 | msg.author.settings.update('offlineReportPing', bool);
31 |
32 | return msg.send(`Offline ping set to: ${bool}`);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/commands/Moderation/purge.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license.
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import { TextChannel } from 'discord.js';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | permissionLevel: 5,
9 | requiredPermissions: ['MANAGE_MESSAGES'],
10 | runIn: ['text'],
11 | description: 'Purges a certain amount of messages between 1 and 100.',
12 | extendedHelp:
13 | '!purge [optional: link (website links) | invite (discord invites) | bots (any bot) | you (the bot) | me (yourself) | upload (attachments) | user (usertag | userid)',
14 | usage: ' [link|invite|bots|you|me|upload|user:user]',
15 | usageDelim: ' ',
16 | });
17 | }
18 |
19 | async run(msg: KlasaMessage, [limit, filter = null]) {
20 | let messages = await msg.channel.messages.fetch({ limit: 100 });
21 | if (filter) {
22 | const user = typeof filter !== 'string' ? filter : null;
23 | const type = typeof filter === 'string' ? filter : 'user';
24 | messages = messages.filter(this.getFilter(msg, type, user));
25 | }
26 | const messageArray = messages.array().slice(0, limit);
27 | await (msg.channel as TextChannel).bulkDelete(messageArray);
28 | const res = (await msg.send(
29 | `Successfully deleted ${messageArray.length} messages from ${limit}.`,
30 | )) as KlasaMessage;
31 | res.delete({ timeout: 2500 });
32 | return null;
33 | }
34 |
35 | getFilter(msg: KlasaMessage, filter: string, user: KlasaUser) {
36 | switch (filter) {
37 | case 'link':
38 | return (mes) => /https?:\/\/[^ /.]+\.[^ /.]+/.test(mes.content);
39 | case 'invite':
40 | return (mes) =>
41 | /(https?:\/\/)?(www\.)?(discord\.(gg|li|me|io)|discordapp\.com\/invite)\/.+/.test(mes.content);
42 | case 'bots':
43 | return (mes) => mes.author.bot;
44 | case 'you':
45 | return (mes) => mes.author.id === this.client.user.id;
46 | case 'me':
47 | return (mes) => mes.author.id === msg.author.id;
48 | case 'upload':
49 | return (mes) => mes.attachments.size > 0;
50 | case 'user':
51 | return (mes) => mes.author.id === user.id;
52 | default:
53 | return () => true;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/commands/Moderation/unban.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | runIn: ['text'],
10 | requiredPermissions: ['BAN_MEMBERS'],
11 | aliases: [],
12 | guarded: true,
13 | permissionLevel: 7,
14 | description: 'Unbans a member. (Server Owner and Bot Owner only)',
15 | extendedHelp: '!unban [optional: reason]',
16 | usage: ' [reason:...string]',
17 | usageDelim: ' ',
18 | });
19 | }
20 |
21 | async run(msg: KlasaMessage, [user, reason]: [KlasaUser, string]) {
22 | const bannedPlayers = await msg.guild.fetchBans();
23 | if (!bannedPlayers.has(user.id)) return msg.send('Target is not banned.');
24 | await msg.guild.members.unban(user, reason);
25 | if (user.settings.get('warnPoints') >= 600) user.settings.update('warnPoints', 450);
26 |
27 | const c = await this.buildCase(msg, reason, user);
28 |
29 | this.sendEmbed(msg, user, reason, c);
30 | }
31 |
32 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser) {
33 | const c = new Case({
34 | id: this.client.settings.get('caseID'),
35 | type: 'UNBAN',
36 | date: Date.now(),
37 | until: undefined,
38 | modID: msg.author.id,
39 | modTag: msg.author.tag,
40 | reason: reason,
41 | punishment: undefined,
42 | currentWarnPoints: user.settings.get('warnPoints'),
43 | });
44 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
45 | await user.settings.update('cases', c, { action: 'add' });
46 | return c;
47 | }
48 |
49 | sendEmbed(msg: KlasaMessage, user: KlasaUser, reason: string, c: Case) {
50 | const channelID = msg.guild.settings.get('channels.public');
51 | if (!channelID) return 'logchannel';
52 | const embed = new MessageEmbed()
53 | .setTitle('Member Unbanned')
54 | .setThumbnail(user.avatarURL({ format: 'jpg' }))
55 | .setColor('GREEN')
56 | .addField('Member', `${user.tag} (<@${user.id}>)`, true)
57 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`, true)
58 | .addField('Reason', reason ? reason : 'No reason.')
59 | .setFooter(`Case #${c.id} | ${user.id}`)
60 | .setTimestamp();
61 |
62 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
63 | channel.send(embed);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/commands/Moderation/unmute.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | runIn: ['text'],
10 | requiredPermissions: [],
11 | requiredSettings: [],
12 | guarded: false,
13 | permissionLevel: 5,
14 | description: 'Unmutes a member.',
15 | extendedHelp: '!unmute [optional: reason]',
16 | usage: ' [reason:...string]',
17 | usageDelim: ' ',
18 | });
19 | }
20 |
21 | async run(msg: KlasaMessage, [member, reason]: [GuildMember, string]) {
22 | if (!member.roles.cache.has(msg.guild.settings.get('roles.muted'))) return msg.send('Target is not muted.');
23 | if (!member.user.settings.get('isMuted')) msg.send('Target not muted.');
24 | await member.roles.remove(msg.guild.settings.get('roles.muted'));
25 | await member.user.settings.update('isMuted', false);
26 | const c = await this.buildCase(msg, reason, member.user);
27 | this.sendEmbed(msg, member, reason, c);
28 | }
29 |
30 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser) {
31 | const c = new Case({
32 | id: this.client.settings.get('caseID'),
33 | type: 'UNMUTE',
34 | date: Date.now(),
35 | until: undefined,
36 | modID: msg.author.id,
37 | modTag: msg.author.tag,
38 | reason: reason,
39 | punishment: undefined,
40 | currentWarnPoints: user.settings.get('warnPoints'),
41 | });
42 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
43 | await user.settings.update('cases', c, { action: 'add' });
44 | return c;
45 | }
46 |
47 | sendEmbed(msg: KlasaMessage, member: GuildMember, reason: string, c: Case) {
48 | const channelID = msg.guild.settings.get('channels.public');
49 | if (!channelID) return;
50 | const embed = new MessageEmbed()
51 | .setTitle('Member Unmuted')
52 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
53 | .setColor('GREEN')
54 | .addField('Member', `${member.user.tag} (<@${member.id}>)`)
55 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`)
56 | .addField('Reason', reason ? reason : 'No reason.')
57 | .setFooter(`Case #${c.id} | ${member.id}`)
58 | .setTimestamp();
59 |
60 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
61 | return channel.send(embed);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/commands/Moderation/warn.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, KlasaUser } from 'klasa';
3 | import Case from '../../util/case';
4 |
5 | export default class extends Command {
6 | constructor(store: CommandStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | runIn: ['text'],
9 | requiredPermissions: [],
10 | requiredSettings: [],
11 | guarded: true,
12 | permissionLevel: 5,
13 | description: 'Warn a member with points.',
14 | extendedHelp: '!warn [optional: reason]',
15 | usage: ' [reason:...string]',
16 | usageDelim: ' ',
17 | quotedStringSupport: false,
18 | subcommands: false,
19 | });
20 | }
21 |
22 | async run(msg: KlasaMessage, [member, points, reason]: [GuildMember, number, string]) {
23 | if (points <= 0) return msg.send('🤔');
24 | let warnPoints = member.user.settings.get('warnPoints');
25 | if (member.roles.highest.position >= msg.member.roles.highest.position)
26 | return msg.send('Your highest role is even or lower than the target users role.');
27 | await member.user.settings.update('warnPoints', (warnPoints += points));
28 | const c = await this.buildCase(msg, reason, points, member.user);
29 |
30 | this.sendWarnEmbed(msg, member, points, reason, c);
31 | if (warnPoints >= 600) {
32 | if (!member.bannable) return msg.send('Could not ban.');
33 | await member.ban({ days: 1, reason: '600 or more Warnpoints reached.' });
34 | await this.sendBanEmbed(msg, member);
35 | }
36 |
37 | if (warnPoints >= 400 && !member.user.settings.get('warnKicked')) {
38 | if (!member.kickable) return msg.send('Could not kick.');
39 | await member.kick('400 or more Warnpoints reached.');
40 | await member.user.settings.update('warnKicked', true);
41 | await this.sendKickEmbed(msg, member);
42 | }
43 | }
44 |
45 | async buildCase(msg: KlasaMessage, reason: string, points: number, user: KlasaUser) {
46 | const c = new Case({
47 | id: this.client.settings.get('caseID'),
48 | type: 'WARN',
49 | date: Date.now(),
50 | until: undefined,
51 | modID: msg.author.id,
52 | modTag: msg.author.tag,
53 | reason: reason,
54 | punishment: points,
55 | currentWarnPoints: user.settings.get('warnPoints'),
56 | });
57 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
58 | await user.settings.update('cases', c, { action: 'add' });
59 | return c;
60 | }
61 |
62 | sendWarnEmbed(msg: KlasaMessage, member: GuildMember, points: number, reason = 'No reason.', c: Case) {
63 | const channelID = msg.guild.settings.get('channels.public');
64 | if (!channelID) return;
65 | const embed = new MessageEmbed()
66 | .setTitle('Member Warned')
67 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
68 | .setColor('ORANGE')
69 | .addField('Member', `${member.user.tag} (<@${member.user.id}>)`, true)
70 | .addField('Mod', `${msg.author.tag} (<@${msg.author.id}>)`, true)
71 | .addField('Increase', points, true)
72 | .addField('Reason', reason)
73 | .setFooter(`Case #${c.id} | ${member.user.id}`)
74 | .setTimestamp();
75 |
76 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
77 | channel.send(embed);
78 | }
79 |
80 | async sendKickEmbed(msg: KlasaMessage, member: GuildMember) {
81 | const c = new Case({
82 | id: this.client.settings.get('caseID'),
83 | type: 'KICK',
84 | date: Date.now(),
85 | until: undefined,
86 | modID: this.client.user.id,
87 | modTag: this.client.user.tag,
88 | reason: '400 or more Warnpoints reached.',
89 | punishment: undefined,
90 | currentWarnPoints: member.user.settings.get('warnPoints'),
91 | });
92 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
93 | await member.user.settings.update('cases', c, { action: 'add' });
94 | const channelID = msg.guild.settings.get('channels.public');
95 | if (!channelID) return;
96 | const embed = new MessageEmbed()
97 | .setTitle('Member Kicked')
98 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
99 | .setColor('ORANGE')
100 | .addField('Member', `${member.user.tag} (<@${member.user.id}>)`, true)
101 | .addField('Mod', msg.author.tag, true)
102 | .addField('Reason', '400 or more Warnpoints reached.')
103 | .setFooter(`Case #${c.id} | ${member.user.id}`)
104 | .setTimestamp();
105 |
106 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
107 | channel.send(embed);
108 | }
109 |
110 | async sendBanEmbed(msg: KlasaMessage, member: GuildMember) {
111 | const c = new Case({
112 | id: this.client.settings.get('caseID'),
113 | type: 'BAN',
114 | date: Date.now(),
115 | until: undefined,
116 | modID: this.client.user.id,
117 | modTag: this.client.user.tag,
118 | reason: '600 or more Warnpoints reached.',
119 | punishment: 'PERMANENT',
120 | currentWarnPoints: member.user.settings.get('warnPoints'),
121 | });
122 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
123 | await member.user.settings.update('cases', c, { action: 'add' });
124 | const channelID = msg.guild.settings.get('hannels.public');
125 | if (!channelID) return;
126 | const embed = new MessageEmbed()
127 | .setTitle('Member Banned')
128 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
129 | .setColor('RED')
130 | .addField('Member', `${member.user.tag} (<@${member.user.id}>)`, true)
131 | .addField('Mod', msg.author.tag, true)
132 | .addField('Reason', '600 or more Warnpoints reached.')
133 | .setFooter(`Case #${c.id} | ${member.user.id}`)
134 | .setTimestamp();
135 |
136 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
137 | channel.send(embed);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/commands/Voice/loop.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import JanetClient from '../../lib/client';
3 | import Dispatcher from '../../util/dispatcher';
4 |
5 | export default class extends Command {
6 | client: JanetClient;
7 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: [],
12 | aliases: ['l'],
13 | cooldown: 5,
14 | description: (lang) => lang.get('LOOP_DESCRIPTION'),
15 | });
16 | }
17 |
18 | async run(msg: KlasaMessage) {
19 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
20 | if (!msg.guild.settings.get('channels.botspam')) return;
21 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
22 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
23 | }
24 | }
25 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
26 | if (!dispatcher) return msg.send('No music playing in here.');
27 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
28 | return msg.send('We need to be in the same voice channel.');
29 | }
30 | dispatcher.loop = !dispatcher.loop;
31 | return msg.send(`Loop is: ${dispatcher.loop}`);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/commands/Voice/nowplaying.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import JanetClient from '../../lib/client';
3 | import Dispatcher from '../../util/dispatcher';
4 |
5 | export default class extends Command {
6 | client: JanetClient;
7 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: ['EMBED_LINKS'],
12 | aliases: ['np'],
13 | cooldown: 5,
14 | description: (lang) => lang.get('NOWPLAYING_DESCRIPTION'),
15 | });
16 | }
17 |
18 | async run(msg: KlasaMessage) {
19 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
20 | if (!msg.guild.settings.get('channels.botspam')) return;
21 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
22 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
23 | }
24 | }
25 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
26 | if (!dispatcher) return msg.send('No music playing in here.');
27 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
28 | return msg.send('We need to be in the same voice channel.');
29 | }
30 | return msg.send(`Now playing: ${dispatcher.current.info.title}`);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/commands/Voice/pause.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import JanetClient from '../../lib/client';
3 | import Dispatcher from '../../util/dispatcher';
4 |
5 | export default class extends Command {
6 | client: JanetClient;
7 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: ['EMBED_LINKS'],
12 | cooldown: 5,
13 | description: (lang) => lang.get('PAUSE_DESCRIPTION'),
14 | });
15 | }
16 |
17 | async run(msg: KlasaMessage) {
18 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
19 | if (!msg.guild.settings.get('channels.botspam')) return;
20 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
21 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
22 | }
23 | }
24 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
25 | if (!msg.guild.settings.get('channels.botspam')) return;
26 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
27 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
28 | }
29 | }
30 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
31 | if (!dispatcher) return msg.send('No music playing in here.');
32 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
33 | return msg.send('We need to be in the same voice channel.');
34 | }
35 | return msg.send('I could not pause/unpause');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/commands/Voice/play.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaClient, KlasaMessage, TextPrompt, Usage } from 'klasa';
3 | import { ShoukakuTrackList } from 'shoukaku';
4 | import JanetClient from '../../lib/client';
5 |
6 | export default class extends Command {
7 | client: JanetClient;
8 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
9 | super(store, file, dir, {
10 | enabled: true,
11 | runIn: ['text'],
12 | requiredPermissions: ['EMBED_LINKS'],
13 | aliases: ['p'],
14 | cooldown: 0,
15 | description: (lang) => lang.get('PLAY_DESCRIPTION'),
16 | usage: '',
17 | usageDelim: ' ',
18 | });
19 | }
20 |
21 | async run(msg: KlasaMessage, [song]: [string]) {
22 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
23 | if (!msg.guild.settings.get('channels.botspam')) return;
24 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
25 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
26 | }
27 | }
28 | if (!msg.member.voice.channel) return msg.send('You are not in a VoiceChannel right now.');
29 | const node = this.client.shoukaku.getNode();
30 | const tracks = await node.rest.resolve(song, 'youtube');
31 | if (!tracks) return msg.send('No tracks found.');
32 | if (Array.isArray(tracks)) {
33 | const dispatcher = await this.client.queue.handleTrack(node, tracks.shift(), msg);
34 | tracks.forEach((track) => {
35 | this.client.queue.handleTrack(node, track, msg);
36 | });
37 | msg.send('Added Playlist...');
38 | if (dispatcher) await dispatcher.play();
39 | return null;
40 | }
41 |
42 | // Should be a LoadTrackResponse at this point.
43 | const ltr = tracks as ShoukakuTrackList;
44 | if (Array.isArray(ltr.tracks)) {
45 | ltr.tracks = ltr.tracks.slice(0, 5);
46 | const embed = new MessageEmbed()
47 | .setTitle('Music search')
48 | .setDescription('Type in the number of the track you wanna play...');
49 | let counter = 1;
50 | for (const track of ltr.tracks) {
51 | embed.addField(`**#${counter++}** ${track.info.title}`, track.info.author);
52 | }
53 | await msg.send(embed);
54 | const usage = new Usage(msg.client as KlasaClient, '(selection:selection)', ' ');
55 | usage.createCustomResolver('selection', (arg: string) => {
56 | const tracknumber = parseInt(arg);
57 | if (tracknumber > 0 && tracknumber <= ltr.tracks.length) return tracknumber;
58 | throw `Track number doesn't exist`;
59 | });
60 | const prompt = new TextPrompt(msg, usage, { limit: 3 });
61 | const response = (await prompt.run('Please select the Track')) as number;
62 | const dispatcher = await this.client.queue.handleTrack(node, ltr.tracks[response - 1], msg);
63 | msg.send(`Added **${ltr.tracks[response - 1].info.title}** to the queue!`);
64 | if (dispatcher) await dispatcher.play();
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/commands/Voice/queue.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { Command, CommandStore, KlasaMessage, RichDisplay } from 'klasa';
3 | import JanetClient from '../../lib/client';
4 | import Dispatcher from '../../util/dispatcher';
5 |
6 | export default class extends Command {
7 | client: JanetClient;
8 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
9 | super(store, file, dir, {
10 | enabled: true,
11 | runIn: ['text'],
12 | requiredPermissions: ['EMBED_LINKS'],
13 | aliases: ['q'],
14 | cooldown: 5,
15 | description: (lang) => lang.get('QUEUE_DESCRIPTION'),
16 | });
17 | }
18 |
19 | async run(msg: KlasaMessage) {
20 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
21 | if (!msg.guild.settings.get('channels.botspam')) return;
22 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
23 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
24 | }
25 | }
26 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
27 | if (!dispatcher) return msg.send('No music playing in here.');
28 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
29 | return msg.send('We need to be in the same voice channel.');
30 | }
31 | const display = new RichDisplay();
32 | let embed = this.generateQueueEmbed();
33 | if (dispatcher.queue.length === 0) return this.sendEmptyQueueEmbed(msg);
34 | let counter = 0;
35 | for (const song of dispatcher.queue) {
36 | embed.addField(`**#${counter}** ${song.info.title}`, song.info.author);
37 | counter++;
38 | if (counter % 10 === 0) {
39 | display.addPage(embed);
40 | embed = this.generateQueueEmbed();
41 | }
42 | }
43 | if (counter % 10 !== 0) display.addPage(embed);
44 | display.run(msg, {
45 | jump: false,
46 | stop: false,
47 | firstLast: false,
48 | time: 30000,
49 | });
50 | return null;
51 | }
52 |
53 | async sendEmptyQueueEmbed(msg: KlasaMessage): Promise {
54 | const embed = new MessageEmbed()
55 | .setTitle('Queue')
56 | .setColor('GREEN')
57 | .setTimestamp()
58 | .setDescription('The Queue is empty!');
59 | return (await msg.send(embed)) as KlasaMessage;
60 | }
61 |
62 | generateQueueEmbed(): MessageEmbed {
63 | const embed = new MessageEmbed().setTitle('Queue').setColor('GREEN').setTimestamp();
64 | return embed;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/commands/Voice/skip.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import JanetClient from '../../lib/client';
3 | import Dispatcher from '../../util/dispatcher';
4 |
5 | export default class extends Command {
6 | client: JanetClient;
7 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: ['EMBED_LINKS'],
12 | cooldown: 5,
13 | description: (lang) => lang.get('SKIP_DESCRIPTION'),
14 | });
15 | }
16 |
17 | async run(msg: KlasaMessage) {
18 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
19 | if (!msg.guild.settings.get('channels.botspam')) return;
20 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
21 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
22 | }
23 | }
24 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
25 | if (!dispatcher) return msg.send('No music playing in here.');
26 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
27 | return msg.send('We need to be in the same voice channel.');
28 | }
29 | dispatcher.loop = false;
30 | if (dispatcher.player.stopTrack()) return msg.send('Skipped the track.');
31 | return msg.send('I could not skip the track');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/commands/Voice/stop.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import JanetClient from '../../lib/client';
3 | import Dispatcher from '../../util/dispatcher';
4 |
5 | export default class extends Command {
6 | client: JanetClient;
7 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: ['EMBED_LINKS'],
12 | aliases: ['leave'],
13 | cooldown: 5,
14 | description: (lang) => lang.get('STOP_DESCRIPTION'),
15 | });
16 | }
17 |
18 | async run(msg: KlasaMessage) {
19 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
20 | if (!msg.guild.settings.get('channels.botspam')) return;
21 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
22 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
23 | }
24 | }
25 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
26 | if (!dispatcher) return msg.send('No music playing in here.');
27 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
28 | return msg.send('We need to be in the same voice channel.');
29 | }
30 | dispatcher.onEvent(undefined);
31 | return msg.send('I am stopping the music.');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/commands/Voice/volume.ts:
--------------------------------------------------------------------------------
1 | import { Command, CommandStore, KlasaMessage } from 'klasa';
2 | import JanetClient from '../../lib/client';
3 | import Dispatcher from '../../util/dispatcher';
4 |
5 | export default class extends Command {
6 | client: JanetClient;
7 | constructor(client: JanetClient, store: CommandStore, file: string[], dir: string) {
8 | super(store, file, dir, {
9 | enabled: true,
10 | runIn: ['text'],
11 | requiredPermissions: [],
12 | cooldown: 5,
13 | description: (lang) => lang.get('VOLUME_DESCRIPTION'),
14 | usage: '[volume:int]',
15 | });
16 | }
17 |
18 | async run(msg: KlasaMessage, [volume]: [number]) {
19 | if (!(await msg.hasAtLeastPermissionLevel(5))) {
20 | if (!msg.guild.settings.get('channels.botspam')) return;
21 | if (msg.channel.id != msg.guild.settings.get('channels.botspam')) {
22 | return msg.send(`Command only allowed in <#${msg.guild.settings.get('channels.botspam')}>`);
23 | }
24 | }
25 | const dispatcher = this.client.queue.get(msg.guild.id) as Dispatcher;
26 | if (!dispatcher) return msg.send('No music playing in here.');
27 | if (msg.member.voice.channel.id != dispatcher.player.voiceConnection.voiceChannelID) {
28 | return msg.send('We need to be in the same voice channel.');
29 | }
30 | if (volume > 200 || volume < 1) return msg.send('Volume restriction 1%-200%');
31 | dispatcher as Dispatcher;
32 | await dispatcher.player.setVolume(volume);
33 | return msg.send(`Set volume to ${volume}`);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { KlasaClientOptions } from 'klasa';
2 |
3 | /**
4 | * The following are all client options for Klasa/Discord.js.
5 | * Any option that you wish to use the default value can be removed from this file.
6 | * This file is init with defaults from both Klasa and Discord.js.
7 | */
8 |
9 | export const config: KlasaClientOptions = {
10 | /**
11 | * General Options
12 | */
13 | // Disables/Enables a process.on('unhandledRejection'...) handler
14 | production: false,
15 | owners: process.env.OWNERS.split(','),
16 | // The default language that comes with klasa. More base languages can be found on Klasa-Pieces
17 | language: 'en-US',
18 | // The default configurable prefix for each guild
19 | prefix: '!',
20 | // If custom settings should be preserved when a guild removes your bot
21 | preserveSettings: true,
22 | // The time in ms to add to ratelimits, to ensure you wont hit a 429 response
23 | restTimeOffset: 500,
24 | // A presence to login with
25 | presence: {},
26 |
27 | /**
28 | * Caching Options
29 | */
30 | fetchAllMembers: true,
31 | messageCacheMaxSize: 200,
32 | messageCacheLifetime: 0,
33 | commandMessageLifetime: 1800,
34 | // The above 2 options are ignored while the interval is 0
35 | messageSweepInterval: 0,
36 |
37 | /**
38 | * Command Handler Options
39 | */
40 | commandEditing: false,
41 | commandLogging: false,
42 | typing: false,
43 |
44 | /**
45 | * Database Options
46 | */
47 | providers: {
48 | postgresql: {
49 | host: process.env.DB_HOST,
50 | port: process.env.DB_PORT,
51 | database: process.env.DB_NAME,
52 | user: process.env.DB_USER,
53 | password: process.env.DB_PASSWORD,
54 | options: {},
55 | },
56 |
57 | default: 'postgresql',
58 | },
59 |
60 | /**
61 | * Custom Prompt Defaults
62 | */
63 | customPromptDefaults: {
64 | time: 30000,
65 | limit: Infinity,
66 | quotedStringSupport: false,
67 | },
68 |
69 | /**
70 | * Klasa Piece Defaults
71 | */
72 | pieceDefaults: {
73 | commands: {
74 | aliases: [],
75 | autoAliases: true,
76 | bucket: 1,
77 | cooldown: 0,
78 | description: '',
79 | enabled: true,
80 | guarded: false,
81 | nsfw: false,
82 | permissionLevel: 0,
83 | promptLimit: 0,
84 | promptTime: 30000,
85 | requiredSettings: [],
86 | requiredPermissions: 0,
87 | runIn: ['text', 'dm'],
88 | subcommands: false,
89 | usage: '',
90 | quotedStringSupport: false,
91 | deletable: false,
92 | },
93 | events: {
94 | enabled: true,
95 | once: false,
96 | },
97 | extendables: {
98 | enabled: true,
99 | appliesTo: [],
100 | },
101 | finalizers: { enabled: true },
102 | inhibitors: {
103 | enabled: true,
104 | spamProtection: false,
105 | },
106 | languages: { enabled: true },
107 | monitors: {
108 | enabled: true,
109 | ignoreBots: true,
110 | ignoreSelf: true,
111 | ignoreOthers: true,
112 | ignoreWebhooks: true,
113 | ignoreEdits: true,
114 | },
115 | providers: {
116 | enabled: true,
117 | },
118 | tasks: { enabled: true },
119 | },
120 |
121 | /**
122 | * Console Event Handlers (enabled/disabled)
123 | */
124 | consoleEvents: {
125 | debug: false,
126 | error: true,
127 | log: true,
128 | verbose: false,
129 | warn: true,
130 | wtf: true,
131 | },
132 |
133 | /**
134 | * Custom Setting Gateway Options
135 | */
136 | gateways: {
137 | guilds: {},
138 | users: {},
139 | clientStorage: {},
140 | },
141 | };
142 | export const token = process.env.DISCORD_TOKEN;
143 |
--------------------------------------------------------------------------------
/src/events/guildMemberAdd.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import ASCIIFolder from 'fold-to-ascii';
3 | import { Event, EventStore } from 'klasa';
4 |
5 | export default class extends Event {
6 | constructor(store: EventStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | once: false,
10 | });
11 | }
12 |
13 | async run(member: GuildMember) {
14 | if (member.guild.settings.get('filter.enableWordFiltering')) {
15 | const nick = ASCIIFolder.foldMaintaining(member.displayName).toLowerCase();
16 |
17 | for (const filteredWord of member.guild.settings.get('filter.words')) {
18 | if (!nick.includes(filteredWord.word.toLowerCase())) continue;
19 | member.setNickname('change name pls', 'filtered word');
20 | }
21 | }
22 |
23 | if (member.guild.settings.get('roles.member')) {
24 | member.roles.add(member.guild.settings.get('roles.member'));
25 | }
26 |
27 | let channelID = member.guild.settings.get('channels.private');
28 | if (!channelID) return;
29 |
30 | const embed = new MessageEmbed()
31 | .setTitle('Member Joined')
32 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
33 | .setColor('GREEN')
34 | .addField('User', `${member.user.tag} (<@${member.user.id}>)`, true)
35 | .addField('Warnpoints', member.user.settings.get('warnPoints'), true)
36 | .addField('Joined', member.joinedAt)
37 | .addField('Created', member.user.createdAt)
38 | .setTimestamp()
39 | .setFooter(member.user.id);
40 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
41 | channel.send(embed);
42 |
43 | channelID = member.guild.settings.get('channels.reports');
44 | if (!channelID) return;
45 | if (member.user.settings.get('isMuted')) {
46 | await member.roles.add(member.guild.settings.get('roles.muted'));
47 | embed.setTitle('Mute Evasion').setColor('RED');
48 |
49 | channel.send(embed);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/events/guildMemberRemove.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import { Event, EventStore } from 'klasa';
3 |
4 | export default class extends Event {
5 | constructor(store: EventStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | once: false,
9 | });
10 | }
11 |
12 | async run(member: GuildMember) {
13 | if (!member.guild.settings.get('channels.private')) return;
14 | const embed = new MessageEmbed()
15 | .setTitle('Member Left')
16 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
17 | .setColor('#9012FE')
18 | .addField('User', `${member.user.tag} (<@${member.user.id}>)`)
19 | .setFooter(member.user.id)
20 | .setTimestamp();
21 |
22 | const channel = this.client.channels.cache.get(member.guild.settings.get('channels.private')) as TextChannel;
23 | channel.send(embed);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/events/guildMemberUpdate.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import ASCIIFolder from 'fold-to-ascii';
3 | import { Event, EventStore } from 'klasa';
4 | import JanetClient from '../lib/client';
5 |
6 | export default class extends Event {
7 | client: JanetClient;
8 | constructor(client: JanetClient, store: EventStore, file: string[], dir: string) {
9 | super(store, file, dir, {
10 | enabled: true,
11 | once: false,
12 | });
13 | }
14 |
15 | async run(oldMember: GuildMember, newMember: GuildMember) {
16 | if (!(oldMember || newMember)) return;
17 | if (oldMember.nickname != newMember.nickname) return this.nickNameChange(oldMember, newMember);
18 | if (newMember.roles.cache.size != oldMember.roles.cache.size) return this.roleChange(oldMember, newMember);
19 | }
20 |
21 | nickNameChange(oldMember: GuildMember, newMember: GuildMember) {
22 | const channelID = oldMember.guild.settings.get('channels.private');
23 | if (!channelID) return;
24 |
25 | const nick = ASCIIFolder.foldMaintaining(newMember.displayName).toLowerCase();
26 |
27 | if (oldMember.guild.settings.get('filter.enableWordFiltering')) {
28 | for (const filteredWord of oldMember.guild.settings.get('filter.words')) {
29 | if (!nick.includes(filteredWord.word.toLowerCase())) continue;
30 | newMember.setNickname('change name pls', 'filtered word');
31 | }
32 | }
33 |
34 | const embed = new MessageEmbed()
35 | .setTitle('Member Renamed')
36 | .setThumbnail(oldMember.user.avatarURL({ format: 'jpg' }))
37 | .setColor('ORANGE')
38 | .addField('Member', `${oldMember.user.tag} (<@${oldMember.id}>)`)
39 | .addField('Old Nickname', oldMember.nickname ? oldMember.nickname : 'No Nickname', true)
40 | .addField('New Nickname', newMember.nickname ? newMember.nickname : 'No Nickname', true)
41 | .setFooter(oldMember.user.id)
42 | .setTimestamp();
43 |
44 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
45 | channel.send(embed);
46 | }
47 |
48 | roleChange(oldMember: GuildMember, newMember: GuildMember) {
49 | const channelID = oldMember.guild.settings.get('channels.private');
50 | if (!channelID) return;
51 | const newRole = newMember.roles.cache.difference(oldMember.roles.cache);
52 |
53 | newRole.delete(newMember.guild.settings.get('roles.member'));
54 | if (newRole.size < 1) return;
55 |
56 | const embedTitle =
57 | newMember.roles.cache.size > oldMember.roles.cache.size ? 'Member Role Added' : 'Member Role Removed';
58 |
59 | const embed = new MessageEmbed()
60 | .setTitle(embedTitle)
61 | .setThumbnail(oldMember.user.avatarURL({ format: 'jpg' }))
62 | .setColor('BLUE')
63 | .addField('Member', `${oldMember.user.tag} (<@${oldMember.id}>)`)
64 | .addField('Role', `${newRole.map((role) => role.name)}`, true)
65 | .setFooter(oldMember.user.id)
66 | .setTimestamp();
67 |
68 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
69 | channel.send(embed);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/events/messageDelete.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed, TextChannel } from 'discord.js';
2 | import { Event, EventStore, KlasaMessage } from 'klasa';
3 |
4 | export default class extends Event {
5 | constructor(store: EventStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | });
9 | }
10 |
11 | async run(msg: KlasaMessage) {
12 | if (msg.author.bot) return;
13 | const channelID = msg.guild.settings.get('channels.private');
14 | if (!channelID) return;
15 | if (!msg.content) return;
16 | if (msg.channel.id === channelID) return;
17 | for (const channel of msg.guild.settings.get('logging.excludedChannels')) {
18 | if (msg.channel.id === channel) return;
19 | }
20 | const embed = new MessageEmbed()
21 | .setTitle('Message Deleted')
22 | .setThumbnail(msg.author.avatarURL({ format: 'jpg' }))
23 | .setColor('#FF0000')
24 | .addField('User', `${msg.author.tag} (<@${msg.author.id}>)`, true)
25 | .addField('Channel', `<#${msg.channel.id}>`, true)
26 | .addField('Message', msg.content)
27 | .setFooter(msg.author.id)
28 | .setTimestamp();
29 |
30 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
31 | channel.send(embed);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/events/messageUpdate.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed, TextChannel } from 'discord.js';
2 | import { Event, EventStore, KlasaMessage } from 'klasa';
3 |
4 | export default class extends Event {
5 | constructor(store: EventStore, file: string[], dir: string) {
6 | super(store, file, dir, {
7 | enabled: true,
8 | name: 'janetMessageUpdate',
9 | event: 'messageUpdate',
10 | });
11 | }
12 |
13 | async run(oldMsg: KlasaMessage, newMsg: KlasaMessage) {
14 | if (oldMsg.author.bot) return;
15 | if (!newMsg.content || !oldMsg.content) return;
16 | if (oldMsg.content === newMsg.content) return;
17 | const channelID = oldMsg.guild.settings.get('channels.private');
18 | if (!channelID) return;
19 | for (const channel of oldMsg.guild.settings.get('logging.excludedChannels')) {
20 | if (oldMsg.channel.id === channel) return;
21 | }
22 | const embed = new MessageEmbed()
23 | .setTitle('Message Updated')
24 | .setThumbnail(oldMsg.author.avatarURL({ format: 'jpg' }))
25 | .setColor('BLUE')
26 | .addField('User', `${oldMsg.author.tag} (<@${oldMsg.author.id}>)`)
27 | .addField('Old Message', oldMsg.content)
28 | .addField('New Message', newMsg.content)
29 | .addField('Channel', `<#${oldMsg.channel.id}>`)
30 | .setTimestamp();
31 |
32 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
33 | channel.send(embed);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/events/voiceStateUpdate.ts:
--------------------------------------------------------------------------------
1 | import { GuildMember } from 'discord.js';
2 | import { Event, EventStore } from 'klasa';
3 | import JanetClient from '../lib/client';
4 | import Dispatcher from '../util/dispatcher';
5 |
6 | export default class extends Event {
7 | client: JanetClient;
8 | constructor(client: JanetClient, store: EventStore, file: string[], dir: string) {
9 | super(store, file, dir, {
10 | enabled: true,
11 | once: false,
12 | });
13 | }
14 |
15 | async run(oldMember: GuildMember, newMember: GuildMember) {
16 | if (!(oldMember && newMember)) return;
17 | const dispatcher = this.client.queue.get(oldMember.guild.id) as Dispatcher;
18 | if (!dispatcher) return;
19 | const voiceChannel = oldMember.guild.channels.cache.get(dispatcher.player.voiceConnection.voiceChannelID);
20 | if (voiceChannel.members.size === 1) dispatcher.onEvent(undefined);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/finalizers/delete.ts:
--------------------------------------------------------------------------------
1 | import { Finalizer, KlasaMessage } from 'klasa';
2 |
3 | export default class extends Finalizer {
4 | async run(msg: KlasaMessage) {
5 | if (msg.guild && msg.deletable) {
6 | await msg.delete({ timeout: 2500 });
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | import { PermissionLevels } from 'klasa';
3 | import { config, token } from './config';
4 | import JanetClient from './lib/client';
5 |
6 | config.permissionLevels = new PermissionLevels()
7 | // everyone can use these commands
8 | .add(0, () => true)
9 | .add(
10 | 1,
11 | ({ guild, member }) =>
12 | guild &&
13 | guild.settings.get('roles.memberplus') &&
14 | member.roles.cache.has(guild.settings.get('roles.memberplus')),
15 | )
16 | .add(
17 | 2,
18 | ({ guild, member }) =>
19 | guild &&
20 | guild.settings.get('roles.memberpro') &&
21 | member.roles.cache.has(guild.settings.get('roles.memberpro')),
22 | )
23 | .add(
24 | 3,
25 | ({ guild, member }) =>
26 | guild &&
27 | guild.settings.get('roles.memberedition') &&
28 | member.roles.cache.has(guild.settings.get('roles.memberedition')),
29 | )
30 | .add(
31 | 4,
32 | ({ guild, member }) =>
33 | guild && guild.settings.get('roles.genius') && member.roles.cache.has(guild.settings.get('roles.genius')),
34 | )
35 | .add(
36 | 5,
37 | ({ guild, member }) =>
38 | guild &&
39 | guild.settings.get('roles.moderator') &&
40 | member.roles.cache.has(guild.settings.get('roles.moderator')),
41 | )
42 | // Members of guilds must have 'MANAGE_GUILD' permission
43 | .add(6, ({ guild, member }) => guild && member.permissions.has('MANAGE_GUILD'), { fetch: true })
44 | // The member using this command must be the guild owner
45 | .add(7, ({ guild, member }) => guild && member === guild.owner, { fetch: true })
46 | /*
47 | * Allows the Bot Owner to use any lower commands
48 | * and causes any command with a permission level 9 or lower to return an error if no check passes.
49 | */
50 | .add(9, ({ author, client }) => client.owners.has(author), { break: true })
51 | // Allows the bot owner to use Bot Owner only commands, which silently fail for other users.
52 | .add(10, ({ author, client }) => client.owners.has(author));
53 |
54 | new JanetClient(config).login(token);
55 |
--------------------------------------------------------------------------------
/src/lib/client.ts:
--------------------------------------------------------------------------------
1 | import { KlasaClient, KlasaClientOptions } from 'klasa';
2 | import { Shoukaku } from 'shoukaku';
3 | import Queue from '../util/queue';
4 |
5 | const shoukakuConfig = {
6 | moveOnDisconnect: true,
7 | resumable: true,
8 | resumableTimeout: 30,
9 | reconnectTries: 10,
10 | restTimeout: 10000,
11 | };
12 |
13 | const shoukakuNodes = [
14 | {
15 | name: process.env.VOICE_NAME,
16 | host: process.env.VOICE_HOST,
17 | port: parseInt(process.env.VOICE_PORT),
18 | auth: process.env.VOICE_PASSWORD,
19 | },
20 | ];
21 |
22 | export default class JanetClient extends KlasaClient {
23 | shoukaku: Shoukaku;
24 | queue: Queue;
25 | constructor(options: KlasaClientOptions) {
26 | super(options);
27 | KlasaClient.defaultClientSchema.add('caseID', 'integer', { default: 0, min: 0, configurable: false });
28 | KlasaClient.defaultGuildSchema
29 | .add('roles', (folder) => {
30 | folder.add('muted', 'role');
31 | folder.add('member', 'role');
32 | folder.add('memberplus', 'role');
33 | folder.add('memberpro', 'role');
34 | folder.add('memberedition', 'role');
35 | folder.add('genius', 'role');
36 | folder.add('moderator', 'role');
37 | })
38 | .add('channels', (folder) => {
39 | folder.add('public', 'textchannel');
40 | folder.add('private', 'textchannel');
41 | folder.add('reports', 'textchannel');
42 | folder.add('botspam', 'textchannel');
43 | })
44 | .add('filter', (folder) => {
45 | folder.add('enableWordFiltering', 'boolean', { default: true }),
46 | folder.add('enableSpoilerFiltering', 'boolean', { default: true });
47 | folder.add('words', 'filteredword', { array: true, configurable: false });
48 | folder.add('excludedChannels', 'textchannel', { array: true });
49 | folder.add('enableInviteFiltering', 'boolean', { default: false });
50 | })
51 | .add('logging', (folder) => {
52 | folder.add('excludedChannels', 'textchannel', { array: true });
53 | });
54 | KlasaClient.defaultUserSchema
55 | .add('isMuted', 'boolean', { default: false, configurable: false })
56 | .add('clem', 'boolean', { default: false, configurable: false })
57 | .add('xpFrozen', 'boolean', { default: false, configurable: false })
58 | .add('warnKicked', 'boolean', { default: false, configurable: false })
59 | .add('warnPoints', 'integer', { default: 0, min: 0, configurable: false })
60 | .add('xp', 'integer', { default: 0, min: 0, configurable: false })
61 | .add('level', 'integer', { default: 0, min: 0, configurable: false })
62 | .add('cases', 'any', { array: true, configurable: false })
63 | .add('offlineReportPing', 'boolean', { default: false, configurable: false });
64 | this.shoukaku = new Shoukaku(this, shoukakuNodes, shoukakuConfig);
65 | this.queue = new Queue(this);
66 | this.shoukaku.on('ready', (name, resumed) =>
67 | console.log(
68 | `Lavalink Node: ${name} is now connected. This connection is ${
69 | resumed ? 'resumed' : 'a new connection'
70 | }`,
71 | ),
72 | );
73 | this.shoukaku.on('error', (name, error) => console.log(`Lavalink Node: ${name} emitted an error.`, error));
74 | this.shoukaku.on('close', (name, code, reason) =>
75 | console.log(`Lavalink Node: ${name} closed with code ${code}. Reason: ${reason || 'No reason'}`),
76 | );
77 | this.shoukaku.on('disconnected', (name, reason) =>
78 | console.log(`Lavalink Node: ${name} disconnected. Reason: ${reason || 'No reason'}`),
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/monitors/experience.ts:
--------------------------------------------------------------------------------
1 | import { KlasaMessage, Monitor, MonitorStore } from 'klasa';
2 |
3 | export default class extends Monitor {
4 | constructor(store: MonitorStore, file: string[], dir: string) {
5 | super(store, file, dir, {
6 | enabled: true,
7 | ignoreBots: true,
8 | ignoreSelf: true,
9 | ignoreOthers: false,
10 | ignoreWebhooks: true,
11 | ignoreEdits: true,
12 | });
13 | }
14 |
15 | async run(message: KlasaMessage) {
16 | if (!message.guild) return;
17 | if (message.author.settings.get('xpFrozen')) return;
18 | const gainedXP = Math.floor(Math.random() * 10 + 1);
19 | //if (message.member.lastMessage.content === message.content) gainedXP = gainedXP * -1;
20 | const currentXP = message.author.settings.get('xp');
21 | await message.author.settings.update('xp', currentXP + gainedXP);
22 | await message.author.settings.update('level', this.getLevel(currentXP + gainedXP));
23 |
24 | if (message.author.settings.get('level') >= 15 && message.guild.settings.get('roles.memberplus')) {
25 | await message.member.roles.add(message.guild.settings.get('roles.memberplus'));
26 | }
27 |
28 | if (message.author.settings.get('level') >= 30 && message.guild.settings.get('roles.memberpro')) {
29 | await message.member.roles.add(message.guild.settings.get('roles.memberpro'));
30 | }
31 |
32 | if (message.author.settings.get('level') >= 50 && message.guild.settings.get('roles.memberedition')) {
33 | await message.member.roles.add(message.guild.settings.get('roles.memberedition'));
34 | }
35 | }
36 |
37 | getLevel(userXP: number) {
38 | let level = 0;
39 | let xp = 0;
40 | while (xp <= userXP) {
41 | xp = xp + 45 * level * (Math.floor(level / 10) + 1);
42 | level++;
43 | }
44 | return level;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/monitors/filter.ts:
--------------------------------------------------------------------------------
1 | import { Collection, GuildMember, MessageEmbed, TextChannel } from 'discord.js';
2 | import ASCIIFolder from 'fold-to-ascii';
3 | import { Duration, KlasaMessage, KlasaUser, Monitor, MonitorStore, RateLimit } from 'klasa';
4 | import Case from '../util/case';
5 |
6 | export default class extends Monitor {
7 | ratelimits: Collection;
8 |
9 | constructor(store: MonitorStore, file: string[], dir: string) {
10 | super(store, file, dir, {
11 | enabled: true,
12 | ignoreBots: false,
13 | ignoreSelf: true,
14 | ignoreOthers: false,
15 | ignoreWebhooks: true,
16 | ignoreEdits: false,
17 | });
18 |
19 | this.ratelimits = new Collection();
20 | }
21 |
22 | async run(msg: KlasaMessage) {
23 | if (!msg.guild.settings.get('filter.enableWordFiltering')) return;
24 |
25 | const content = ASCIIFolder.foldMaintaining(msg.content).toLowerCase();
26 | const filteredWords = [];
27 | let notify = false;
28 |
29 | for (const filterWord of msg.guild.settings.get('filter.words')) {
30 | if (!(content.indexOf(filterWord.word.toLowerCase()) > -1)) continue;
31 | if (await msg.hasAtLeastPermissionLevel(filterWord.bypass)) continue;
32 |
33 | filteredWords.push(filterWord.word);
34 | if (filterWord.notify) notify = true;
35 | }
36 |
37 | if (filteredWords.length === 0) return;
38 | const excludedChannels = msg.guild.settings.get('filter.excludedChannels');
39 | if (excludedChannels.some((excludedChannel) => msg.channel.id == excludedChannel)) return;
40 |
41 | await msg.delete();
42 |
43 | if (!this.ratelimits.has(msg.author.id)) {
44 | this.ratelimits.set(msg.author.id, new RateLimit(5, 10 * 1000));
45 | }
46 |
47 | const limiter = this.ratelimits.get(msg.author.id);
48 |
49 | if (limiter.limited && !msg.member.roles.cache.has(msg.guild.settings.get('roles.muted'))) {
50 | await msg.member.roles.add(msg.guild.settings.get('roles.muted'));
51 | await msg.member.user.settings.update('isMuted', true);
52 |
53 | const d = new Date();
54 | d.setMinutes(d.getMinutes() + 30);
55 | const c = await this.buildCase(msg, 'Filter Spam', msg.member.user, d);
56 | await this.client.schedule.create('unmute', d, {
57 | data: {
58 | guildID: msg.guild.id,
59 | memberID: msg.member.id,
60 | },
61 | catchUp: true,
62 | });
63 |
64 | this.sendEmbed(msg, msg.member, 'Filter Spam', d, c);
65 | }
66 |
67 | if (!limiter.limited) {
68 | limiter.drip();
69 | }
70 |
71 | if (!notify) return;
72 |
73 | const membersToPing = [];
74 | msg.guild.roles.cache.get(msg.guild.settings.get('roles.moderator')).members.map((member) => {
75 | if (
76 | !(
77 | member.presence.status === 'online' ||
78 | member.presence.status === 'idle' ||
79 | member.user.settings.get('offlineReportPing')
80 | )
81 | )
82 | return;
83 | membersToPing.push(member);
84 | });
85 |
86 | const channelID = msg.guild.settings.get('channels.reports');
87 | let toPing = '';
88 | if (membersToPing.length > 0) {
89 | membersToPing.forEach((member) => {
90 | toPing += `<@${member.user.id}> `;
91 | });
92 | }
93 | const embed = new MessageEmbed()
94 | .setTitle('Word filter')
95 | .setThumbnail(msg.member.user.avatarURL({ format: 'jpg' }))
96 | .setColor('RED')
97 | .addField('Member', `${msg.member.user.tag} (<@${msg.member.user.id}>)`, true)
98 | .addField('Channel', `<#${msg.channel.id}>`, true)
99 | .addField('Message', msg.content)
100 |
101 | .setTimestamp();
102 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
103 | channel.send(toPing, embed);
104 | }
105 |
106 | async buildCase(msg: KlasaMessage, reason: string, user: KlasaUser, duration: Date) {
107 | const c = new Case({
108 | id: this.client.settings.get('caseID'),
109 | type: 'MUTE',
110 | date: Date.now(),
111 | until: duration,
112 | modID: msg.author.id,
113 | modTag: msg.author.tag,
114 | reason: reason,
115 | punishment: duration ? Duration.toNow(duration) : 'PERMANENT',
116 | currentWarnPoints: user.settings.get('warnPoints'),
117 | });
118 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
119 | await user.settings.update('cases', c, { action: 'add' });
120 | return c;
121 | }
122 |
123 | sendEmbed(msg: KlasaMessage, member: GuildMember, reason: string, duration: Date, c: Case) {
124 | const channelID = msg.guild.settings.get('channels.public');
125 | if (!channelID) return 'logchannel';
126 | const embed = new MessageEmbed()
127 | .setTitle('Member Muted')
128 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
129 | .setColor('RED')
130 | .addField('Member', `${member.user.tag} (${member.user})`, true)
131 | .addField('Mod', `${msg.client.user.tag} (${msg.client.user})`, true)
132 | .addField('Duration', duration ? Duration.toNow(duration) : 'PERMANENT')
133 | .addField('Reason', reason ? reason : 'No reason.')
134 | .setFooter(`Case #${c.id} | ${member.id}`)
135 | .setTimestamp();
136 |
137 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
138 | return channel.send(embed);
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/monitors/invites.ts:
--------------------------------------------------------------------------------
1 | import { KlasaMessage, Monitor, MonitorStore } from 'klasa';
2 |
3 | export default class extends Monitor {
4 | constructor(store: MonitorStore, file: string[], dir: string) {
5 | super(store, file, dir, {
6 | enabled: true,
7 | ignoreBots: true,
8 | ignoreSelf: false,
9 | ignoreOthers: false,
10 | ignoreWebhooks: true,
11 | ignoreEdits: false,
12 | });
13 | }
14 |
15 | async run(msg: KlasaMessage) {
16 | if (!msg.guildSettings.get('filter.enableInviteFiltering')) return;
17 | const regex = /(https?:\/\/)?(www\.)?(discord\.(gg|li|me|io)|discordapp\.com\/invite)\/(.+)/;
18 | const matches = regex.exec(msg.content);
19 | if (!matches) return;
20 | if (matches[5].toLowerCase() !== 'jb') return msg.delete()
21 | }
22 | }
--------------------------------------------------------------------------------
/src/monitors/spoilers.ts:
--------------------------------------------------------------------------------
1 | import { KlasaMessage, Monitor, MonitorStore } from 'klasa';
2 |
3 | export default class extends Monitor {
4 | constructor(store: MonitorStore, file: string[], dir: string) {
5 | super(store, file, dir, {
6 | enabled: true,
7 | ignoreBots: true,
8 | ignoreSelf: false,
9 | ignoreOthers: false,
10 | ignoreWebhooks: true,
11 | ignoreEdits: false,
12 | });
13 | }
14 |
15 | async run(msg: KlasaMessage) {
16 | if (!msg.guild.settings.get('filter.enableSpoilerFiltering')) return;
17 | const roleID = msg.guild.settings.get('roles.moderator');
18 | if (roleID && msg.guild.roles.cache.get(roleID).position <= msg.member.roles.highest.position) return;
19 | await this.checkTextSpoiler(msg);
20 | await this.checkImageSpoiler(msg);
21 | }
22 |
23 | async checkTextSpoiler(msg: KlasaMessage) {
24 | if (!/\|{2}[\S\s]+\|{2}/gm.test(msg.content)) return;
25 | return msg.delete();
26 | }
27 |
28 | async checkImageSpoiler(msg: KlasaMessage) {
29 | if (!msg.attachments) return;
30 | if (msg.attachments.some((attachment) => attachment.spoiler)) return msg.delete();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/monitors/tweaks.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed } from 'discord.js';
2 | import { KlasaMessage, Monitor, MonitorStore, RichDisplay } from 'klasa';
3 | import fetch from 'node-fetch';
4 |
5 | export default class extends Monitor {
6 | constructor(store: MonitorStore, file: string[], dir: string) {
7 | super(store, file, dir, {
8 | enabled: true,
9 | ignoreBots: true,
10 | ignoreSelf: true,
11 | ignoreOthers: false,
12 | ignoreWebhooks: true,
13 | ignoreEdits: true,
14 | });
15 | }
16 |
17 | async run(msg: KlasaMessage) {
18 | const regex = new RegExp(/\[\[(.*)\]\]/);
19 | const matches = regex.exec(msg.content);
20 | if (!matches || !matches[1]) return;
21 | const response = await fetch(
22 | `https://tss-saver.cloud.tyk.io/repoapi/v1/repo?query=${encodeURIComponent(String(matches))}`,
23 | );
24 | const data = await response.json();
25 | if (!data.results.length) return msg.send('No Tweak found.');
26 | const tweak = data.results[0];
27 | const display = new RichDisplay().useCustomFooters();
28 | const tweakInfoEmbed = new MessageEmbed()
29 | .setTitle(tweak.display)
30 | .setThumbnail(tweak.img)
31 | .setColor('GREEN')
32 | .addField(
33 | 'Repo',
34 | `[${tweak.repo ? tweak.repo.name : tweak.repo_name}](${tweak.repo ? tweak.repo.url : tweak.repo_url})`,
35 | true,
36 | )
37 | .addField('Version', tweak.version, true)
38 | .addField('Price', tweak.paid ? await this.getTweakPrice(msg, tweak) : 'FREE', true)
39 | .addField('BundleID', tweak.name, true)
40 | .addField('Download', tweak.deb ? `[Click here](${tweak.deb})` : 'Not available.', true)
41 | .addField('Description', tweak.summary)
42 | .setFooter('Provided by: tss-saver.cloud.tyk.io')
43 | .setTimestamp();
44 | display.addPage(tweakInfoEmbed);
45 |
46 | const compatibilityData = await this.getCompatibilityData(tweak);
47 | const tweakCompatibilityEmbed = new MessageEmbed()
48 | .setTitle(tweak.display)
49 | .setThumbnail(tweak.img)
50 | .setColor('GREEN')
51 | .addField('Version', tweak.version)
52 | .setFooter('Provided by: jlippold.github.io')
53 | .setTimestamp();
54 |
55 | if (!compatibilityData || !(compatibilityData.length > 0))
56 | tweakCompatibilityEmbed.addField('Version Data', 'No version data found.');
57 | for (const version of compatibilityData) {
58 | let emoji = '❔';
59 | switch (version.status.toLowerCase()) {
60 | case 'working':
61 | emoji = '✅';
62 | break;
63 | case 'not working':
64 | emoji = '⛔';
65 | break;
66 | }
67 | tweakCompatibilityEmbed.addField(`iOS ${version.iOSVersion}`, emoji, true);
68 | }
69 | display.addPage(tweakCompatibilityEmbed);
70 | return display.run(msg, { firstLast: false, jump: false, time: 120000 });
71 | }
72 |
73 | async getTweakPrice(msg: KlasaMessage, tweak: { type: string; name: string }) {
74 | const response = await fetch(
75 | `https://tss-saver.cloud.tyk.io/repoapi/v1/price?type=${tweak.type}&query=${tweak.name}`,
76 | );
77 | if (response.status === 429) return msg.send('Ratelimit reached!');
78 | const data = await response.text();
79 | if (data.length === 0 || data == '0') return 'Paid';
80 | return `$${data}`;
81 | }
82 |
83 | async getCompatibilityData(tweak: { name: string; version: string }) {
84 | const result = [];
85 | const response = await fetch(`https://jlippold.github.io/tweakCompatible/json/packages/${tweak.name}.json`);
86 | if (response.status === 404) return result;
87 | const data = await response.json();
88 | const reports = data.versions.filter(
89 | (v: { tweakVersion: string; iOSVersion: string }) =>
90 | v.tweakVersion === tweak.version && parseInt(v.iOSVersion.split('.')[0]) >= 11,
91 | );
92 | for (const report of reports) {
93 | result.push({
94 | iOSVersion: report.iOSVersion,
95 | status: report.outcome.calculatedStatus,
96 | });
97 | }
98 | return result;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/providers/postgresql.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license.
2 | import { QueryBuilder, SQLProvider, util } from 'klasa';
3 | import { Pool } from 'pg';
4 |
5 | const { mergeDefault, isNumber } = util;
6 |
7 | /**
8 | * @param {string} value The string to sanitize as a key
9 | * @returns {string}
10 | * @private
11 | */
12 | function sanitizeKeyName(value) {
13 | if (typeof value !== 'string') throw new TypeError(`[SANITIZE_NAME] Expected a string, got: ${new Type(value)}`);
14 | if (/`|"/.test(value)) throw new TypeError(`Invalid input (${value}).`);
15 | if (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') return value;
16 | return `"${value}"`;
17 | }
18 |
19 | /**
20 | * @param {number} [min] The minimum value
21 | * @param {number} [max] The maximum value
22 | * @returns {string}
23 | * @private
24 | */
25 | function parseRange(min, max) {
26 | // Min value validation
27 | if (typeof min === 'undefined') return '';
28 | if (!isNumber(min)) {
29 | throw new TypeError(`[PARSE_RANGE] 'min' parameter expects an integer or undefined, got ${min}`);
30 | }
31 | if (min < 0) {
32 | throw new RangeError(`[PARSE_RANGE] 'min' parameter expects to be equal or greater than zero, got ${min}`);
33 | }
34 |
35 | // Max value validation
36 | if (typeof max !== 'undefined') {
37 | if (!isNumber(max)) {
38 | throw new TypeError(`[PARSE_RANGE] 'max' parameter expects an integer or undefined, got ${max}`);
39 | }
40 | if (max <= min) {
41 | throw new RangeError(
42 | `[PARSE_RANGE] 'max' parameter expects ${max} to be greater than ${min}. Got: ${max} <= ${min}`,
43 | );
44 | }
45 | }
46 |
47 | return `LIMIT ${min}${typeof max === 'number' ? `,${max}` : ''}`;
48 | }
49 |
50 | module.exports = class extends SQLProvider {
51 | constructor(...args) {
52 | super(...args);
53 | this.qb = new QueryBuilder({
54 | boolean: 'BOOL',
55 | integer: ({ max }) => (max !== null && max >= 2 ** 32 ? 'BIGINT' : 'INTEGER'),
56 | float: 'DOUBLE PRECISION',
57 | uuid: 'UUID',
58 | json: { type: 'JSON', resolver: (input) => `'${JSON.stringify(input)}'::json` },
59 | any: { type: 'JSON', resolver: (input) => `'${JSON.stringify(input)}'::json` },
60 | filteredword: { type: 'JSON', resolver: (input) => `'${JSON.stringify(input)}'::json` },
61 | case: { type: 'JSON', resolver: (input) => `'${JSON.stringify(input)}'::json` },
62 | array: (type) => `${type}[]`,
63 | arrayResolver: (values, piece, resolver) =>
64 | values.length ? `array[${values.map((value) => resolver(value, piece)).join(', ')}]` : "'{}'",
65 | formatDatatype: (name, datatype, def = null) =>
66 | `"${name}" ${datatype}${def !== null ? ` NOT NULL DEFAULT ${def}` : ''}`,
67 | });
68 | this.db = null;
69 | }
70 |
71 | async init() {
72 | const connection = mergeDefault(
73 | {
74 | host: 'localhost',
75 | port: 5432,
76 | database: '',
77 | user: '',
78 | password: '',
79 | options: {
80 | max: 20,
81 | idleTimeoutMillis: 30000,
82 | connectionTimeoutMillis: 2000,
83 | },
84 | },
85 | this.client.options.providers.postgresql,
86 | );
87 | this.db = new Pool(
88 | Object.assign(
89 | {
90 | host: connection.host,
91 | port: connection.port,
92 | user: connection.user,
93 | password: connection.password,
94 | database: connection.database,
95 | },
96 | connection.options,
97 | ),
98 | );
99 |
100 | this.db.on('error', (err) => this.client.emit('error', err));
101 | this.dbconnection = await this.db.connect();
102 | }
103 |
104 | shutdown() {
105 | this.dbconnection.release();
106 | return this.db.end();
107 | }
108 |
109 | /* Table methods */
110 |
111 | hasTable(table) {
112 | return this.runAll(`SELECT true FROM pg_tables WHERE tablename = '${table}';`)
113 | .then((result) => result.length !== 0 && result[0].bool === true)
114 | .catch(() => false);
115 | }
116 |
117 | createTable(table, rows) {
118 | if (rows)
119 | return this.run(
120 | `CREATE TABLE ${sanitizeKeyName(table)} (${rows
121 | .map(([k, v]) => `${sanitizeKeyName(k)} ${v}`)
122 | .join(', ')});`,
123 | );
124 | const gateway = this.client.gateways[table];
125 | if (!gateway)
126 | throw new Error(
127 | `There is no gateway defined with the name ${table} nor an array of rows with datatypes have been given. Expected any of either.`,
128 | );
129 |
130 | const schemaValues = [...gateway.schema.values(true)];
131 | return this.run(`
132 | CREATE TABLE ${sanitizeKeyName(table)} (
133 | ${[
134 | `id VARCHAR(${gateway.idLength || 18}) PRIMARY KEY NOT NULL UNIQUE`,
135 | ...schemaValues.map(this.qb.parse.bind(this.qb)),
136 | ].join(', ')}
137 | )`);
138 | }
139 |
140 | deleteTable(table) {
141 | return this.run(`DROP TABLE IF EXISTS ${sanitizeKeyName(table)};`);
142 | }
143 |
144 | countRows(table) {
145 | return this.runOne(`SELECT COUNT(*) FROM ${sanitizeKeyName(table)};`).then((result) => Number(result.count));
146 | }
147 |
148 | /* Row methods */
149 |
150 | getAll(table, entries = []) {
151 | if (entries.length) {
152 | return this.runAll(
153 | `SELECT * FROM ${sanitizeKeyName(table)} WHERE id IN ('${entries.join("', '")}');`,
154 | ).then((results) => results.map((output) => this.parseEntry(table, output)));
155 | }
156 | return this.runAll(`SELECT * FROM ${sanitizeKeyName(table)};`).then((results) =>
157 | results.map((output) => this.parseEntry(table, output)),
158 | );
159 | }
160 |
161 | getKeys(table) {
162 | return this.runAll(`SELECT id FROM ${sanitizeKeyName(table)};`).then((rows) => rows.map((row) => row.id));
163 | }
164 |
165 | get(table, key, value) {
166 | // If a key is given (id), swap it and search by id - value
167 | if (typeof value === 'undefined') {
168 | value = key;
169 | key = 'id';
170 | }
171 | return this.runOne(`SELECT * FROM ${sanitizeKeyName(table)} WHERE ${sanitizeKeyName(key)} = $1 LIMIT 1;`, [
172 | value,
173 | ]).then((output) => this.parseEntry(table, output));
174 | }
175 |
176 | has(table, id) {
177 | return this.runOne(`SELECT id FROM ${sanitizeKeyName(table)} WHERE id = $1 LIMIT 1;`, [id]).then((result) =>
178 | Boolean(result),
179 | );
180 | }
181 |
182 | getRandom(table) {
183 | return this.runOne(`SELECT * FROM ${sanitizeKeyName(table)} ORDER BY RANDOM() LIMIT 1;`);
184 | }
185 |
186 | getSorted(table, key, order = 'DESC', limitMin, limitMax) {
187 | return this.runAll(
188 | `SELECT * FROM ${sanitizeKeyName(table)} ORDER BY ${sanitizeKeyName(key)} ${order} ${parseRange(
189 | limitMin,
190 | limitMax,
191 | )};`,
192 | );
193 | }
194 |
195 | create(table, id, data) {
196 | const [keys, values] = this.parseUpdateInput(data, false);
197 |
198 | // Push the id to the inserts.
199 | if (!keys.includes('id')) {
200 | keys.push('id');
201 | values.push(id);
202 | }
203 | return this.run(
204 | `
205 | INSERT INTO ${sanitizeKeyName(table)} (${keys.map(sanitizeKeyName).join(', ')})
206 | VALUES (${Array.from({ length: keys.length }, (__, i) => `$${i + 1}`).join(', ')});`,
207 | values,
208 | );
209 | }
210 |
211 | update(table, id, data) {
212 | const [keys, values] = this.parseUpdateInput(data, false);
213 | return this.run(
214 | `
215 | UPDATE ${sanitizeKeyName(table)}
216 | SET ${keys.map((key, i) => `${sanitizeKeyName(key)} = $${i + 1}`)}
217 | WHERE id = '${id.replace(/'/g, "''")}';`,
218 | values,
219 | );
220 | }
221 |
222 | replace(...args) {
223 | return this.update(...args);
224 | }
225 |
226 | incrementValue(table, id, key, amount = 1) {
227 | return this.run(`UPDATE ${sanitizeKeyName(table)} SET $2 = $2 + $3 WHERE id = $1;`, [id, key, amount]);
228 | }
229 |
230 | decrementValue(table, id, key, amount = 1) {
231 | return this.run(`UPDATE ${sanitizeKeyName(table)} SET $2 = GREATEST(0, $2 - $3) WHERE id = $1;`, [
232 | id,
233 | key,
234 | amount,
235 | ]);
236 | }
237 |
238 | delete(table, id) {
239 | return this.run(`DELETE FROM ${sanitizeKeyName(table)} WHERE id = $1;`, [id]);
240 | }
241 |
242 | addColumn(table, piece) {
243 | return this.run(
244 | piece.type !== 'Folder'
245 | ? `ALTER TABLE ${sanitizeKeyName(table)} ADD COLUMN ${this.qb.parse(piece)};`
246 | : `ALTER TABLE ${sanitizeKeyName(table)} ${[...piece.values(true)]
247 | .map((subpiece) => `ADD COLUMN ${this.qb.parse(subpiece)}`)
248 | .join(', ')};`,
249 | );
250 | }
251 |
252 | removeColumn(table, columns) {
253 | if (typeof columns === 'string')
254 | return this.run(`ALTER TABLE ${sanitizeKeyName(table)} DROP COLUMN ${sanitizeKeyName(columns)};`);
255 | if (Array.isArray(columns))
256 | return this.run(
257 | `ALTER TABLE ${sanitizeKeyName(table)} DROP COLUMN ${columns.map(sanitizeKeyName).join(', ')};`,
258 | );
259 | throw new TypeError('Invalid usage of PostgreSQL#removeColumn. Expected a string or string[].');
260 | }
261 |
262 | updateColumn(table, piece) {
263 | const [column, datatype] = this.qb.parse(piece).split(' ');
264 | return this.run(
265 | `ALTER TABLE ${sanitizeKeyName(table)} ALTER COLUMN ${column} TYPE ${datatype}${
266 | piece.default
267 | ? `, ALTER COLUMN ${column} SET NOT NULL, ALTER COLUMN ${column} SET DEFAULT ${this.qb.parseValue(
268 | piece.default,
269 | piece,
270 | )}`
271 | : ''
272 | };`,
273 | );
274 | }
275 |
276 | getColumns(table, schema = 'public') {
277 | return this.runAll(
278 | `
279 | SELECT column_name
280 | FROM information_schema.columns
281 | WHERE table_schema = $1
282 | AND table_name = $2;
283 | `,
284 | [schema, table],
285 | ).then((result) => result.map((row) => row.column_name));
286 | }
287 |
288 | run(...sql) {
289 | return this.db.query(...sql).then((result) => result);
290 | }
291 |
292 | runAll(...sql) {
293 | return this.run(...sql).then((result) => result.rows);
294 | }
295 |
296 | runOne(...sql) {
297 | return this.run(...sql).then((result) => result.rows[0]);
298 | }
299 | };
300 |
--------------------------------------------------------------------------------
/src/serializers/case.ts:
--------------------------------------------------------------------------------
1 | import { Serializer, SerializerStore } from 'klasa';
2 | import Case from '../util/case';
3 |
4 | export default class extends Serializer {
5 | constructor(store: SerializerStore, file: string[], dir: string) {
6 | super(store, file, dir);
7 | }
8 |
9 | async deserialize(data) {
10 | return new Case(data);
11 | }
12 |
13 | serialize(data) {
14 | return data;
15 | }
16 |
17 | stringify(data) {
18 | return data;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/serializers/filteredword.ts:
--------------------------------------------------------------------------------
1 | import { Serializer, SerializerStore } from 'klasa';
2 | import FilteredWord from '../util/filteredWord';
3 |
4 | export default class extends Serializer {
5 | constructor(store: SerializerStore, file: string[], dir: string) {
6 | super(store, file, dir);
7 | }
8 |
9 | async deserialize(data) {
10 | return new FilteredWord(data);
11 | }
12 |
13 | serialize(data) {
14 | return data;
15 | }
16 |
17 | stringify(data) {
18 | return data;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/tasks/unmute.ts:
--------------------------------------------------------------------------------
1 | import { MessageEmbed, TextChannel } from 'discord.js';
2 | import { Task, TaskStore } from 'klasa';
3 | import Case from '../util/case';
4 |
5 | type unmuteData = {
6 | guildID: string;
7 | memberID: string;
8 | };
9 |
10 | export default class extends Task {
11 | constructor(store: TaskStore, file: string[], dir: string) {
12 | super(store, file, dir, { enabled: true });
13 | }
14 |
15 | async run(data: unmuteData) {
16 | const guild = this.client.guilds.cache.get(data.guildID);
17 | if (!guild) return;
18 | const member = guild.members.cache.get(data.memberID);
19 | if (!member) return;
20 |
21 | if (!member.roles.cache.has(guild.settings.get('roles.muted'))) return;
22 | await member.roles.remove(guild.settings.get('roles.muted'));
23 | await member.user.settings.update('isMuted', false);
24 |
25 | const c = new Case({
26 | id: this.client.settings.get('caseID'),
27 | type: 'UNMUTE',
28 | date: Date.now(),
29 | until: undefined,
30 | modID: this.client.user.id,
31 | modTag: this.client.user.tag,
32 | reason: 'Temporary mute expired!',
33 | punishment: undefined,
34 | currentWarnPoints: member.user.settings.get('warnPoints'),
35 | });
36 | await this.client.settings.update('caseID', this.client.settings.get('caseID') + 1);
37 | await member.user.settings.update('cases', c, { action: 'add' });
38 |
39 | const channelID = guild.settings.get('channels.public');
40 | if (!channelID) return;
41 | const embed = new MessageEmbed()
42 | .setTitle('Member Unmuted')
43 | .setThumbnail(member.user.avatarURL({ format: 'jpg' }))
44 | .setColor('GREEN')
45 | .addField('Member', `${member.user.tag} (<@${member.id}>)`)
46 | .addField('Mod', this.client.user.tag)
47 | .addField('Reason', 'Temporary mute expired!')
48 | .setFooter(`Case #${c.id} | ${member.id}`)
49 | .setTimestamp();
50 |
51 | const channel = this.client.channels.cache.get(channelID) as TextChannel;
52 | channel.send(embed);
53 |
54 | return null;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true,
6 | "alwaysStrict": true,
7 | "declaration": false,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "incremental": true,
11 | "skipLibCheck": true,
12 | "allowJs": true,
13 | "lib": ["esnext", "esnext.array", "esnext.asynciterable", "esnext.intl", "esnext.symbol"],
14 | "module": "commonjs",
15 | "moduleResolution": "node",
16 | "noUnusedLocals": true,
17 | "outDir": "../dist",
18 | "pretty": true,
19 | "removeComments": true,
20 | "resolveJsonModule": true,
21 | "sourceMap": true,
22 | "strictFunctionTypes": true,
23 | "strictBindCallApply": true,
24 | "target": "esnext",
25 | "types": ["node", "discord.js", "klasa"]
26 | },
27 | "exclude": ["node_modules/**/*"]
28 | }
29 |
--------------------------------------------------------------------------------
/src/util/case.ts:
--------------------------------------------------------------------------------
1 | type CaseOptions = {
2 | id: number;
3 | type: string;
4 | date: number;
5 | until: Date;
6 | modID: string;
7 | modTag: string;
8 | reason: string;
9 | punishment: string | number;
10 | currentWarnPoints: number;
11 | };
12 |
13 | export default class Case {
14 | id: number;
15 | type: string;
16 | date: number;
17 | until: Date;
18 | modID: string;
19 | modTag: string;
20 | reason: string;
21 | punishment: string | number;
22 | currentWarnPoints: number;
23 |
24 | constructor(options: CaseOptions) {
25 | this.id = options.id;
26 | this.type = options.type;
27 | this.date = options.date;
28 | this.until = options.until;
29 | this.modID = options.modID;
30 | this.modTag = options.modTag;
31 | this.reason = options.reason;
32 | (this.punishment = options.punishment), (this.currentWarnPoints = options.currentWarnPoints);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/util/dispatcher.ts:
--------------------------------------------------------------------------------
1 | import { TextChannel } from 'discord.js';
2 | import { KlasaGuild } from 'klasa';
3 | import { ShoukakuPlayer, ShoukakuTrack } from 'shoukaku';
4 | import JanetClient from '../lib/client';
5 |
6 | class EventHandlers {
7 | static onEvent(param: unknown) {
8 | if (param instanceof Error || param instanceof Object) console.error(param);
9 | this.leave();
10 | }
11 | static leave() {
12 | return null;
13 | }
14 | }
15 |
16 | class Dispatcher {
17 | client: JanetClient;
18 | guild: KlasaGuild;
19 | textChanel: TextChannel;
20 | player: ShoukakuPlayer;
21 | queue: ShoukakuTrack[];
22 | playing: boolean;
23 | onEvent: (param: unknown) => void;
24 | current: ShoukakuTrack;
25 | loop: boolean;
26 | constructor(options) {
27 | this.client = options.client;
28 | this.guild = options.guild;
29 | this.textChanel = options.textChannel;
30 | this.player = options.player;
31 | this.queue = [];
32 | this.playing = null;
33 | this.loop = false;
34 |
35 | this.onEvent = EventHandlers.onEvent.bind(this);
36 |
37 | this.player.on('end', () => {
38 | this.play().catch((error) => {
39 | console.error(error);
40 | this.leave();
41 | });
42 | });
43 | this.player.on('closed', this.onEvent);
44 | this.player.on('error', this.onEvent);
45 | this.player.on('nodeDisconnect', this.onEvent);
46 | this.player.on('trackException', this.onEvent);
47 | }
48 |
49 | async leave() {
50 | this.player.disconnect();
51 | this.client.queue.delete(this.guild.id);
52 | }
53 |
54 | async play() {
55 | if (!this.client.queue.has(this.guild.id) || (!this.queue.length && !this.loop)) return this.leave();
56 | if (!this.loop) this.current = this.queue.shift();
57 | await this.player.playTrack(this.current.track);
58 | this.playing = true;
59 | if (!this.loop) this.textChanel.send(`Playing: ${this.current.info.title}`);
60 | }
61 |
62 | async addTrack(track: ShoukakuTrack) {
63 | this.queue.push(track);
64 | }
65 | }
66 |
67 | export default Dispatcher;
68 |
--------------------------------------------------------------------------------
/src/util/filteredWord.ts:
--------------------------------------------------------------------------------
1 | type FilteredWordOptions = {
2 | notify: boolean;
3 | bypass: number;
4 | word: string;
5 | };
6 |
7 | export default class FilteredWord {
8 | notify: boolean;
9 | bypass: number;
10 | word: string;
11 |
12 | constructor(options: FilteredWordOptions) {
13 | this.notify = options.notify;
14 | this.bypass = options.bypass;
15 | this.word = options.word;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/util/queue.ts:
--------------------------------------------------------------------------------
1 | import { Client, KlasaMessage } from 'klasa';
2 | import { ShoukakuSocket, ShoukakuTrack } from 'shoukaku';
3 | import Dispatcher from './dispatcher';
4 |
5 | export default class Queue extends Map {
6 | public client: Client;
7 |
8 | constructor(client: Client) {
9 | super();
10 | this.client = client;
11 | }
12 |
13 | async handleTrack(node: ShoukakuSocket, track: ShoukakuTrack, msg: KlasaMessage): Promise {
14 | if (!track) return;
15 | let dispatcher = this.get(msg.guild.id);
16 | if (!dispatcher) {
17 | const player = await node.joinVoiceChannel({
18 | guildID: msg.guild.id,
19 | voiceChannelID: msg.member.voice.channelID,
20 | });
21 | dispatcher = new Dispatcher({
22 | client: this.client,
23 | guild: msg.guild,
24 | textChannel: msg.channel,
25 | player: player,
26 | });
27 | dispatcher.queue.push(track);
28 | this.set(msg.guild.id, dispatcher);
29 | return dispatcher;
30 | }
31 | dispatcher.queue.push(track);
32 | return null;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------