├── .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 | --------------------------------------------------------------------------------