├── LICENSE ├── README.md ├── app.py ├── commands ├── autorole.py ├── core.py ├── invite_tracker.py ├── mod.py ├── modlog.py └── test.py ├── config └── config.json ├── handlers ├── ErrorHandler.py ├── GuildHandler.py ├── MessageHandler.py ├── ModlogHandler.py └── ReadyHandler.py ├── requirements.txt ├── res └── banner.txt └── utils ├── checks ├── bot_checks.py └── checks.py ├── colors.py ├── config ├── config.py └── setup_bot.py ├── ctx.py ├── db └── models.py ├── formats.py ├── help ├── HelpFormater.py └── Paginator.py └── utils.py /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 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 General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://cdn.discordapp.com/attachments/377619690513498133/406183455123177481/OpenSauce.svg) ![](https://cdn.discordapp.com/attachments/330777295952543744/478325842188042241/license.svg) [![Patreon](https://img.shields.io/badge/patreon-donate-green.svg)](https://www.patreon.com/OfficialBoobBot) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/boobbot) 2 | ![](https://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg) ![](https://forthebadge.com/images/badges/built-with-love.svg) 3 | # Yes, Daddy 4 | ## A multi purpose bot meant for nsfw discord servers 5 | 6 | This Repository is meant for education and allowing people to improve the current bot more than anything, However if you insist on self-hosting you will need to do several things see prerequisites for more information 7 | 8 | ## Prerequisites 9 | 10 | * Requires [RethinkDb](https://www.rethinkdb.com/) 11 | 12 | ## Built With 13 | 14 | * [discord.py](https://github.com/Rapptz/discord.py) - A Python library providing coverage of [the Discord API](https://discord.com/developers/docs/intro). 15 | 16 | ## Contributing 17 | 18 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 19 | 20 | ## License 21 | 22 | This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details 23 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | 3 | import discord 4 | from discord.ext.commands import AutoShardedBot 5 | 6 | from utils.db.models import get_prefix, check_db 7 | from utils.config.setup_bot import setup_bot, shut_down 8 | from utils.config.setup_bot import setup_logger 9 | 10 | 11 | description = "a bot" 12 | 13 | 14 | class Bot(AutoShardedBot): 15 | def __init__(self): 16 | atexit.register(shut_down, self) 17 | message = 'ydhelp | ydinvite' 18 | sgame = discord.Game(name=message, type=0) 19 | super().__init__(command_prefix=get_prefix, game=sgame, description=description) 20 | setup_bot(self) 21 | token = self.keys['debug_token'] if self.debug else self.keys['token'] 22 | try: 23 | self.loop.run_until_complete(self.start(token)) 24 | except discord.errors.LoginFailure or discord.errors.HTTPException as e: 25 | self.log.error(f"shit: {repr(e)}", exc_info=False) 26 | 27 | 28 | if __name__ == "__main__": 29 | setup_logger() 30 | check_db() 31 | Bot() 32 | -------------------------------------------------------------------------------- /commands/autorole.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from utils.checks import checks 4 | 5 | 6 | class Autorole: 7 | 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @checks.admin_or_permissions(manage_roles=True) 12 | @commands.group(no_pm=True) 13 | async def autorole(self, ctx): 14 | """Autorole Settings 15 | Adds a role on join to users and bots.""" 16 | if ctx.invoked_subcommand is None: 17 | await ctx.send_cmd_help(ctx) 18 | 19 | @checks.admin_or_permissions(administrator=True) 20 | @autorole.command(no_pm=True) 21 | async def info(self, ctx): 22 | """Shows Auto Status.""" 23 | guild = await ctx.get_guild() 24 | bot_role_id = guild.auto_role_bot 25 | role_id = guild.auto_role 26 | desc = '**Status:**\n' 27 | if role_id: 28 | role = discord.utils.get(ctx.guild.roles, id=int(role_id)) 29 | desc += f'➠ Role: **@{role.name}** \n' 30 | else: 31 | desc += '➠ Role: **none** \n' 32 | if bot_role_id: 33 | role = discord.utils.get(ctx.guild.roles, id=int(bot_role_id)) 34 | desc += f'➠ Bot role: **@{role.name}** \n' 35 | else: 36 | desc += '➠ Bot role: **none** \n' 37 | 38 | em = discord.Embed(description=desc, color=discord.Color.blue()) 39 | await ctx.send(embed=em) 40 | 41 | @autorole.command(no_pm=True) 42 | @checks.admin_or_permissions(manage_roles=True) 43 | async def role(self, ctx, *, role: discord.Role): 44 | """Sets the Autorole Role.""" 45 | if role is None: 46 | return await ctx.send("❌ That role cannot be found.") 47 | if not ctx.channel.permissions_for(ctx.guild.me).manage_roles: 48 | return await ctx.send("❌ I don't have manage_roles.") 49 | guild = await ctx.get_guild() 50 | guild.auto_role = str(role.id) 51 | await guild.save() 52 | desc = f'➠ AutoRole set to: **@{role.name}**\n' 53 | em = discord.Embed(description=desc, color=discord.Color.blue()) 54 | await ctx.send(embed=em) 55 | 56 | @autorole.command(no_pm=True) 57 | @checks.admin_or_permissions(manage_roles=True) 58 | async def botrole(self, ctx, *, role: discord.Role): 59 | """Sets the Autorole for Bots.""" 60 | if role is None: 61 | return await ctx.send("❌ That role cannot be found.") 62 | if not ctx.channel.permissions_for(ctx.guild.me).manage_roles: 63 | return await ctx.send("❌ I don't have manage_roles.") 64 | guild = await ctx.get_guild() 65 | guild.auto_role_bot = str(role.id) 66 | await guild.save() 67 | desc = f'➠ AutoRole for Bots set to: **@{role.name}**\n' 68 | em = discord.Embed(description=desc, color=discord.Color.blue()) 69 | await ctx.send(embed=em) 70 | 71 | @checks.admin_or_permissions(administrator=True) 72 | @autorole.command(no_pm=True) 73 | async def disable(self, ctx): 74 | """Disable Autorole""" 75 | guild = await ctx.get_guild() 76 | guild.auto_role = None 77 | await guild.save() 78 | desc = f'➠ Autorole disabled on **{ctx.guild.name}** \n' 79 | em = discord.Embed(description=desc, color=discord.Color.blue()) 80 | await ctx.send(embed=em) 81 | 82 | @checks.admin_or_permissions(administrator=True) 83 | @autorole.command(no_pm=True) 84 | async def disablebot(self, ctx): 85 | """Disable Autorole for Bots""" 86 | guild = await ctx.get_guild() 87 | guild.auto_role_bot = None 88 | await guild.save() 89 | desc = f'➠ Autorole for Bots disabled on **{ctx.guild.name}** \n' 90 | em = discord.Embed(description=desc, color=discord.Color.blue()) 91 | await ctx.send(embed=em) 92 | 93 | 94 | def setup(bot): 95 | bot.add_cog(Autorole(bot)) 96 | -------------------------------------------------------------------------------- /commands/core.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import discord 4 | import rethinkdb as r 5 | from discord.ext import commands as Commands 6 | 7 | from utils.checks import checks 8 | from utils.ctx import CustomContext 9 | from utils.db.models import get_prefix 10 | from utils.formats import pagify 11 | from utils.config.setup_bot import module_loader 12 | 13 | 14 | class Core: 15 | 16 | def __init__(self, bot): 17 | self.bot = bot 18 | 19 | @Commands.group(name='load') 20 | @checks.is_bot_owner() 21 | async def load(self, ctx: Commands.Context): 22 | """module loader commands""" 23 | if ctx.invoked_subcommand is None: 24 | await ctx.send_cmd_help(ctx) 25 | 26 | @load.command(name="handler", aliases=["event", "h", "e"]) 27 | @checks.is_bot_owner() 28 | async def handler(self, ctx: Commands.Context, *, module: str): 29 | """loads a event/handler""" 30 | await module_loader(bot=self.bot, path="handlers", name=module, ctx=ctx) 31 | 32 | @load.command(name="command", aliases=["cog", "com", "c"]) 33 | @checks.is_bot_owner() 34 | async def command(self, ctx: Commands.Context, *, module: str): 35 | """loads a command/cog""" 36 | await module_loader(bot=self.bot, path="commands", name=module, ctx=ctx) 37 | 38 | @load.command(name="all") 39 | @checks.is_bot_owner() 40 | async def all(self, ctx: Commands.Context): 41 | """loads everything,(this will likely error as at a min this command is already loaded)""" 42 | await module_loader(bot=self.bot, path="commands", ctx=ctx) 43 | await module_loader(bot=self.bot, path="handlers", ctx=ctx) 44 | 45 | @Commands.group(name='unload') 46 | @checks.is_bot_owner() 47 | async def unload(self, ctx: Commands.Context): 48 | """module unloader commands""" 49 | if ctx.invoked_subcommand is None: 50 | await ctx.send_cmd_help(ctx) 51 | 52 | @unload.command(name="handler", aliases=["event", "h", "e"]) 53 | @checks.is_bot_owner() 54 | async def _handler(self, ctx, *, module: str): 55 | """unloads a event/handler""" 56 | await module_loader(bot=self.bot, path="handlers", name=module, unload=True, ctx=ctx) 57 | 58 | @unload.command(name="command", aliases=["cog", "com", "c"]) 59 | @checks.is_bot_owner() 60 | async def _command(self, ctx, *, module: str): 61 | """unloads a command/cog""" 62 | await module_loader(bot=self.bot, path="commands", name=module, unload=True, ctx=ctx) 63 | 64 | @unload.command(name="all") 65 | @checks.is_bot_owner() 66 | async def _all(self, ctx: Commands.Context): 67 | """unloads everything,(including this command, rending the bot useless until restart)""" 68 | maybe = await ctx.confirm(ctx, 69 | message="This will unload ***all*** modules " 70 | "***including*** this one!(rending the bot useless until restart)" 71 | " y/n?") 72 | if maybe: 73 | await module_loader(bot=self.bot, path="commands", unload=True, ctx=ctx) 74 | await module_loader(bot=self.bot, path="handlers", unload=True, ctx=ctx) 75 | return 76 | await ctx.send("ok") 77 | 78 | @Commands.group(name='reload') 79 | @checks.is_bot_owner() 80 | async def reload(self, ctx: Commands.Context): 81 | """module reloader commands""" 82 | if ctx.invoked_subcommand is None: 83 | await ctx.send_cmd_help(ctx) 84 | 85 | @reload.command(name="handler", aliases=["event", "h", "e"]) 86 | @checks.is_bot_owner() 87 | async def __handler(self, ctx: Commands.Context, *, module: str): 88 | """reloads a event/handler""" 89 | await module_loader(bot=self.bot, path="handlers", name=module, reload=True, ctx=ctx) 90 | 91 | @reload.command(name="command", aliases=["cog", "com", "c"]) 92 | @checks.is_bot_owner() 93 | async def __command(self, ctx: Commands.Context, *, module: str): 94 | """reloads a command/cog""" 95 | await module_loader(bot=self.bot, path="commands", name=module, reload=True, ctx=ctx) 96 | 97 | @reload.command(name="all") 98 | @checks.is_bot_owner() 99 | async def __all(self, ctx: Commands.Context): 100 | """reloads everything""" 101 | await module_loader(bot=self.bot, path="commands", reload=True, ctx=ctx) 102 | await module_loader(bot=self.bot, path="handlers", reload=True, ctx=ctx) 103 | 104 | @Commands.command(name="debug", aliases=["eval"]) 105 | @checks.is_bot_owner() 106 | async def debug(self, ctx: Commands.Context, *, code: str): 107 | """Evaluates code.""" 108 | code = code.strip('` ') 109 | python = '```py\n{}\n```' 110 | 111 | env = { 112 | 'r': r, 113 | 'bot': self.bot, 114 | 'ctx': ctx, 115 | 'message': ctx.message, 116 | 'guild': ctx.message.guild, 117 | 'channel': ctx.message.channel, 118 | 'author': ctx.message.author 119 | } 120 | 121 | env.update(globals()) 122 | 123 | try: 124 | result = eval(code, env) 125 | if inspect.isawaitable(result): 126 | result = await result 127 | except Exception as e: 128 | await ctx.send(python.format(type(e).__name__ + ': ' + str(e))) 129 | return 130 | if len(str(result)) >= 1900: 131 | pages = pagify(str(result)) 132 | for page in pages: 133 | await ctx.send(f"```{page}```") 134 | return 135 | await ctx.send(python.format(result)) 136 | 137 | async def process_commands(self, message: discord.Message): 138 | ctx = await self.bot.get_context(message, cls=CustomContext) 139 | if ctx.command is None: 140 | return 141 | await self.bot.invoke(ctx) 142 | 143 | @Commands.command() 144 | @checks.is_bot_owner() 145 | async def sudo(self, ctx, user: discord.Member, *, command): 146 | "Runs the [command] as if [user] had run it. DON'T ADD A PREFIX\n " 147 | message = ctx.message 148 | message.author = user 149 | p = await get_prefix(self.bot, message) 150 | message.content = (p[0] + command) 151 | await self.process_commands(message) 152 | 153 | # https://github.com/Rapptz/discord.py/blob/rewrite/discord/ext/commands/bot.py#L95 154 | @Commands.command(name="help", aliases=["h", "halp"]) 155 | async def _help(self, ctx: Commands.Context, *commands: str): 156 | """Help menu""" 157 | destination = ctx.message.channel 158 | if len(commands) == 0: 159 | pages = await self.bot.formatter.format_help_for(ctx, self.bot) 160 | elif len(commands) == 1: 161 | name = commands[0] 162 | if name in self.bot.cogs: 163 | command = self.bot.cogs[name] 164 | else: 165 | command = self.bot.all_commands.get(name) 166 | if command is None: 167 | await destination.send(self.bot.command_not_found.format(name)) 168 | return 169 | 170 | pages = await self.bot.formatter.format_help_for(ctx, command) 171 | else: 172 | name = commands[0] 173 | command = self.bot.all_commands.get(name) 174 | if command is None: 175 | await destination.send(self.bot.command_not_found.format(name)) 176 | return 177 | for key in commands[1:]: 178 | try: 179 | command = command.all_commands.get(key) 180 | if command is None: 181 | await destination.send(self.bot.command_not_found.format(key)) 182 | return 183 | except AttributeError: 184 | await destination.send(self.bot.command_has_no_subcommands.format(command, key)) 185 | return 186 | pages = await self.bot.formatter.format_help_for(ctx, command) 187 | p = 0 188 | links = "Website: **[https://tails.fun](https://tails.fun)**\n" 189 | for embed in pages: 190 | if p == 0: 191 | embed.set_author( 192 | name=f"Help for {self.bot.user.name}", 193 | icon_url=self.bot.user.avatar_url, 194 | url=discord.utils.oauth_url(self.bot.user.id) 195 | ) 196 | if embed.description: 197 | desc = links 198 | desc += embed.description 199 | embed.description = desc 200 | else: 201 | embed.description = links 202 | p += 1 203 | embed.color = await ctx.get_color(self.bot, ctx.author) 204 | embed.set_footer(text=f"page {p}/{len(pages)}") 205 | await destination.send(embed=embed) 206 | 207 | 208 | def setup(bot): 209 | bot.add_cog(Core(bot)) 210 | -------------------------------------------------------------------------------- /commands/invite_tracker.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from utils.checks import checks 5 | 6 | 7 | class InviteMirror: 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @checks.admin_or_permissions(administrator=True) 12 | @commands.group(name='invitemirror', no_pm=True) 13 | async def invite_mirror(self, ctx): 14 | """Shows who joined with which invite""" 15 | if ctx.invoked_subcommand is None: 16 | await ctx.send_cmd_help(ctx) 17 | 18 | @invite_mirror.command(name='here', no_pm=True) 19 | async def channel(self, ctx, channel: discord.TextChannel = None): 20 | """Sets the Modlog Channel.""" 21 | if channel is None: 22 | channel = ctx.channel 23 | guild = ctx.message.guild 24 | g = await ctx.get_guild() 25 | if g.invite_tracker["Channel"]: 26 | await ctx.send( 27 | "Channel already registered, use invitemirror disable then set a new channel to switch channels.") 28 | return 29 | if not ctx.message.guild.me.permissions_in(channel).manage_channels: 30 | await ctx.send("I dont have the manage channels permission.") 31 | return 32 | if ctx.message.guild.me.permissions_in(channel).send_messages: 33 | invlist = await guild.invites() 34 | g.invite_tracker = {"Channel": str(channel.id), "Invites": {}} 35 | for i in invlist: 36 | g.invite_tracker["Invites"][i.url] = i.uses 37 | await g.save() 38 | await ctx.send("I will now send invitemirror notifications here") 39 | else: 40 | return 41 | 42 | @invite_mirror.command(name='disable', pass_context=True, no_pm=True) 43 | async def disable(self, ctx): 44 | """disables the invite mirror""" 45 | g = await ctx.get_guild() 46 | db = g.invite_tracker 47 | if not db: 48 | await ctx.send("settings not found, use invitemirror channel to set a channel.") 49 | return 50 | g.invite_tracker = {"Channel": None, "Invites": None} 51 | await g.save() 52 | await ctx.send("I will no longer send invite tracker notifications here") 53 | 54 | 55 | def setup(bot): 56 | bot.add_cog(InviteMirror(bot)) 57 | -------------------------------------------------------------------------------- /commands/mod.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | import logging 4 | import os 5 | from datetime import timedelta 6 | 7 | import discord 8 | import psutil 9 | from discord.ext import commands 10 | 11 | from utils.checks import checks 12 | from utils.db.models import get_prefix 13 | from utils.ctx import CustomContext 14 | 15 | 16 | class Mod: 17 | def __init__(self, bot): 18 | self.bot = bot 19 | self.process = psutil.Process(os.getpid()) 20 | self.env = {} 21 | self.stdout = io.StringIO() 22 | 23 | @staticmethod 24 | def _role_from_string(guild, rolename, roles=None): 25 | if roles is None: 26 | roles = guild.roles 27 | roles = [r for r in roles if (r is not None)] 28 | role = discord.utils.find((lambda r: (r.name.lower() == rolename.lower())), roles) 29 | return role 30 | 31 | ######################################################### 32 | # snipe things 33 | ######################################################### 34 | # TODO non place holder msgs 35 | @commands.group(name="snipe") 36 | async def _snipe(self, ctx: CustomContext): 37 | if not ctx.subcommand_passed: 38 | g = await ctx.get_guild() 39 | m = g.snipe["data"].get(str(ctx.channel.id), None) 40 | if m: 41 | em = discord.Embed(color=discord.Color.blue()) 42 | user = await self.bot.get_user_info(int(str(m["user"]))) 43 | em.set_author(name=user.name, icon_url=user.avatar_url) 44 | em.add_field(name="Sniped message", value=m["msg"], inline=False) 45 | em.set_footer(text=f"Sniped by {ctx.author}", icon_url=ctx.author.avatar_url) 46 | 47 | await ctx.send(embed=em) 48 | else: 49 | await ctx.send("No snipes") 50 | 51 | @_snipe.command(name="clear") 52 | async def _clear(self, ctx: CustomContext): 53 | g = await ctx.get_guild() 54 | m = g.snipe["enabled"] 55 | if m: 56 | g.snipe["data"].clear() 57 | await g.save() 58 | await ctx.send("done") 59 | else: 60 | await ctx.send("not on") 61 | 62 | @_snipe.command(name="enable") 63 | async def _enable(self, ctx: CustomContext): 64 | g = await ctx.get_guild() 65 | m = g.snipe["enabled"] 66 | if m: 67 | await ctx.send("on") 68 | else: 69 | g.snipe["enabled"] = True 70 | await g.save() 71 | await ctx.send("done") 72 | 73 | @_snipe.command(name="disable") 74 | async def _disable(self, ctx: CustomContext): 75 | g = await ctx.get_guild() 76 | m = g.snipe["enabled"] 77 | if not m: 78 | await ctx.send("off") 79 | else: 80 | g.snipe["enabled"] = None 81 | g.snipe["data"].clear() 82 | await g.save() 83 | await ctx.send("done") 84 | 85 | ######################################################### 86 | # mass add things 87 | ######################################################### 88 | 89 | # noinspection PyBroadException 90 | @commands.command(pass_context=True) 91 | @checks.admin_or_permissions(manage_guild=True) 92 | async def massadd(self, ctx: CustomContext, *, rolename): 93 | """Adds all members to a role""" 94 | guild = ctx.message.guild 95 | role = self._role_from_string(guild, rolename) 96 | # YAY counter 97 | nc = 0 98 | if role is None: 99 | await ctx.send("Role not found") 100 | return 101 | await ctx.send("Adding everyone to role '{}', please wait!".format(role.name)) 102 | # we copy the list over so it doesn't change while we're iterating over it 103 | members = list(guild.members) 104 | for member in members: 105 | try: 106 | await member.add_roles(role, reason=f"Lets party! massadd command ran by {ctx.message.author}") 107 | nc += 1 108 | except Exception: 109 | continue 110 | await ctx.send("Finished Adding Roles to {} members!".format(nc)) 111 | 112 | ######################################################### 113 | # slow delete 114 | ######################################################### 115 | 116 | @checks.admin_or_permissions(Manage_Messages=True) 117 | @commands.command(aliases=['slowdelete']) 118 | async def sd(self, ctx: CustomContext, limit: int): 119 | """deletes messages the slow why yes even over 14 days""" 120 | channel = ctx.channel 121 | if limit > 5000: 122 | limit = 5000 123 | async for msg in channel.history(limit=limit): 124 | await msg.delete() 125 | 126 | 127 | ######################################################### 128 | # kick/bans 129 | ######################################################### 130 | 131 | 132 | @commands.command(no_pm=True) 133 | @checks.admin_or_permissions(kick_members=True) 134 | async def kick(self, ctx: CustomContext, user: discord.Member, *, reason=None): 135 | """Kicks user.""" 136 | 137 | if ctx.author == user: 138 | await ctx.send("lol no") 139 | return 140 | try: 141 | if reason: 142 | await user.kick(reason=f"{reason}, Command ran by {ctx.author}") 143 | await ctx.send('bye 👋🏼') 144 | else: 145 | await user.kick(reason=f"{ctx.author} ran this with no reason provided") 146 | await ctx.send('bye 👋🏼') 147 | 148 | except discord.errors.Forbidden: 149 | await ctx.send('❌ Permissions error of some kind.') 150 | except Exception as e: 151 | ctx.bot.log.error("wot", e) 152 | await ctx.send('❌ error of some kind.') 153 | 154 | @commands.command(no_pm=True) 155 | @checks.admin_or_permissions(ban_members=True) 156 | async def ban(self, ctx: CustomContext, user: discord.Member, days: str = None, *, reason=None): 157 | """Bans user and deletes last X days worth of messages.""" 158 | if ctx.author == user: 159 | return await ctx.send("lol no") 160 | if days: 161 | if days.isdigit(): 162 | days = int(days) 163 | else: 164 | days = 0 165 | else: 166 | days = 0 167 | if days > 7 or days < 0: 168 | days = 7 169 | else: 170 | days = days 171 | if reason: 172 | reason = f"{reason}, Command ran by {ctx.author}" 173 | else: 174 | reason = f"Command ran by {ctx.author}, with no reason provided" 175 | try: 176 | await ctx.guild.ban(user=user, 177 | reason=reason, 178 | delete_message_days=days) 179 | await ctx.send('bye 👋🏼 next time just stfu') 180 | except discord.errors.Forbidden: 181 | (await ctx.send('❌ Permissions error of some kind.')) 182 | except Exception as e: 183 | ctx.bot.log.error("? ", e) 184 | 185 | @commands.command(no_pm=True) 186 | @checks.admin_or_permissions(ban_members=True) 187 | async def softban(self, ctx: CustomContext, user: discord.Member): 188 | """Kicks the user, deleting 3 day worth of messages.""" 189 | guild = ctx.guild 190 | channel = ctx.channel 191 | can_ban = channel.permissions_for(guild.me).ban_members 192 | author = ctx.author 193 | if (author == user): 194 | (await ctx.send( 195 | '😂 LOL as much as i would love to see you ban yourself i cant let you do that.\n You should try the leave server button <:ls:380047736877088788> or `dfban` yourself.')) 196 | return 197 | try: 198 | invite = (await guild.channels[0].create_invite()) 199 | except: 200 | invite = '' 201 | if can_ban: 202 | try: 203 | try: 204 | (await user.send( 205 | 'You have been softbanned as a quick way to delete your messages.\nYou can now join the server again.{}'.format( 206 | invite))) 207 | except: 208 | pass 209 | (await ctx.guild.ban(user=user, reason="soft ban", delete_message_days=3)) 210 | (await guild.unban(user)) 211 | (await ctx.send('bye 👋🏼 see you in a sec.')) 212 | except discord.errors.Forbidden: 213 | (await ctx.send('❌ Permissions error of some kind.')) 214 | except Exception as e: 215 | print(e) 216 | else: 217 | (await ctx.send('❌ Permissions error of some kind.')) 218 | 219 | @commands.command(no_pm=True) 220 | @checks.admin_or_permissions(manage_nicknames=True) 221 | async def setnick(self, ctx: CustomContext, user: discord.Member, *, nickname=''): 222 | 'Changes a members nick leaving empty will remove it.' 223 | nickname = nickname.strip() 224 | cl = 'changed' 225 | if (nickname == ''): 226 | nickname = None 227 | cl = 'cleared' 228 | try: 229 | (await user.edit(nick=nickname, reason="command ran by {}".format(ctx.message.author))) 230 | (await ctx.send("I have {} {}'s nickname 👌🏼".format(cl, user.name))) 231 | except discord.Forbidden: 232 | (await ctx.send('❌ Permissions error of some kind.')) 233 | 234 | @commands.command(no_pm=True) 235 | @checks.mod_or_permissions(manage_messages=True) 236 | async def warn(self, ctx: CustomContext, user: discord.Member, *, reason: str = None): 237 | 'Warns a Member.' 238 | msg = [('%s, ' % user.mention)] 239 | msg.append("you're doing something that might get you banned 🔨 if you keep doing it.") 240 | if reason: 241 | msg.append((' Specifically, %s.' % reason)) 242 | msg.append('Be sure to **__read__** the dam rules huh?') 243 | try: 244 | (await user.send(' '.join(msg))) 245 | except: 246 | pass 247 | (await ctx.send(' '.join(msg))) 248 | 249 | @commands.command(aliases=['ar'], no_pm=True) 250 | @checks.admin_or_permissions(manage_roles=True) 251 | async def addrole(self, ctx: CustomContext, rolename, user: discord.Member = None): 252 | 'Adds a role to a user, defaults to author\n\n Role name must be in quotes if there are spaces.' 253 | author = ctx.author 254 | channel = ctx.channel 255 | guild = ctx.guild 256 | if (user is None): 257 | user = author 258 | role = self._role_from_string(guild, rolename) 259 | if (role is None): 260 | (await ctx.send('❌ That role cannot be found.')) 261 | return 262 | if (not channel.permissions_for(guild.me).manage_roles): 263 | (await ctx.send("❌ I don't have manage_roles.")) 264 | return 265 | (await user.add_roles(role, reason="{} ran this command!".format(ctx.message.author))) 266 | (await ctx.send('Added role {} to {} 👌🏼'.format(role.name, user.name))) 267 | 268 | @commands.command(aliases=['rr'], no_pm=True) 269 | @checks.admin_or_permissions(manage_roles=True) 270 | async def removerole(self, ctx: CustomContext, rolename, user: discord.Member = None): 271 | 'Removes a role from a user,\n Role name must be in quotes if there are spaces.' 272 | guild = ctx.guild 273 | author = ctx.author 274 | role = self._role_from_string(guild, rolename) 275 | if (role is None): 276 | (await ctx.send('❌ Role not found.')) 277 | return 278 | if (user is None): 279 | user = author 280 | if (role in user.roles): 281 | try: 282 | (await user.remove_roles(role)) 283 | (await ctx.send('Role successfully removed.👌🏼')) 284 | except discord.Forbidden: 285 | (await ctx.send("❌ I don't have permissions to manage roles!")) 286 | else: 287 | (await ctx.send('❌ User does not have that role.')) 288 | 289 | @commands.group(aliases=['er'], no_pm=True) 290 | @checks.admin_or_permissions(manage_roles=True) 291 | async def editrole(self, ctx: CustomContext): 292 | 'Edits roles' 293 | if (ctx.invoked_subcommand is None): 294 | (await ctx.send_cmd_help(ctx)) 295 | 296 | @editrole.command(aliases=['colour', 'c']) 297 | async def color(self, ctx: CustomContext, role: discord.Role, value: discord.Colour): 298 | "Edits a role's color" 299 | try: 300 | (await role.edit(color=value, reason="{} ran this command!".format(ctx.message.author))) 301 | (await ctx.send('Done.👌🏼')) 302 | except discord.Forbidden: 303 | (await ctx.send('❌ I need permissions to manage roles first.')) 304 | except Exception as e: 305 | print(e) 306 | (await ctx.send('❌ Something went wrong.')) 307 | 308 | @editrole.command(aliases=['n'], name='name') 309 | @checks.admin_or_permissions(administrator=True) 310 | async def edit_role_name(self, ctx: CustomContext, role: discord.Role, name: str): 311 | "Edits a role's name " 312 | if (name == ''): 313 | (await ctx.send('❌ Name cannot be empty.')) 314 | return 315 | try: 316 | (await role.edit(name=name, reason="{} ran this command!".format(ctx.message.author))) 317 | (await ctx.send('Done.👌🏼')) 318 | except discord.Forbidden: 319 | (await ctx.send('❌ I need permissions to manage roles first.')) 320 | except Exception as e: 321 | print(e) 322 | (await ctx.send('❌ Something went wrong.')) 323 | 324 | # @commands.cooldown(1, 15000, type=commands.BucketType.guild) 325 | @commands.command(aliases=['mn'], no_pm=True) 326 | @checks.admin_or_permissions(manage_roles=True) 327 | async def massnick(self, ctx: CustomContext, rolename, *, nickname): 328 | 'changes everyones nickname in a role' 329 | guild = ctx.guild 330 | counter = 0 331 | nc = 0 332 | (await ctx.send('This may take a min ⏲️')) 333 | if discord.utils.get(guild.roles, name=rolename): 334 | member_list = [members for members in guild.members if 335 | (discord.utils.get(guild.roles, name=rolename) in members.roles)] 336 | for user in member_list: 337 | try: 338 | (await user.edit(nick=nickname, reason="Changing nicks, requested by {}" 339 | .format(ctx.message.author))) 340 | nc += 1 341 | (await asyncio.sleep(1.5)) 342 | except discord.Forbidden: 343 | counter += 1 344 | except discord.HTTPException: 345 | print('lol nickname rate limits') 346 | (await ctx.send('lol nickname rate limits')) 347 | (await ctx.send('Finished 👌🏼 There were {} nicks set and {} errors❌'.format(nc, counter))) 348 | else: 349 | (await ctx.send( 350 | ('❌ ERROR: Could not find rolename `%s`, please make sure you typed it case accurate' % rolename))) 351 | 352 | @massnick.error 353 | async def mn_error(self, ctx: CustomContext, error): 354 | if type(error) is commands.CommandOnCooldown: 355 | fmt = (str(error)).split() 356 | word = fmt[7].strip("s") 357 | time = float(word) 358 | timer = round(time, 0) 359 | tdelta = str(timedelta(seconds=int(timer))).lstrip("0").lstrip(":") 360 | await ctx.send("❌ lol nickname rate limits You can try again in `{}`".format(tdelta)) 361 | 362 | @commands.cooldown(1, 14400, type=commands.BucketType.guild) 363 | @commands.command(aliases=['mnc'], no_pm=True) 364 | @checks.admin_or_permissions(manage_roles=True) 365 | async def massnickc(self, ctx: CustomContext, rolename): 366 | 'clears everyones nickname in a role' 367 | guild = ctx.guild 368 | counter = 0 369 | nc = 0 370 | (await ctx.send('This may take a min ⏲️')) 371 | if discord.utils.get(guild.roles, name=rolename): 372 | member_list = [members for members in guild.members if 373 | (discord.utils.get(guild.roles, name=rolename) in members.roles)] 374 | for user in member_list: 375 | try: 376 | (await user.edit(nick='', reason="rip nicks. Ran by {}".format(ctx.message.author))) 377 | nc += 1 378 | (await asyncio.sleep(1.5)) 379 | except discord.Forbidden: 380 | counter += 1 381 | except discord.HTTPException: 382 | print('lol nickname rate limits') 383 | (await ctx.send('❌ lol nickname rate limits')) 384 | (await ctx.send('Finished 👌🏼 There were {} nicks cleared and {} errors❌'.format(nc, counter))) 385 | else: 386 | (await ctx.send( 387 | ('❌ ERROR: Could not find rolename `%s`, please make sure you typed it case accurate' % rolename))) 388 | 389 | @massnickc.error 390 | async def mnc_error(self, ctx: CustomContext, error): 391 | if type(error) is commands.CommandOnCooldown: 392 | fmt = (str(error)).split() 393 | word = fmt[7].strip("s") 394 | time = float(word) 395 | timer = round(time, 0) 396 | tdelta = str(timedelta(seconds=int(timer))).lstrip("0").lstrip(":") 397 | await ctx.send("❌ lol nickname rate limits You can try again in `{}`".format(tdelta)) 398 | 399 | @checks.admin_or_permissions(manage_channels=True) 400 | @commands.command(aliases=["sm"]) 401 | async def slowmode(self, ctx: CustomContext, rate: int, channel: discord.TextChannel = None): 402 | """Activate slow mode for channel""" 403 | if channel is None: 404 | channel = ctx.channel 405 | headers = dict(Authorization=f"Bot {self.bot.http.token}") 406 | payload = {"rate_limit_per_user": rate} 407 | if 1 <= int(rate) <= 120: 408 | r = await self.bot.session.patch(f"https://discordapp.com/api/v7/channels/{channel.id}", 409 | headers=headers, 410 | json=payload) 411 | res = await r.json 412 | await ctx.send( 413 | content=f"slow mode set to {res['rate_limit_per_user']} seconds per user", 414 | delete_after=2) 415 | else: 416 | if int(rate) == 0: 417 | r = await self.bot.session.patch(f"https://discordapp.com/api/v7/channels/{channel.id}", 418 | headers=headers, json=payload) 419 | await r.json() 420 | await ctx.send(content="Slow-mode disabled", delete_after=2) 421 | else: 422 | await ctx.send(content="You have entered an invalid rate. Please select aa number between 1 and 120.", 423 | delete_after=2) 424 | 425 | 426 | def setup(bot): 427 | bot.add_cog(Mod(bot)) 428 | -------------------------------------------------------------------------------- /commands/modlog.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from utils.checks import checks 5 | from utils.db.models import get_guild, GuildEntry 6 | 7 | 8 | class ModLog: 9 | 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @checks.admin_or_permissions(manage_roles=True) 14 | @commands.group(no_pm=True) 15 | async def modlog(self, ctx): 16 | """Modlog Settings""" 17 | if ctx.invoked_subcommand is None: 18 | await ctx.send_cmd_help(ctx) 19 | 20 | @checks.admin_or_permissions(administrator=True) 21 | @modlog.group(no_pm=True) 22 | async def channel(self, ctx, channel: discord.TextChannel = None): 23 | """Sets the Modlog Channel.""" 24 | if channel is None: 25 | channel = ctx.channel 26 | g: GuildEntry = await ctx.get_guild() 27 | g.modlog_channel = str(channel.id) 28 | await g.save() 29 | desc = f'➠ Modlog Channel set to: **#{channel.name}**\n' 30 | em = discord.Embed(description=desc, color=discord.Color.blue()) 31 | await ctx.send(embed=em) 32 | 33 | @checks.admin_or_permissions(administrator=True) 34 | @modlog.command(no_pm=True) 35 | async def off(self, ctx): 36 | """Disables Modlog""" 37 | g: GuildEntry = await ctx.get_guild() 38 | g.modlog_channel = None 39 | await g.save() 40 | desc = f'➠ Modlog disabled on **{ctx.guild.name}** \n' 41 | em = discord.Embed(description=desc, color=discord.Color.blue()) 42 | await ctx.send(embed=em) 43 | 44 | @checks.admin_or_permissions(administrator=True) 45 | @modlog.command(no_pm=True) 46 | async def status(self, ctx): 47 | """Shows the Modlog Status""" 48 | g: GuildEntry = await ctx.get_guild() 49 | modlog_channel = g.modlog_channel 50 | await g.save() 51 | if modlog_channel: 52 | channel = self.bot.get_channel(int(modlog_channel)) 53 | desc = f'➠ Modlog enabled on **#{channel.name}**' 54 | em = discord.Embed(description=desc, color=discord.Color.blue()) 55 | await ctx.send(embed=em) 56 | else: 57 | await ctx.send(f'Modlog disabled on **{ctx.guild.name}**') 58 | 59 | 60 | def setup(bot): 61 | bot.add_cog(ModLog(bot)) 62 | -------------------------------------------------------------------------------- /commands/test.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | from utils.checks import checks 4 | 5 | 6 | class Test: 7 | def __init__(self, bot): 8 | self.bot = bot 9 | 10 | @commands.cooldown(1, 15000, type=commands.BucketType.guild) 11 | @commands.command(hidden=True) 12 | @checks.is_bot_owner() 13 | async def test(self, ctx): 14 | self.thisshoulderror 15 | 16 | 17 | def setup(bot): 18 | bot.add_cog(Test(bot)) 19 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "owners": [ 3 | "248294452307689473" 4 | ], 5 | "home": "440526421388165120", 6 | "db": "yes_daddy", 7 | "tables": [ 8 | "guilds", 9 | "users" 10 | ], 11 | "prefixes": [ 12 | "yd", 13 | "!", 14 | "!!" 15 | ], 16 | "dm_prefix": [ 17 | "!", 18 | "yd" 19 | ], 20 | "debug_prefix": "!" 21 | } -------------------------------------------------------------------------------- /handlers/ErrorHandler.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from datetime import timedelta 3 | 4 | import discord 5 | from discord import Webhook, AsyncWebhookAdapter 6 | from discord.ext import commands 7 | import traceback 8 | from utils.formats import pagify 9 | 10 | 11 | class Error: 12 | def __init__(self, bot): 13 | self.bot = bot 14 | 15 | @staticmethod 16 | async def on_command_error(ctx, error): 17 | if isinstance(error, commands.MissingRequiredArgument): 18 | ctx.command.reset_cooldown(ctx) 19 | await ctx.send_cmd_help(ctx) 20 | elif isinstance(error, commands.BadArgument): 21 | ctx.command.reset_cooldown(ctx) 22 | await ctx.send_cmd_help(ctx) 23 | elif isinstance(error, commands.DisabledCommand): 24 | pass 25 | elif isinstance(error, commands.CommandOnCooldown): 26 | fmt = (str(error)).split() 27 | word = fmt[7].strip("s") 28 | time = float(word) 29 | timer = round(time, 0) 30 | delta = str(timedelta(seconds=int(timer))).lstrip("0").lstrip(":") 31 | await ctx.send(f"❌ lol, rate limits You can try again in `{delta}`") 32 | elif isinstance(error, commands.CommandNotFound): 33 | pass 34 | elif isinstance(error, commands.CheckFailure): 35 | ctx.command.reset_cooldown(ctx) 36 | pass 37 | elif isinstance(error, commands.BotMissingPermissions): 38 | ctx.command.reset_cooldown(ctx) 39 | await ctx.send(error.missing_perms) 40 | elif isinstance(error, commands.CheckFailure): 41 | ctx.command.reset_cooldown(ctx) 42 | pass 43 | elif isinstance(error, commands.NoPrivateMessage): 44 | ctx.command.reset_cooldown(ctx) 45 | await ctx.send("Nope that command is not available in DMs.") 46 | 47 | else: 48 | if ctx.bot.debug: 49 | ctx.bot.log.error(f"shit: {repr(error)}", exc_info=True) 50 | else: 51 | ctx.bot.log.error(f"shit: {repr(error)}", exc_info=False) 52 | ctx.command.reset_cooldown(ctx) 53 | webhook = Webhook.from_url(ctx.bot.keys['error_wh'], adapter=AsyncWebhookAdapter(ctx.bot.session)) 54 | await webhook.edit(name="Error") 55 | channel = ctx.message.channel 56 | t = datetime.datetime.now() 57 | avatar = ctx.bot.user.avatar_url 58 | fmt = '[ %I:%M:%S ] %B/%d/%Y' 59 | long = "".join(traceback.format_exception(type(error), error, error.__traceback__)) 60 | error_title = "Error" 61 | desc = '`{}: {}`'.format(type(error.original).__name__, str(error.original)) 62 | em = discord.Embed(color=0xFF0000, description=desc) 63 | em.set_author(name=error_title, icon_url=avatar) 64 | em.set_footer(text=t.strftime(fmt)) 65 | em.add_field(name="Content", value=ctx.message.content) 66 | em.add_field(name="Invoker", value="{}\n({})".format(ctx.message.author.mention, str(ctx.message.author))) 67 | c = "Private channel" if isinstance(ctx.message.channel, discord.abc.PrivateChannel) else \ 68 | f"{channel.mention}\n({channel.name})" 69 | em.add_field(name="Channel", value=c) 70 | if not isinstance(ctx.message.channel, discord.abc.PrivateChannel): 71 | em.add_field(name="Guild", value=ctx.message.guild.name) 72 | await webhook.send(embed=em) 73 | print(long) 74 | pages = pagify(str(long)) 75 | if pages: 76 | for page in pages: 77 | await webhook.send(f"```py\n{page}\n```") 78 | er = f"Error in command '**__{ctx.command.qualified_name}__**' - `{type(error.original).__name__}`: `" \ 79 | f"{str(error.original)}` " 80 | e = discord.Embed(title="**Error** :x:", description="Error in command", color=0xFFC0CB) 81 | e.add_field(name="traceback", value=er) 82 | await channel.send(embed=e) 83 | 84 | 85 | def setup(bot): 86 | bot.add_cog(Error(bot)) 87 | -------------------------------------------------------------------------------- /handlers/GuildHandler.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import discord 4 | from discord import Webhook, AsyncWebhookAdapter 5 | 6 | from utils.db.models import check_and_create_guild 7 | 8 | 9 | class GuildLog: 10 | def __init__(self, bot): 11 | self.bot = bot 12 | self.once = False 13 | 14 | async def on_guild_join(self, guild): 15 | await check_and_create_guild(self.bot, guild) 16 | guilds = str(len(self.bot.guilds)) 17 | users = str(len(set(self.bot.get_all_members()))) 18 | for x in range(self.bot.shard_count): 19 | message = 'ydhelp | shard {}/{} | {} guilds | {} users'.format(x + 1, self.bot.shard_count, guilds, users) 20 | game = discord.Game(name=message, type=2) 21 | self.bot.log.info("joined: {} and set status as {} on shard {}".format(guild.name, message, x+1)) 22 | await self.bot.change_presence(activity=game, shard_id=x) 23 | webhook = Webhook.from_url(self.bot.keys['guild_join_wh'], adapter=AsyncWebhookAdapter(self.bot.session)) 24 | time = datetime.datetime.now() 25 | em = discord.Embed(title="New guild Added", color=discord.Color.green()) 26 | em.set_author(name=guild.name, icon_url=guild.icon_url) 27 | em.set_thumbnail(url=guild.icon_url) 28 | em.add_field(name="Total users", value=str(len(guild.members))) 29 | em.add_field(name="guild Owner", value=str(guild.owner)) 30 | em.add_field(name="Total guilds", value=str(len(self.bot.guilds))) 31 | em.set_footer(text="guild ID: " + str(guild.id)) 32 | em.timestamp = time 33 | await webhook.send(username=guild.name, avatar_url=guild.icon_url if guild.icon else self.bot.user.avatar_url, 34 | embed=em) 35 | 36 | def setup(bot): 37 | bot.add_cog(GuildLog(bot)) 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /handlers/MessageHandler.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from utils.ctx import CustomContext 5 | 6 | 7 | # :( 8 | async def on_message(message): 9 | pass 10 | 11 | 12 | class MessageHandler: 13 | def __init__(self, bot): 14 | self.bot = bot 15 | 16 | async def on_command(self, ctx: commands.Context): 17 | cmd = ctx.command.qualified_name.replace(' ', '_') 18 | self.bot.commands_used[cmd] += 1 19 | message = ctx.message 20 | if isinstance(ctx.message.channel, discord.abc.PrivateChannel): 21 | destination = 'Private Message' 22 | else: 23 | destination = f'#{message.channel.name} ({message.guild.name})' 24 | 25 | self.bot.log.info(f'{message.created_at}: {message.author.name} in {destination}: {message.content}') 26 | 27 | async def process_commands(self, message: discord.Message): 28 | ctx = await self.bot.get_context(message, cls=CustomContext) 29 | if ctx.command is None: 30 | return 31 | await self.bot.invoke(ctx) 32 | 33 | async def on_message(self, message: discord.Message): 34 | self.bot.messages_sent += 1 35 | if message.mention_everyone: 36 | self.bot.at_everyone_seen += 1 37 | if message.author.bot: 38 | return 39 | await self.process_commands(message) 40 | 41 | 42 | def setup(bot): 43 | bot.event(on_message) 44 | bot.add_cog(MessageHandler(bot)) 45 | -------------------------------------------------------------------------------- /handlers/ModlogHandler.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | 4 | import discord 5 | 6 | from utils.config.config import set_home 7 | from utils.db import models 8 | from utils.db.models import get_guild, GuildEntry 9 | 10 | fmt = '%H:%M:%S' 11 | 12 | 13 | class ModlogHandler: 14 | def __init__(self, bot): 15 | self.bot = bot 16 | 17 | async def on_message_delete(self, message): 18 | guild = message.guild 19 | db: GuildEntry = await models.get_guild(self.bot, guild) 20 | if db.snipe["enabled"] and not message.author.bot: 21 | if len(message.content) > 0: 22 | db.snipe["data"][str(message.channel.id)] = {"msg": message.content, "user": str(message.author.id)} 23 | await db.save() 24 | 25 | modlog_channel = db.modlog_channel 26 | if modlog_channel: 27 | channel = self.bot.get_channel(int(modlog_channel)) 28 | time = datetime.datetime.now() 29 | cleanmsg = message.clean_content 30 | msg = \ 31 | f":pencil: `{time.strftime(fmt)}` **Channel** {message.channel.mention}(`{message.channel.id}`) " \ 32 | f"**{message.author}(`{message.author.id}`)'s** message(`{message.id}`) has been deleted. Content: " \ 33 | f"{cleanmsg} " 34 | await channel.send(msg) 35 | 36 | async def on_member_join(self, member): 37 | guild = member.guild 38 | db: GuildEntry = await models.get_guild(self.bot, guild) 39 | modlog_channel = db.modlog_channel 40 | if modlog_channel: 41 | channel = self.bot.get_channel(int(modlog_channel)) 42 | time = datetime.datetime.now() 43 | users = len([e.name for e in guild.members]) 44 | msg = f':white_check_mark: `{time.strftime(fmt)}` User **{member.name}** with id: `{member.id}` has ' \ 45 | f'joined the guild. Total users: `{users}`. ' 46 | await channel.send(msg) 47 | 48 | if member.bot: 49 | autoroleb = db.auto_role_bot 50 | if autoroleb: 51 | roles = guild.roles 52 | role = discord.utils.get(roles, id=int(autoroleb)) 53 | await member.add_roles(role) 54 | else: 55 | autorole = db.auto_role 56 | if autorole: 57 | roles = guild.roles 58 | role = discord.utils.get(roles, id=int(autorole)) 59 | try: 60 | await member.add_roles(role) 61 | except discord.errors.NotFound or discord.errors.Forbidden as e: 62 | self.bot.log.error(f"shit: {repr(e)}", exc_info=False) 63 | 64 | await asyncio.sleep(1) 65 | g = await get_guild(self.bot, member.guild) 66 | if not g.invite_tracker["Channel"]: 67 | return 68 | db = g.invite_tracker 69 | channel = guild.get_channel(int(str(db["Channel"]))) 70 | if not channel: 71 | g.invite_tracker = {"Channel": None, "Invites": {}} 72 | return await g.save() 73 | db_list = db["Invites"] 74 | inv_list = await guild.invites() 75 | for a in inv_list: 76 | try: 77 | if int(a.uses) > int(db_list[a.url]): 78 | await channel.send( 79 | f"{member.name} | {member.mention} (`{member.id}`)\nJoined using invite: `{a.url}`\nThe invite " 80 | f"was used {a.uses} times\nMade by {a.inviter} (`{a.inviter.id}`)") 81 | break 82 | except KeyError: 83 | await channel.send( 84 | f"{member.name} | {member.mention} (`{member.id}`)\nJoined using invite: `{a.url}`\nThe invite " 85 | f"was used {a.uses} times\nMade by {a.inviter} (`{a.inviter.id}`)") 86 | break 87 | else: 88 | pass 89 | for inv in inv_list: 90 | db["Invites"][inv.url] = inv.uses 91 | db["id"] = str(guild.id) 92 | await g.save() 93 | 94 | async def on_member_remove(self, member): 95 | guild = member.guild 96 | db: GuildEntry = await models.get_guild(self.bot, guild) 97 | modlog_channel = db.modlog_channel 98 | if modlog_channel: 99 | channel = self.bot.get_channel(int(modlog_channel)) 100 | time = datetime.datetime.now() 101 | users = len([e.name for e in guild.members]) 102 | msg = f"<:ls:380047736877088788> `{time.strftime(fmt)}` User **{member.name}** with id: `{member.id}` has " \ 103 | f"left the guild or was kicked. Total members `{users}`. " 104 | await channel.send(msg) 105 | 106 | async def on_message_edit(self, before, after): 107 | if before.author.bot: 108 | return 109 | 110 | guild = before.guild 111 | db: GuildEntry = await models.get_guild(self.bot, guild) 112 | modlog_channel = db.modlog_channel 113 | if modlog_channel: 114 | channel = self.bot.get_channel(int(modlog_channel)) 115 | if before.content == after.content: 116 | return 117 | 118 | cleanbefore = before.clean_content 119 | cleanafter = after.clean_content 120 | time = datetime.datetime.now() 121 | 122 | msg = f":pencil: `{time.strftime(fmt)}` **Channel**: {before.channel.mention}(`{before.channel.id}`) **" \ 123 | f"{before.author}(`{before.author.id}`)'s** message(`{before.id}`) has been edited.\nBefore: " \ 124 | f"{cleanbefore}\nAfter: {cleanafter} " 125 | await channel.send(msg) 126 | 127 | async def on_guild_update(self, before, after): 128 | if before == self.bot.home: 129 | set_home(self.bot) 130 | guild = before 131 | db: GuildEntry = await models.get_guild(self.bot, guild) 132 | modlog_channel = db.modlog_channel 133 | if modlog_channel: 134 | channel = self.bot.get_channel(int(modlog_channel)) 135 | time = datetime.datetime.now() 136 | if before.name != after.name: 137 | msg = f':globe_with_meridians: `{time.strftime(fmt)}` guild name update. Before: **{before.name}** ' \ 138 | f'After: **{after.name}**. ' 139 | await channel.send(msg) 140 | if before.region != after.region: 141 | msg = f':globe_with_meridians: `{time.strftime(fmt)}` guild region update. Before: **' \ 142 | f'{before.region}** After: **{after.region}**. ' 143 | await channel.send(msg) 144 | if before.icon != after.icon: 145 | msg = f':globe_with_meridians: `{time.strftime(fmt)}` guild icon update. Before: **' \ 146 | f'{before.icon_url}** After: **{after.icon_url}**. ' 147 | await channel.send(msg) 148 | if before.emojis != after.emojis: 149 | for i in before.emojis: 150 | emojis = str(i) 151 | msg = f':globe_with_meridians: `{time.strftime(fmt)}` Emoji update. Before: **{emojis}**.' 152 | await channel.send(msg) 153 | for i in after.emojis: 154 | emojis = str(i) 155 | msg = f':globe_with_meridians: `{time.strftime(fmt)}` Emoji update. Before: **{emojis}**.' 156 | await channel.send(msg) 157 | 158 | async def on_member_update(self, before, after): 159 | guild = before.guild 160 | db: GuildEntry = await models.get_guild(self.bot, guild) 161 | modlog_channel = db.modlog_channel 162 | if modlog_channel: 163 | channel = self.bot.get_channel(int(modlog_channel)) 164 | time = datetime.datetime.now() 165 | if not before.nick == after.nick: 166 | await channel.send( 167 | f':person_with_pouting_face::skin-tone-3: `{time.strftime(fmt)}` **{before.name}** changed their ' 168 | f'nickname from **`{before.nick}`** to **`{after.nick}`**') 169 | if not before.name == after.name: 170 | await channel.send( 171 | f':person_with_pouting_face::skin-tone-3: `{time.strftime(fmt)}` **{before.name}** changed their ' 172 | f'name from **`{before.name}`** to **`{after.name}`**') 173 | if not before.roles == after.roles: 174 | msg = f":person_with_pouting_face::skin-tone-3: `{time.strftime(fmt)}` **{before.name}'s** roles have" \ 175 | f" changed. Old: `{', '.join([r.name for r in before.roles])}` New: " \ 176 | f"`{', '.join([r.name for r in after.roles])}` " 177 | await channel.send(msg) 178 | if not before.bot: 179 | if not before.avatar == after.avatar: 180 | await channel.send( 181 | f':person_with_pouting_face::skin-tone-3: `{time.strftime(fmt)}` **{before.name}** changed ' 182 | f'their avatar from **{before.avatar_url}** to **{after.avatar_url}**') 183 | 184 | async def on_member_ban(self, guild, member): 185 | db: GuildEntry = await models.get_guild(self.bot, guild) 186 | modlog_channel = db.modlog_channel 187 | if modlog_channel: 188 | channel = self.bot.get_channel(int(modlog_channel)) 189 | time = datetime.datetime.now() 190 | msg = f':hammer: `{time.strftime(fmt)}` {member}({member.id}) has been banned!' 191 | await channel.send(msg) 192 | 193 | async def on_member_unban(self, guild, member): 194 | db: GuildEntry = await models.get_guild(self.bot, guild) 195 | modlog_channel = db.modlog_channel 196 | if modlog_channel: 197 | channel = self.bot.get_channel(int(modlog_channel)) 198 | time = datetime.datetime.now() 199 | msg = f'🕊 `{time.strftime(fmt)}` {member}({member.id}) has been Unbanned!' 200 | await channel.send(msg) 201 | 202 | 203 | def setup(bot): 204 | bot.add_cog(ModlogHandler(bot)) 205 | -------------------------------------------------------------------------------- /handlers/ReadyHandler.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import discord 4 | from discord import Webhook, AsyncWebhookAdapter 5 | 6 | from utils.config.config import set_home 7 | from utils.db.models import check_and_create_guild 8 | from utils import formats 9 | 10 | 11 | class Ready: 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self.once = False 15 | 16 | async def on_connect(self): 17 | self.bot.gateway_server_name = self.bot.shards[0].ws._trace[0] 18 | self.bot.session_server_name = self.bot.shards[0].ws._trace[1] 19 | 20 | async def on_resumed(self): 21 | print(dir(self)) 22 | self.bot.gateway_server_name = self.bot.shards[0].ws._trace[0] 23 | self.bot.session_server_name = self.bot.shards[0].ws._trace[1] 24 | 25 | async def on_ready(self): 26 | set_home(self.bot) 27 | self.bot.gateway_server_name = self.bot.shards[0].ws._trace[0] 28 | self.bot.session_server_name = self.bot.shards[0].ws._trace[1] 29 | for x in self.bot.guilds: 30 | await check_and_create_guild(self.bot, x) 31 | guilds = str(len(self.bot.guilds)) 32 | users = str(len(set(self.bot.get_all_members()))) 33 | info = f"\nConnected {formats.get_emoji_by_name(':zap:')}\n" \ 34 | f"Gateway server {formats.get_emoji_by_name(':globe_with_meridians:')}: " \ 35 | f"{self.bot.gateway_server_name}\n" \ 36 | f"session server {formats.get_emoji_by_name(':lock:')}: {self.bot.session_server_name}\n" \ 37 | f"Logged in {formats.get_emoji_by_name(':satellite:')}\n" \ 38 | f"User {formats.get_emoji_by_name(':bust_in_silhouette:')}: " \ 39 | f"{self.bot.user.name}({str(self.bot.user.id)})\n" \ 40 | f"Avatar {formats.get_emoji_by_name(':rice_scene:')}:\n" \ 41 | f"{self.bot.user.avatar_url_as(static_format='png', size=512)}\n" \ 42 | f"oauth link {formats.get_emoji_by_name(':link:')}:\n{discord.utils.oauth_url(self.bot.user.id)}\n" \ 43 | f"guilds {formats.get_emoji_by_name(':bar_chart:')}: {guilds}\n" \ 44 | f"Users {formats.get_emoji_by_name(':bar_chart:')}: {users}\n" \ 45 | f"Home {formats.get_emoji_by_name(':house:')}: {self.bot.home.name}\n" \ 46 | f"Home users {formats.get_emoji_by_name(':bar_chart:')}: {str(len(set(self.bot.home.members)))}\n" 47 | self.bot.log.info(info) 48 | 49 | for x in range(self.bot.shard_count): 50 | message = "with hammers 🔨" 51 | # '!help | shard {}/{} | {} guilds | {} users'.format(x + 1, self.bot.shard_count, guilds, users) 52 | game = discord.Game(name=message, type=2) 53 | self.bot.log.info("logged in and ready on shard: {} and set status as ".format(x + 1) + message) 54 | await self.bot.change_presence(activity=game, shard_id=x) 55 | res = await self.bot.session.get(self.bot.user.avatar_url_as(static_format='png', size=1024)) 56 | data = await res.read() 57 | webhook = Webhook.from_url(self.bot.keys['ready_wh'], adapter=AsyncWebhookAdapter(self.bot.session)) 58 | await webhook.edit(name="on_ready", avatar=data) 59 | if self.bot.debug: 60 | self.bot.log.info(f'Is running in Debug, prefix is now {self.bot.config["debug_prefix"]}') 61 | 62 | time = datetime.datetime.now() 63 | fmt = '[ %H:%M:%S ] %d-%B-%Y' 64 | em = discord.Embed(title=f"{self.bot.user.name} Restarted", 65 | description=info, 66 | colour=0x20B2AA) 67 | em.set_author(name='', url=self.bot.user.avatar_url, icon_url=self.bot.user.avatar_url) 68 | em.set_footer(text=time.strftime(fmt), 69 | icon_url=self.bot.user.avatar_url) 70 | await webhook.send(embed=em) 71 | self.bot.log.info("Finished on ready task") 72 | 73 | 74 | 75 | 76 | def setup(bot): 77 | bot.add_cog(Ready(bot)) 78 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.21.0 2 | aiofiles==0.4.0 3 | aiohttp==3.5.4 4 | beautifulsoup4==4.7.1 5 | colorthief==0.2.1 6 | discord==0.16.12 7 | emoji==0.5.1 8 | numpy==1.16.1 9 | psutil==5.6.6 10 | rethinkdb==2.3.0.post6 11 | typing==3.6.6 12 | -e https://github.com/Rapptz/discord.py/archive/rewrite.zip#egg=discord.py[voice] -------------------------------------------------------------------------------- /res/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | MMMMMMMMMMMNmhyssoossyhdNMMMMMMMMMMMMMMM 3 | MMMMMMMmy+-..............-/sdNMMMMMMMMMM 4 | MMMMmo-.....:/+++o+++/:-....../yNMMMMMMM 5 | MMm+...-+ymMMMMMMMMMMMMMNdy+:....omMMMMM 6 | Mm-../hMMMMMMMMMMMMMMMMMMMMMMNy+...yMMMM 7 | M+.-hMMMMMMMMMMMMMMMMMMMMMMMMMMMNy-.sMMM 8 | M+.hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNo.dMM 9 | Md/NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMs/MM 10 | N-hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMsmM 11 | M:./hMMdmyhNMMMMMMMMMMMMMMMMMMMMMMMMMM/m 12 | Md-..:m:sy+/sNMMMMms//h++NMMMMMMMMMMMs-N 13 | MMm/.-h--/oshddmMdsssso--yMMMMMMMMNh/.hM 14 | MMMMmhd--:yhy:--:hy/+o:--omhhyso/:..+mMM 15 | MMMMMMMy:-/ho----:Nm+----y/.....-+yNMMMM 16 | MMMMMMMMMdyyMhoood+-----oh:/+shmMMMMMMMM 17 | MMMMMMMMMMMMmhshdymyyyhNMMMMMMMMMMMMMMMM 18 | MMMMMMMMMMN/.--.`+MMMMMMMMMMMMMMMMMMMMMM 19 | MMMMMMMMMMN-.ds``+MMMMMMMMMMMMMMMMMMMMMM 20 | MMMMMMMMMMMNymosdMMMMMMMMMMMMMMMMMMMMMMM 21 | -------------------------------------------------------------------------------- /utils/checks/bot_checks.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | 4 | 5 | def check_hierarchy(ctx: commands.Context, role: discord.Role): 6 | return ctx.guild.me.top_role.position > role.position 7 | 8 | 9 | def can_send(ctx: commands.Context): 10 | if not ctx.guild: 11 | return False 12 | return ctx.guild.me.permissions_in(ctx.channel).send_messages 13 | 14 | 15 | def can_embed(ctx: commands.Context): 16 | if not ctx.guild: 17 | return False 18 | return ctx.guild.me.permissions_in(ctx.channel).embed_links 19 | 20 | 21 | def can_delete(ctx: commands.Context): 22 | if not ctx.guild: 23 | return False 24 | return ctx.guild.me.permissions_in(ctx.channel).manage_messages 25 | 26 | 27 | def can_ban(ctx: commands.Context, member: discord.Member): 28 | if not ctx.guild: 29 | return False 30 | return ctx.guild.me.permissions_in(ctx.channel).ban_members and check_hierarchy(ctx, member.top_role) 31 | 32 | 33 | def can_kick(ctx: commands.Context, member: discord.Member): 34 | if not ctx.guild: 35 | return False 36 | return ctx.guild.me.permissions_in(ctx.channel).kick_members and check_hierarchy(ctx, member.top_role) 37 | 38 | 39 | def can_edit_user_nick(ctx: commands.Context, member: discord.Member): 40 | if not ctx.guild: 41 | return False 42 | return ctx.guild.me.permissions_in(ctx.channel).manage_nicknames and check_hierarchy(ctx, member.top_role) 43 | 44 | 45 | def can_edit_channel(ctx: commands.Context): 46 | if not ctx.guild: 47 | return False 48 | return ctx.guild.me.permissions_in(ctx.channel).manage_channels 49 | 50 | 51 | def can_edit_role(ctx: commands.Context, role: discord.Role): 52 | if not ctx.guild: 53 | return False 54 | return ctx.guild.me.permissions_in(ctx.channel).manage_roles and check_hierarchy(ctx, role) 55 | -------------------------------------------------------------------------------- /utils/checks/checks.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | 4 | 5 | def is_owner_check(ctx): 6 | return str(ctx.message.author.id) in ctx.bot.config["owners"] 7 | 8 | 9 | def is_bot_owner(): 10 | return commands.check(is_owner_check) 11 | 12 | 13 | # TODO check role/db 14 | def is_donor_check(ctx): 15 | return False 16 | 17 | 18 | def is_donor(): 19 | return commands.check(is_donor_check) 20 | 21 | 22 | def check_permissions(ctx, perms): 23 | if is_owner_check(ctx): 24 | return True 25 | elif not perms: 26 | return False 27 | ch = ctx.message.channel 28 | author = ctx.message.author 29 | resolved = ch.permissions_for(author) 30 | return all(getattr(resolved, name, None) == value for name, value in perms.items()) 31 | 32 | 33 | def role_or_permissions(ctx, check, **perms): 34 | if check_permissions(ctx, perms): 35 | return True 36 | ch = ctx.message.channel 37 | if isinstance(ch, discord.abc.PrivateChannel): 38 | return False 39 | if check: 40 | return True 41 | 42 | 43 | def mod_or_permissions(**perms): 44 | def predicate(ctx): 45 | return role_or_permissions(ctx, ctx.message.author.permissions_in(ctx.message.channel).manage_roles, **perms) 46 | 47 | return commands.check(predicate) 48 | 49 | 50 | def admin_or_permissions(**perms): 51 | def predicate(ctx): 52 | return role_or_permissions(ctx, ctx.message.author.permissions_in(ctx.message.channel).manage_guild, **perms) 53 | 54 | return commands.check(predicate) 55 | 56 | 57 | def guild_owner_or_permissions(**perms): 58 | def predicate(ctx): 59 | guild = ctx.message.guild 60 | if not guild: 61 | return False 62 | 63 | if ctx.message.author.id == guild.owner.id: 64 | return True 65 | 66 | return check_permissions(ctx, perms) 67 | 68 | return commands.check(predicate) 69 | 70 | 71 | def guild_owner(): 72 | return guild_owner_or_permissions() 73 | 74 | 75 | def admin(): 76 | return admin_or_permissions() 77 | 78 | 79 | def mod(): 80 | return mod_or_permissions() 81 | -------------------------------------------------------------------------------- /utils/colors.py: -------------------------------------------------------------------------------- 1 | import io 2 | import discord 3 | from colorthief import ColorThief 4 | import numpy as np 5 | 6 | from discord.ext.commands import AutoShardedBot 7 | 8 | 9 | async def get_effective_color(bot: AutoShardedBot, member: discord.Member): 10 | if member.color != discord.Color.default(): 11 | return member.color 12 | dom_color = await get_dominant_color(bot, bot.get_user(member.id).avatar_url) 13 | if dom_color != discord.Color.default(): 14 | return dom_color 15 | return get_random_color() 16 | 17 | 18 | def get_random_color(): 19 | color = list(np.random.choice(range(256), size=3)) 20 | print(color[0]) 21 | return discord.Color.from_rgb(int(color[0]), int(color[1]), int(color[2])) 22 | 23 | 24 | async def get_dominant_color(bot: AutoShardedBot, url: str): 25 | async with bot.session.get(url) as resp: 26 | image = await resp.read() 27 | with io.BytesIO(image) as f: 28 | try: 29 | color = ColorThief(f).get_color(quality=10) 30 | except: 31 | return discord.Color.default() 32 | if not color: 33 | return discord.Color.default() 34 | return discord.Color.from_rgb(*color) 35 | -------------------------------------------------------------------------------- /utils/config/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import aiofiles as fs 4 | import random 5 | 6 | 7 | async def get_config(): 8 | file = await fs.open('config/config.json', mode="r") 9 | lines = await file.read() 10 | return json.loads(lines) 11 | 12 | 13 | async def get_keys(): 14 | file = await fs.open('config/keys.json', mode="r") 15 | lines = await file.read() 16 | return json.loads(lines) 17 | 18 | 19 | async def get_proxy(): 20 | keys = await get_keys() 21 | return f'http://{random.choice(keys["proxies"])}' 22 | 23 | 24 | async def get_imagur_id(): 25 | keys = await get_keys() 26 | return f'Client-ID {random.choice(keys["imagur_ids"])}' 27 | 28 | 29 | def set_home(bot): 30 | bot.home = bot.get_guild(int(bot.config["home"])) 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /utils/config/setup_bot.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import os 4 | 5 | import psutil 6 | import sys 7 | from collections.__init__ import Counter 8 | 9 | import aiohttp 10 | import rethinkdb as r 11 | from rethinkdb import Connection 12 | from discord.ext import commands 13 | from discord.ext.commands import AutoShardedBot 14 | 15 | from utils.config.config import get_keys, get_config 16 | from utils.formats import get_emoji_by_name, get_icon 17 | from utils.help.HelpFormater import EmbededHelp 18 | 19 | logger = logging.getLogger() 20 | 21 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 22 | 23 | RESET_SEQ = "\033[0m" 24 | COLOR_SEQ = "\033[1;%dm" 25 | BOLD_SEQ = "\033[1m" 26 | TIME_SEQ = COLOR_SEQ % (30 + MAGENTA) 27 | NAME_SEQ = COLOR_SEQ % (30 + CYAN) 28 | FORMAT = "[$TIME_SEQ%(asctime)-3s$RESET]" \ 29 | "[$NAME_SEQ$BOLD%(name)-2s$RESET]" \ 30 | "[%(levelname)-1s]" \ 31 | "[%(message)s]" \ 32 | "[($BOLD%(filename)s$RESET:%(lineno)d)]" 33 | 34 | 35 | def auto_reconnect(self): 36 | while self._instance is None or not self._instance.is_open(): 37 | self.reconnect() 38 | 39 | 40 | Connection.check_open = auto_reconnect 41 | 42 | 43 | def setup_logger(): 44 | color_format = formatter_message(FORMAT, True) 45 | logging.setLoggerClass(ColoredLogger) 46 | color_formatter = ColoredFormatter(color_format) 47 | console = logging.StreamHandler() 48 | file = logging.FileHandler(filename=f'logs/daddy.log', encoding='utf-8', mode='w') 49 | console.setFormatter(color_formatter) 50 | file.setFormatter(color_formatter) 51 | logger.addHandler(console) 52 | logger.addHandler(file) 53 | wh = WebHookHandler("k") 54 | logger.addHandler(wh) 55 | return logger 56 | 57 | 58 | def formatter_message(message: str, colored: bool = True): 59 | if colored: 60 | message = message.replace("$RESET", RESET_SEQ) 61 | message = message.replace("$BOLD", BOLD_SEQ) 62 | message = message.replace("$TIME_SEQ", TIME_SEQ) 63 | message = message.replace("$NAME_SEQ", NAME_SEQ) 64 | return message 65 | else: 66 | message = message.replace("$RESET", "") 67 | message = message.replace("$BOLD", "") 68 | return message 69 | 70 | 71 | COLORS = { 72 | 'WARNING': YELLOW, 73 | 'INFO': BLUE, 74 | 'DEBUG': WHITE, 75 | 'CRITICAL': YELLOW, 76 | 'ERROR': RED 77 | } 78 | 79 | 80 | class ColoredFormatter(logging.Formatter): 81 | def __init__(self, msg, use_color=True): 82 | logging.Formatter.__init__(self, msg) 83 | self.use_color = use_color 84 | 85 | def format(self, record): 86 | level_name = record.levelname 87 | if self.use_color and level_name in COLORS: 88 | level_name_color = COLOR_SEQ % (30 + COLORS[level_name]) + level_name + RESET_SEQ 89 | record.levelname = level_name_color 90 | message = record.msg 91 | if self.use_color and level_name in COLORS: 92 | message_color = COLOR_SEQ % (30 + BLUE) + message + RESET_SEQ 93 | record.msg = message_color 94 | return logging.Formatter.format(self, record) 95 | 96 | 97 | class ColoredLogger(logging.Logger): 98 | def __init__(self, name): 99 | logging.Logger.__init__(self, name, logging.INFO) 100 | return 101 | 102 | 103 | class WebHookHandler(logging.Handler): 104 | def __init__(self, special_args): 105 | logging.Handler.__init__(self) 106 | 107 | def emit(self, record): 108 | # TODO add webhook to tails.fun (no rate-limits there), use this handler in Production for log output 109 | pass 110 | 111 | 112 | class Formatter(commands.HelpFormatter): 113 | def __init__(self, *args, **kwargs): 114 | super().__init__(*args, **kwargs) 115 | 116 | 117 | def setup_bot(bot): 118 | bot.config = bot.loop.run_until_complete(get_config()) 119 | bot.remove_command('help') 120 | discord_log = logging.getLogger('discord') 121 | log = logging.getLogger("main") 122 | discord_log.setLevel(logging.CRITICAL) 123 | bot.log, bot.logger, = log, log 124 | log.info(f"{get_icon()}\nLoading....\n") 125 | bot.formatter = EmbededHelp() 126 | bot.debug = any('debug' in arg.lower() for arg in sys.argv) 127 | bot.uptime = datetime.datetime.utcnow() 128 | bot.commands_used = Counter() 129 | bot.messages_sent = 0 130 | bot.process = psutil.Process() 131 | bot.at_everyone_seen = 0 132 | bot.session = aiohttp.ClientSession(loop=bot.loop) 133 | bot.keys = bot.loop.run_until_complete(get_keys()) 134 | bot.config = bot.loop.run_until_complete(get_config()) 135 | bot.conn = bot.loop.run_until_complete(r.connect("localhost", db=bot.config['db'], port=28015)) 136 | bot.loop.run_until_complete(module_loader(bot, "handlers")) 137 | bot.loop.run_until_complete(module_loader(bot, "commands")) 138 | if bot.debug: 139 | discord_log.setLevel(logging.INFO) 140 | 141 | 142 | def shut_down(bot: AutoShardedBot): 143 | bot.log.info("exiting, cleaning up things") 144 | bot.http._session.close() 145 | bot.session.close() 146 | bot.conn.close() 147 | handlers = logger.handlers[:] 148 | bot.log.info("bye o/") 149 | for h in handlers: 150 | h.close() 151 | logger.removeHandler(h) 152 | 153 | 154 | async def module_loader(bot, 155 | path: str, 156 | name: str = None, 157 | reload: bool = False, 158 | unload: bool = False, 159 | ctx: commands.Context = None): 160 | if unload and name: 161 | try: 162 | bot.unload_extension(f'{path}.{name}') 163 | if ctx: 164 | await ctx.send(f"Done {get_emoji_by_name(':ok_hand:')},unloaded {path}/{name}") 165 | 166 | except Exception as e: 167 | if ctx: 168 | await ctx.send(f"{get_emoji_by_name(':rage:')}, Failed to unload {path}/{name}: : {repr(e)}") 169 | if bot.debug: 170 | bot.log.error(f"Failed to unload {path}/{name}: {repr(e)}", exc_info=True) 171 | else: 172 | bot.log.error(f"Failed to unload {path}/{name}: {repr(e)}", exc_info=False) 173 | return 174 | 175 | if unload: 176 | unloaded = 0 177 | err_counter = 0 178 | for file in os.listdir(f"{path}"): 179 | try: 180 | if file.endswith(".py"): 181 | file_name = file[:-3] 182 | bot.unload_extension(f"{path}.{file_name}") 183 | unloaded += 1 184 | 185 | except Exception as e: 186 | err_counter += 1 187 | if bot.debug: 188 | bot.log.error(f"Failed to unload {path}/{file_name}: {repr(e)}", exc_info=True) 189 | else: 190 | bot.log.error(f"Failed to unload {path}/{file_name}: {repr(e)}", exc_info=False) 191 | continue 192 | if err_counter > 0 and unloaded < 1: 193 | if ctx: 194 | await ctx.send(f"{get_emoji_by_name(':rage:')}, Failed to unload {err_counter} {path}") 195 | 196 | if err_counter > 0 and unloaded > 0: 197 | if ctx: 198 | await ctx.send(f"{get_emoji_by_name(':rage:')}, " 199 | f"Failed to unload {err_counter} {path}, " 200 | f"Unloaded {unloaded} {path} ") 201 | 202 | if ctx: 203 | await ctx.send(f"Done {get_emoji_by_name(':ok_hand:')}, Unloaded {unloaded} {path}") 204 | bot.log.info(f"Unloaded {unloaded} {path}") 205 | return 206 | 207 | if reload and name: 208 | try: 209 | bot.load_extension(f'{path}.{name}') 210 | if ctx: 211 | await ctx.send(f"Done {get_emoji_by_name(':ok_hand:')}, Reloaded {path}/{name}") 212 | bot.log.info(f"Done {get_emoji_by_name(':ok_hand:')}, Reloaded {path}/{name}") 213 | 214 | except Exception as e: 215 | if ctx: 216 | await ctx.send(f"{get_emoji_by_name(':rage:')}, Failed to reload {path}/{name}: : {repr(e)}") 217 | if bot.debug: 218 | bot.log.error(f"Failed to reload {path}/{name}: {repr(e)}", exc_info=True) 219 | else: 220 | bot.log.error(f"Failed to reload {path}/{name}: {repr(e)}", exc_info=False) 221 | return 222 | 223 | if reload: 224 | reloaded = 0 225 | err_counter = 0 226 | for file in os.listdir(f"{path}"): 227 | try: 228 | if file.endswith(".py"): 229 | file_name = file[:-3] 230 | bot.unload_extension(f"{path}.{file_name}") 231 | bot.load_extension(f"{path}.{file_name}") 232 | reloaded += 1 233 | 234 | except Exception as e: 235 | err_counter += 1 236 | if bot.debug: 237 | bot.log.error(f"Failed to reload {path}/{file_name}: {repr(e)}", exc_info=True) 238 | else: 239 | bot.log.error(f"Failed to reload {path}/{file_name}: {repr(e)}", exc_info=False) 240 | continue 241 | 242 | if err_counter > 0 and reloaded < 1: 243 | if ctx: 244 | await ctx.send(f"{get_emoji_by_name(':rage:')}, Failed to reload {err_counter} {path}") 245 | 246 | if err_counter > 0 and reloaded > 0: 247 | if ctx: 248 | await ctx.send(f"{get_emoji_by_name(':rage:')}, " 249 | f"Failed to reload {err_counter} {path}, " 250 | f"Reloaded {reloaded} {path} ") 251 | 252 | if ctx: 253 | await ctx.send(f"Done {get_emoji_by_name(':ok_hand:')}, Reloaded {reloaded} {path}") 254 | bot.log.info(f"Reloaded {reloaded} {path}") 255 | return 256 | 257 | if not name: 258 | loaded = 0 259 | err_counter = 0 260 | for file in os.listdir(f"{path}"): 261 | try: 262 | if file.endswith(".py"): 263 | file_name = file[:-3] 264 | bot.load_extension(f"{path}.{file_name}") 265 | loaded += 1 266 | except Exception as e: 267 | err_counter += 1 268 | if bot.debug: 269 | bot.log.error(f"Failed to load {path}/{file_name}: {repr(e)}", exc_info=True) 270 | else: 271 | bot.log.error(f"Failed to load {path}/{file_name}: {repr(e)}", exc_info=False) 272 | continue 273 | 274 | if err_counter > 0 and loaded < 1: 275 | if ctx: 276 | await ctx.send(f"{get_emoji_by_name(':rage:')}, Failed to load {err_counter} {path}") 277 | return 278 | if err_counter > 0 and loaded > 0: 279 | if ctx: 280 | await ctx.send(f"{get_emoji_by_name(':rage:')}, " 281 | f"Failed to load {err_counter} {path}, " 282 | f"Loaded {loaded} {path} ") 283 | bot.log.error(f"{get_emoji_by_name(':rage:')}, " 284 | f"Failed to load {err_counter} {path}, " 285 | f"Loaded {loaded} {path} ") 286 | return 287 | if ctx: 288 | await ctx.send(f"Done {get_emoji_by_name(':ok_hand:')}, Loaded {loaded} {path}") 289 | bot.log.info(f"Loaded {loaded} {path}") 290 | return 291 | 292 | if name: 293 | try: 294 | bot.load_extension(f'{path}.{name}') 295 | bot.log.info(f"Done {get_emoji_by_name(':ok_hand:')}, Loaded {path}/{name}") 296 | if ctx: 297 | await ctx.send(f"Done {get_emoji_by_name(':ok_hand:')}, Loaded {path}/{name}") 298 | 299 | except Exception as e: 300 | if ctx: 301 | await ctx.send(f"{get_emoji_by_name(':rage:')}, Failed to load {path}/{name}: : {repr(e)}") 302 | if bot.debug: 303 | bot.log.error(f"Failed to load {path}/{name}: {repr(e)}", exc_info=True) 304 | else: 305 | bot.log.error(f"Failed to load {path}/{name}: {repr(e)}", exc_info=False) 306 | -------------------------------------------------------------------------------- /utils/ctx.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from utils.db.models import get_guild 5 | from utils.colors import get_effective_color 6 | from utils.formats import embed 7 | 8 | 9 | class CustomContext(commands.Context): 10 | def __init__(self, **kwargs): 11 | super().__init__(**kwargs) 12 | 13 | @property 14 | def session(self): 15 | return self.bot.session 16 | 17 | @property 18 | def db_conn(self): 19 | return self.bot.conn 20 | 21 | @staticmethod 22 | async def confirm(ctx: commands.Context, message: str=None): 23 | text = '*Are you sure you want to proceed?* `(Y/N)`' 24 | if message: 25 | text = message 26 | await ctx.send(text) 27 | resp = await ctx.bot.wait_for('message', check=lambda m: m.author == ctx.author) 28 | if resp.content.lower().strip() == "n": 29 | return False 30 | else: 31 | return True 32 | 33 | @staticmethod 34 | async def send_cmd_help(self): 35 | channel = self.message.channel 36 | if self.invoked_subcommand: 37 | ps = await self.bot.formatter.format_help_for(self, self.invoked_subcommand) 38 | for p in ps: 39 | p.title = "Missing args :x:" 40 | p.color = discord.Color.red() 41 | await channel.send(embed=p) 42 | else: 43 | ps = await self.bot.formatter.format_help_for(self, self.command) 44 | for p in ps: 45 | p.title = "Missing args :x:" 46 | p.color = discord.Color.red() 47 | await channel.send(embed=p) 48 | 49 | @staticmethod 50 | async def get_color(self, member): 51 | return await get_effective_color(self, member) 52 | 53 | @staticmethod 54 | async def em(self, text, color): 55 | return await embed(self, text, color) 56 | 57 | async def get_guild(self): 58 | return await get_guild(self.bot, self.guild) 59 | 60 | -------------------------------------------------------------------------------- /utils/db/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | 4 | import discord 5 | import rethinkdb as r 6 | from rethinkdb import * 7 | import json 8 | # this will likely be a big file so lets use comments so i dont get lost in 4 weeks 9 | 10 | 11 | # define the Guild class 12 | class GuildEntry: 13 | def __init__(self, bot, data): 14 | self.bot = bot 15 | self._id = data["id"] 16 | self._blacklisted = data["blacklisted"] 17 | self._prefix = data["prefix"] 18 | self._modlog_channel = data["modlog_channel"] 19 | self._auto_role = data["auto_role"] 20 | self._auto_role_bot = data["auto_role_bot"] 21 | self._invite_tracker = data["invite_tracker"] 22 | self._snipe = data.get("snipe", {"enabled": None, "data": {}}) 23 | self.guild_added = data["guild_added"] 24 | 25 | @property 26 | def prefix(self): 27 | return self._prefix 28 | 29 | @prefix.setter 30 | def prefix(self, prefix: str): 31 | self._prefix = prefix 32 | 33 | @property 34 | def blacklisted(self): 35 | return self._blacklisted 36 | 37 | @blacklisted.setter 38 | def blacklisted(self, blacklisted: bool): 39 | self._blacklisted = blacklisted 40 | 41 | @property 42 | def modlog_channel(self): 43 | return self._modlog_channel 44 | 45 | @modlog_channel.setter 46 | def modlog_channel(self, modlog_channel: str): 47 | self._modlog_channel = modlog_channel 48 | 49 | @property 50 | def auto_role(self): 51 | return self._auto_role 52 | 53 | @auto_role.setter 54 | def auto_role(self, auto_role: str): 55 | self._auto_role = auto_role 56 | 57 | @property 58 | def auto_role_bot(self): 59 | return self._auto_role_bot 60 | 61 | @auto_role_bot.setter 62 | def auto_role_bot(self, auto_role_bot: str): 63 | self._auto_role_bot = auto_role_bot 64 | 65 | @property 66 | def invite_tracker(self): 67 | return self._invite_tracker 68 | 69 | @invite_tracker.setter 70 | def invite_tracker(self, invite_tracker): 71 | self._invite_tracker = invite_tracker 72 | 73 | @property 74 | def snipe(self): 75 | return self._snipe 76 | 77 | @snipe.setter 78 | def snipe(self, snipe): 79 | self._snipe - snipe 80 | 81 | def guild_added(self): 82 | return self.guild_added 83 | 84 | async def save(self): 85 | await r.table('guilds').insert({ 86 | "id": self._id, 87 | "blacklisted": self._blacklisted, 88 | "prefix": self._prefix, 89 | "modlog_channel": self._modlog_channel, 90 | "auto_role": self._auto_role, 91 | "auto_role_bot": self._auto_role_bot, 92 | "snipe": self._snipe, 93 | "guild_added": self.guild_added, 94 | "invite_tracker": self._invite_tracker 95 | }, conflict="replace").run(self.bot.conn) 96 | 97 | 98 | ######################################################### 99 | # db init things 100 | ######################################################### 101 | def check_db(): 102 | json_file = open('config/config.json', mode="r") 103 | config = json.load(json_file) 104 | json_file.close() 105 | try: 106 | con = r.connect() 107 | try: 108 | d = r.db_create(config['db']).run(con) 109 | print(d) 110 | except ReqlRuntimeError: 111 | pass 112 | for t in config["tables"]: 113 | try: 114 | t = r.db(config['db']).table_create(t).run(con) 115 | print(t) 116 | except ReqlOpFailedError: 117 | continue 118 | con.close() 119 | r.set_loop_type("asyncio") 120 | except RqlDriverError as e: 121 | print(f"{e}\n\rRethinkDb running?\nexiting...") 122 | sys.exit() 123 | 124 | 125 | async def check_and_create_guild(bot, guild): 126 | if not await r.table("guilds").get(str(guild.id)).run(bot.conn): 127 | time_now = datetime.datetime.today() 128 | await r.table('guilds').insert({ 129 | "id": str(guild.id), 130 | "blacklisted": False, 131 | "prefix": None, 132 | "modlog_channel": None, 133 | "auto_role": None, 134 | "auto_role_bot": None, 135 | "invite_tracker": {"Channel": None, "Invites": {}}, 136 | "snipe": {"enabled": None, "data": {}}, 137 | "guild_added": str(time_now.strftime("%A, %B %d %Y, %I:%M:%S %p")) 138 | }, conflict="update").run(bot.conn) 139 | bot.log.info(f"added guild {str(guild.name)}") 140 | 141 | 142 | async def get_guild(bot, guild): 143 | guild_id = str(guild.id) 144 | guild = await r.table('guilds').get(guild_id).run(bot.conn) 145 | if not guild: 146 | await check_and_create_guild(bot, bot.get_guild(int(guild_id))) 147 | guild = await r.table('guilds').get(guild_id).run(bot.conn) 148 | guild = GuildEntry(bot, guild) 149 | return guild 150 | 151 | 152 | ######################################################### 153 | # prefix things 154 | ######################################################### 155 | async def get_prefix(bot, msg): 156 | if bot.debug: 157 | return bot.config["debug_prefix"] 158 | if isinstance(msg.channel, discord.abc.PrivateChannel): 159 | # Dm`s 160 | return bot.config["dm_prefix"] 161 | try: 162 | g = await get_guild(bot, msg.guild) 163 | if g and g.prefix: 164 | return [msg.guild.me.mention + " ", g.prefix] 165 | else: 166 | prefixes = [msg.guild.me.mention + " "] 167 | [prefixes.append(x) for x in bot.config["prefixes"]] 168 | return prefixes 169 | except Exception as e: 170 | bot.log.error(e) 171 | prefixes = [msg.guild.me.mention + " "] 172 | [prefixes.append(x) for x in bot.config["prefixes"]] 173 | return prefixes 174 | -------------------------------------------------------------------------------- /utils/formats.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from typing import Sequence, Iterator 3 | 4 | import aiohttp 5 | import emoji 6 | import discord 7 | import rethinkdb as r 8 | from discord.ext import commands 9 | from discord.ext.commands import AutoShardedBot 10 | 11 | from utils.checks.bot_checks import can_send, can_embed 12 | 13 | 14 | # https://www.webpagefx.com/tools/emoji-cheat-sheet/ 15 | def get_emoji_by_name(name: str): 16 | return emoji.emojize(name, True) 17 | 18 | 19 | def pagify(text: str, page_length: int = 1900,): 20 | return textwrap.wrap(text, page_length, break_long_words=False) 21 | 22 | 23 | def escape(text: str): 24 | text = text.replace("@everyone", "@\u200beveryone") 25 | text = text.replace("@here", "@\u200bhere") 26 | return text 27 | 28 | 29 | async def embed(ctx: commands.Context, text: str, color: discord.Color=discord.Color.purple()): 30 | if not can_send(ctx): 31 | return 32 | if not can_embed(ctx): 33 | await ctx.channel.send(f"{get_emoji_by_name(':x:')} **error** You must give this bot embed permissions") 34 | em = discord.Embed(description=text, color=color) 35 | await ctx.send(embed=em) 36 | 37 | 38 | def get_icon(): 39 | file = open("res/banner.txt") 40 | return file.read() 41 | 42 | 43 | async def create_gist(bot: AutoShardedBot, 44 | description: str, 45 | filename: str, content: str, 46 | public: bool=True, 47 | session: aiohttp.ClientSession=None): 48 | 49 | api_uri = "https://api.github.com/gists" 50 | headers = { 51 | "Authorization": f"token {bot.keys['gist_token']}", 52 | "Content-Type": "application/json" 53 | } 54 | 55 | payload = { 56 | "description": description, 57 | "public": public, 58 | "files": { 59 | filename: { 60 | "content": content 61 | } 62 | } 63 | } 64 | try: 65 | res = await session.post(api_uri, headers=headers, json=payload) 66 | if res.status >= 200: 67 | data = await r.json() 68 | if 'message' in data: 69 | return "Failed to get gist" 70 | elif 'html_url' in data: 71 | return data['html_url'] 72 | else: 73 | return "Failed to get gist" 74 | else: 75 | return "Failed to get gist" 76 | except aiohttp.client_exceptions as e: 77 | if bot.debug: 78 | bot.log.error(f"Failed to get gist: {repr(e)}", exc_info=True) 79 | else: 80 | bot.log.error(f"Failed to get gist: {repr(e)}", exc_info=False) 81 | return "Failed to get gist" 82 | -------------------------------------------------------------------------------- /utils/help/HelpFormater.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import itertools 4 | import inspect 5 | from discord.ext import commands 6 | from discord.ext.commands.core import GroupMixin, Command 7 | from discord.ext.commands.errors import CommandError 8 | 9 | from utils.help.Paginator import Paginator 10 | 11 | 12 | # code was given to me, heavily edited for my needs 13 | # seems to come from https://github.com/Rapptz/discord.py/blob/rewrite/discord/ext/commands/formatter.py#L126 14 | class EmbededHelp(commands.HelpFormatter): 15 | def __init__(self, show_hidden=False, show_check_failure=False, width=65): 16 | super().__init__(show_hidden, show_check_failure, width) 17 | self.width = width 18 | self.show_hidden = show_hidden 19 | self.show_check_failure = show_check_failure 20 | self.context = "" 21 | self.command = "" 22 | self._paginator = "" 23 | 24 | def has_subcommands(self): 25 | return isinstance(self.command, GroupMixin) 26 | 27 | def is_bot(self): 28 | return self.command is self.context.bot 29 | 30 | def is_cog(self): 31 | return not self.is_bot() and not isinstance(self.command, Command) 32 | 33 | @property 34 | def max_name_size(self): 35 | try: 36 | coms = self.command.all_commands if not self.is_cog() else self.context.bot.all_commands 37 | if coms: 38 | return max(map(lambda c: len(c.name) if self.show_hidden or not c.hidden else 0, coms.values())) 39 | return 0 40 | except AttributeError: 41 | return len(self.command.name) 42 | 43 | @property 44 | def clean_prefix(self): 45 | user = self.context.bot.user 46 | return self.context.prefix.replace(user.mention, '@' + user.name) 47 | 48 | def get_command_signature(self): 49 | prefix = self.clean_prefix 50 | cmd = self.command 51 | return prefix + cmd.signature 52 | 53 | def get_ending_note(self): 54 | command_name = self.context.invoked_with 55 | return f"Type {self.clean_prefix}{command_name} command for more info on a command.\nYou can also type " \ 56 | f"{self.clean_prefix}{command_name} category for more info on a category. " 57 | 58 | async def filter_command_list(self): 59 | def sane_no_suspension_point_predicate(tup): 60 | cmd = tup[1] 61 | if self.is_cog(): 62 | if cmd.instance is not self.command: 63 | return False 64 | 65 | if cmd.hidden and not self.show_hidden: 66 | return False 67 | 68 | return True 69 | 70 | async def predicate(tup): 71 | if sane_no_suspension_point_predicate(tup) is False: 72 | return False 73 | 74 | cmd = tup[1] 75 | try: 76 | return await cmd.can_run(self.context) 77 | except CommandError: 78 | return False 79 | 80 | iterator = self.command.all_commands.items() if not self.is_cog() else self.context.bot.all_commands.items() 81 | if self.show_check_failure: 82 | return filter(sane_no_suspension_point_predicate, iterator) 83 | 84 | ret = [] 85 | for elem in iterator: 86 | valid = await predicate(elem) 87 | if valid: 88 | ret.append(elem) 89 | 90 | return ret 91 | 92 | def _add_subcommands_to_page(self, max_width, coms, prefix: bool=True): 93 | p = self.context.prefix 94 | if not prefix: 95 | p = "" 96 | for name, command in coms: 97 | if name in command.aliases: 98 | continue 99 | entry = f'**{p}{name}**: `{command.short_doc}`' 100 | alias = ", ".join(command.aliases) 101 | if alias: 102 | entry = f'**{p}{name}**: `{command.short_doc}`\n**aliases**: `{alias}`' 103 | self._paginator.add_line(f'{entry}') 104 | if hasattr(command, "group"): 105 | for x in command.commands: 106 | entry = f'**{p}{name} {x.name}**\n`{x.short_doc}`' 107 | alias = ", ".join(x.aliases) 108 | if alias: 109 | entry = f'**{p}{name} {x.name}**: `{x.short_doc}`\n**aliases**: `{alias}`' 110 | self._paginator.add_line(f'{entry}') 111 | 112 | async def format_help_for(self, context, command_or_bot): 113 | self.context = context 114 | self.command = command_or_bot 115 | return await self.format() 116 | 117 | async def format(self): 118 | self._paginator = Paginator() 119 | description = self.command.description if not self.is_cog() else inspect.getdoc(self.command) 120 | if description: 121 | self._paginator.add_line(description, empty=False) 122 | 123 | if isinstance(self.command, Command): 124 | signature = self.get_command_signature() 125 | self._paginator.add_line(signature, empty=True) 126 | 127 | if self.command.help: 128 | self._paginator.add_line(self.command.help, empty=True) 129 | 130 | if not self.has_subcommands(): 131 | self._paginator.close_page() 132 | return self._paginator.pages 133 | 134 | max_width = self.max_name_size 135 | 136 | def category(tup): 137 | cog = tup[1].cog_name 138 | cogs = self.context.bot.cogs 139 | if cog in cogs and hasattr(cogs[cog], "name"): 140 | cog = cogs[cog].name 141 | return cog + ':' if cog is not None else '\u200bNo Category:' 142 | 143 | filtered = await self.filter_command_list() 144 | if self.is_bot(): 145 | data = sorted(filtered, key=category) 146 | for category, coms in itertools.groupby(data, key=category): 147 | coms = sorted(coms) 148 | if len(coms) > 0: 149 | self._paginator.add_line(category) 150 | 151 | self._add_subcommands_to_page(max_width, coms) 152 | else: 153 | filtered = sorted(filtered) 154 | if filtered: 155 | self._paginator.add_line('Commands:') 156 | self._add_subcommands_to_page(max_width, filtered, False) 157 | 158 | self._paginator.add_line() 159 | ending_note = self.get_ending_note() 160 | self._paginator.add_line(ending_note) 161 | return self._paginator.pages 162 | -------------------------------------------------------------------------------- /utils/help/Paginator.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | 4 | # code was given to me, edited for my needs 5 | # seems to come from https://github.com/Rapptz/discord.py/blob/rewrite/discord/ext/commands/formatter.py#L56 6 | class Paginator: 7 | 8 | def __init__(self, max_size=1000): 9 | self.max_size = max_size 10 | self._current_embed = discord.Embed() 11 | self._current_field = [] 12 | self._count = 0 13 | self._embeds = [] 14 | self.last_cog = None 15 | 16 | def add_line(self, line='', *, empty=False): 17 | 18 | if len(line) > self.max_size - 2: 19 | raise RuntimeError(f'Line exceeds maximum page size {self.max_size - 2}') 20 | 21 | if self._count + len(line) + 1 > self.max_size: 22 | self.close_page() 23 | 24 | self._count += len(line) + 1 25 | self._current_field.append(line) 26 | 27 | if empty: 28 | self._current_field.append('') 29 | 30 | def close_page(self): 31 | print(dir(self)) 32 | name = value = '' 33 | while self._current_field: 34 | curr = self._current_field.pop(0) 35 | if curr.strip().endswith(':'): 36 | if name: 37 | if value: 38 | self._current_embed.add_field(name=name, value=value, inline=False) 39 | name, value = curr, '' 40 | self.last_cog = curr 41 | else: 42 | if value: 43 | if self.last_cog: 44 | self._current_embed.add_field( 45 | name=f'{self.last_cog} (continued)', 46 | value=value, 47 | inline=False) 48 | value = '' 49 | name = curr 50 | self.last_cog = curr 51 | else: 52 | value += curr + '\n' 53 | 54 | if self.last_cog and value: 55 | self._current_embed.add_field(name=self.last_cog, value=value) 56 | value = '' 57 | 58 | if value and not self.last_cog: 59 | f = list(filter(None, value.split('\n'))) 60 | y = "\n".join(f[1:]) 61 | self._current_embed.description = f"\n{f[0]}\n{y}" 62 | 63 | self._embeds.append(self._current_embed) 64 | self._current_embed = discord.Embed() 65 | self._current_field = [] 66 | self._count = 1 67 | 68 | @property 69 | def pages(self): 70 | if len(self._current_field) > 1: 71 | self.close_page() 72 | return self._embeds 73 | 74 | def __repr__(self): 75 | fmt = '' 76 | return fmt.format(self) 77 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | 4 | import discord 5 | import rethinkdb as r 6 | 7 | random = random.SystemRandom() 8 | 9 | 10 | def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): 11 | return ''.join(random.choice(allowed_chars) for i in range(length)) 12 | 13 | 14 | def gen_key(): 15 | chars = 'abcdefghijklmnopqrstuvwxyz0123456789.' 16 | return get_random_string(50, chars) 17 | 18 | 19 | async def update(bot): 20 | for url, auth in bot.keys["bot_lists"].items(): 21 | payload = { 22 | 'server_count': bot.gcount 23 | } 24 | headers = { 25 | 'authorization': auth, 26 | 'content-type': 'application/json' 27 | } 28 | async with bot.session.post(url.format(bot.user.id), json=payload, headers=headers) as resp: 29 | bot.log.info(f'Updated {url}, Got {resp}') 30 | 31 | 32 | def get_role_by_id(roles, role_id: int): 33 | return discord.utils.get(roles, id=role_id) 34 | 35 | 36 | async def get_role_from_string(guild, rolename, roles=None): 37 | if roles is None: 38 | roles = guild.roles 39 | roles = [role for role in roles if (r is not None)] 40 | role = discord.utils.find((lambda role: (role.name.lower() == rolename.lower())), roles) 41 | return role 42 | 43 | 44 | def get_bot_uptime(self, *, brief=False): 45 | now = datetime.datetime.utcnow() 46 | delta = now - self.bot.uptime 47 | hours, remainder = divmod(int(delta.total_seconds()), 3600) 48 | minutes, seconds = divmod(remainder, 60) 49 | days, hours = divmod(hours, 24) 50 | if not brief: 51 | if days: 52 | fmt = '{d} days, {h} hours, {m} minutes, and {s} seconds' 53 | else: 54 | fmt = '{h} hours, {m} minutes, and {s} seconds' 55 | else: 56 | fmt = '{h} hours {m} minutes and {s} seconds' 57 | if days: 58 | fmt = ('{d} days ' + fmt) 59 | return fmt.format(d=days, h=hours, m=minutes, s=seconds) 60 | --------------------------------------------------------------------------------