1
0
Fork 0
mirror of synced 2024-05-09 23:22:27 +12:00

Compare commits

...

703 commits

Author SHA1 Message Date
Dan Hess fcd5d5f702 Merge branch 'master' into rewrite 2020-04-18 22:49:15 -05:00
Dan Hess 13e20f73ad Clean up API method, man I must have made this a while ago, it's ugly 2020-04-13 15:07:54 -05:00
Dan Hess 04c029b368 Use asyncio's subprocess module 2020-04-13 14:15:37 -05:00
Dan Hess 86546dec16 Move horse noodle API to its own method, add more images 2020-04-13 14:10:03 -05:00
Dan Hess a2cd241265 Correct how to get horse/snek 2020-04-13 14:02:41 -05:00
Dan Hess 33842a145f Correct where to pull config from 2020-04-06 12:03:56 -05:00
Dan Hess 3587154281 Update e621 to work with new API 2020-04-06 12:00:46 -05:00
Dan Hess 84c0ea6414 Catch no permissions to remove poll reaction 2020-04-06 12:00:38 -05:00
Dan Hess 64bf95a872 These aren't used anymore here, remove them 2020-04-04 01:35:02 -05:00
Dan Hess fbff5c1fff Update to work with new flags 2020-04-04 01:33:35 -05:00
Dan Hess 0ee278546a Update to work with new flags 2020-04-04 01:32:28 -05:00
Dan Hess 1c1eade166 Correct cache lookup 2020-03-30 15:19:14 -05:00
Dan Hess 1c0c7fa053 Correct a couple errors 2020-03-25 14:52:33 -05:00
Dan Hess 2770d49b07 Fixed enable/disable not correcting cache values 2020-03-12 15:03:08 -05:00
Dan Hess 1fd5362177 Catch no permissions to remove reaction 2020-03-04 18:32:38 -06:00
Dan Hess 0fc3ccc518 Pass the guild object 2019-12-27 08:26:18 -06:00
Dan Hess 99f8d19cef Ensure cache is updated wherever needed 2019-12-27 08:23:41 -06:00
Dan Hess a612d4231b Provide an error for invalid quotes 2019-11-30 15:24:17 -06:00
Dan Hess 11ff68f7e7 Fix database call 2019-11-23 20:41:37 -06:00
Phxntxm 4f46455fa3 Update to fetch 2019-11-19 17:29:51 -06:00
Phxntxm b6f2b74360 Correct if logic to make sure first error hit is sent too 2019-11-19 17:29:12 -06:00
Phxntxm dd95bade31 Can't strip the error object 2019-11-19 17:27:04 -06:00
Phxntxm ad41accdc3 Fix error channel (thanks ctrl+z) 2019-11-19 17:22:42 -06:00
Phxntxm 0179f3913c Add a way to send errors to an error channel instead of an error log 2019-11-19 17:13:10 -06:00
Phxntxm 8bff707037 Woops, forgot to put that in the try 2019-11-19 09:52:11 -06:00
Phxntxm d37ab8d91c Update the yaml loading 2019-11-18 21:26:39 -06:00
Phxntxm c8da4839a8 Handle an error where the OSU api literally doesn't return anything 2019-11-18 21:26:26 -06:00
MahxieNoodle edb0f8ac2c Change how Horses and Snakes are accessed (#25)
* Update Animal Image Fetch 1/4

* Animal Image Fetch 2/4

* Requested changes for Animal Image Fetch 3/4
2019-11-04 21:56:41 -06:00
phxntxm b963fad7be Update to match new version of discord.py 2019-04-28 14:34:59 -05:00
phxntxm 326ec78004 Correct variable check 2019-03-02 13:15:10 -06:00
phxntxm 443c1a2801 Correct database call 2019-03-02 13:14:57 -06:00
phxntxm 82fa6e9984 Handle no settings set for checking prefix config; show bad argument errors 2019-03-02 13:14:42 -06:00
phxntxm b0c4496f88 Quote because postgres is stupid 2019-03-01 20:10:48 -06:00
phxntxm f7e2bc673f Quote because postgres is stupid 2019-03-01 20:05:29 -06:00
phxntxm b75b4f7776 Correct way to use listeners 2019-02-27 18:03:15 -06:00
phxntxm 5f568b37de Update to use new cog paradigm 2019-02-23 14:13:10 -06:00
phxntxm 2035b963eb Correct how to check settings 2019-02-17 14:13:23 -06:00
phxntxm 867fea273b Ignore if the guild already exists 2019-02-17 14:09:06 -06:00
phxntxm e561e821ca Typo 2019-02-17 14:08:07 -06:00
phxntxm b2f2120529 Ensure entry exists before trying to set anything 2019-02-17 14:07:26 -06:00
phxntxm f836c146df Show message when no role is set 2019-02-17 13:58:09 -06:00
phxntxm 54e3463a51 Handle if no settings in the server at all 2019-02-17 13:57:02 -06:00
phxntxm 04f0b80db8 Handle if tag is not found 2019-02-17 13:56:45 -06:00
phxntxm 2e6903dfc9 Correct what to lookup in tag info 2019-02-17 13:47:34 -06:00
phxntxm be971dda94 Add a way to have Bonfire add a role to someone when they join a server 2019-02-17 13:45:52 -06:00
phxntxm a211fe3048 Correct who to compare losses to 2019-02-16 15:12:34 -06:00
phxntxm f318731882 Forgot to return the embed 2019-02-14 18:38:18 -06:00
phxntxm f99e419a2b Update commands that no longer worked with new database 2019-02-14 18:36:13 -06:00
phxntxm 6e2ab01cbf Handle exceptions being over 2000 characters 2019-02-14 18:35:53 -06:00
phxntxm 73bb685d1c Remove space 2019-02-14 18:35:36 -06:00
phxntxm cae455675b Handle invalid dates being given 2019-02-14 18:35:22 -06:00
phxntxm 54b0ac2538 Don't override the default hugs 2019-02-05 23:52:52 -06:00
phxntxm 9944673b1e Wait until ready, to ensure the database has been loaded 2019-02-03 18:59:53 -06:00
phxntxm aabe972056 Convert data types to their applicable discord types 2019-02-03 16:39:48 -06:00
phxntxm 420aeaac95 Include custom messages for each setting to make things more clear 2019-02-03 16:34:57 -06:00
phxntxm 4d0bb27451 Correct way to remove from arrays 2019-02-03 16:00:12 -06:00
phxntxm 66148d90f2 Convert to int to allow removal 2019-02-03 15:49:41 -06:00
phxntxm 2ce001784f Remove rethinkdb import 2019-02-01 19:33:07 -06:00
phxntxm d7c37dd959 Setup online dict before starting task, to ensure dupe onlines aren't sent when starting up the cog 2019-01-29 17:53:07 -06:00
phxntxm d0943ff1dd Logic fix 2019-01-29 17:37:21 -06:00
phxntxm e239194636 Ensure database exists before loading it 2019-01-29 17:37:13 -06:00
phxntxm d950c458ef remove all temporary print statements 2019-01-29 00:21:00 -06:00
phxntxm a95d32fbcc Nice typo Phantom 2019-01-29 00:18:33 -06:00
phxntxm 2788083257 The entry in the result is no longer the member itself, it's an id....so get the member based on the id 2019-01-29 00:17:56 -06:00
phxntxm b64eedba6b No longer need to convert to string 2019-01-29 00:14:23 -06:00
phxntxm 77de5d1778 Handle removing things based on id's 2019-01-29 00:14:15 -06:00
phxntxm 6668030cd3 COrrect bool comparison 2019-01-29 00:06:05 -06:00
phxntxm ec0fa5509d Base on the actual stream name, not the string 'name' 2019-01-29 00:03:22 -06:00
phxntxm 6269510bf9 Honestly, no idea how the fuck this worked before? That's not how that works... 2019-01-28 23:56:34 -06:00
phxntxm 527d4b19be Some may not exist, assume they're off 2019-01-28 23:48:52 -06:00
phxntxm 59e42d8182 Use new database for some role commands 2019-01-28 23:45:26 -06:00
phxntxm e13b74ce38 Ensure settings exist in battle 2019-01-28 23:45:15 -06:00
phxntxm 943b46d4e9 Correct what to base the status on 2019-01-28 23:45:08 -06:00
phxntxm 110720f278 Ensure it's NEVER None, even if None is in the dict 2019-01-28 00:29:59 -06:00
phxntxm d031459349 Refresh cache on some important updates 2019-01-28 00:23:19 -06:00
phxntxm a2bbe427be Correct mostboops command 2019-01-27 21:26:32 -06:00
phxntxm f8a3b64928 Correct how to check if it's in a list 2019-01-27 21:24:56 -06:00
phxntxm b11f6ea869 Correct how to check if it's in a list 2019-01-27 21:23:52 -06:00
phxntxm ef912ef7b8 Don't try to do anything if server has no settings 2019-01-27 21:22:55 -06:00
phxntxm 0fe4d06b59 Rethinkdb is not needed anymore 2019-01-27 21:09:01 -06:00
phxntxm bd0fa7a049 Add database settings to example config file 2019-01-27 21:00:49 -06:00
phxntxm b1733a9673 Remove overwatch from cogs to load 2019-01-27 20:59:59 -06:00
phxntxm 75af7dcd6f Rewrite of database/configuration 2019-01-27 20:58:39 -06:00
phxntxm 68ccd02fba Commit 2018-10-29 20:17:54 -05:00
phxntxm 29a14d2e3b Add spades; Move utils to it's own folder outside cogs; Remove images and fonts 2018-10-29 20:00:37 -05:00
phxntxm 74f474b9a7 Correct error that disabled all commands if one command is disabled 2018-10-20 23:37:41 -05:00
phxntxm a12d9c6dd7 Catch edge case 2018-10-20 23:37:22 -05:00
phxntxm 1625422a5f Move back to pagination help command 2018-10-16 18:44:19 -05:00
phxntxm 433aaee7be Catch if someone has booped someone else, but not in this server 2018-10-14 12:23:48 -05:00
phxntxm 528546664e Add an exception to ignore 2018-10-14 12:23:29 -05:00
phxntxm d00b878ecd Correct where to import bucket from 2018-10-12 15:50:42 -05:00
phxntxm dc9f49f568 Add coolodown to help to see if this will stop memory problems 2018-10-12 15:47:07 -05:00
phxntxm 2d03bbc672 Remove donation info 2018-10-12 15:46:53 -05:00
phxntxm 883565fbf3 Remove twitch 2018-10-12 15:46:43 -05:00
phxntxm 925beb8b2e Send short messages to the channel 2018-10-10 19:36:39 -05:00
phxntxm f0cbb39d25 Send short messages to the channel 2018-10-10 19:34:52 -05:00
phxntxm f0ddede3c4 Remove twitch cog 2018-10-10 18:46:49 -05:00
phxntxm 9b24998c17 Add a ping command 2018-10-10 18:46:34 -05:00
phxntxm 8388cfa840 Correct couple small errors caused by database format change 2018-10-09 09:38:39 -05:00
phxntxm fda6b3aae2 Remove ljust 2018-10-08 19:29:20 -05:00
phxntxm d19696e0ec Put dev server invite at the bottom of help command 2018-10-08 19:28:47 -05:00
phxntxm 087114033c Add descriptions to classes for help command 2018-10-08 15:40:21 -05:00
phxntxm c63b7a176f Update help command to remove pagination 2018-10-08 15:40:10 -05:00
phxntxm 764759b30a Add a way to print info about a specific role 2018-10-05 10:26:10 -05:00
phxntxm 2395aba4f7 Correct reference to roles 2018-10-05 10:13:10 -05:00
phxntxm 565a7361e6 Add a notify command 2018-10-04 17:28:37 -05:00
phxntxm d1d16390ef Update to use new paginator 2018-10-04 15:36:58 -05:00
phxntxm 6cb45a50d5 Correct activity update 2018-10-04 14:38:15 -05:00
phxntxm ff4eb301c6 Remove custom calls 2018-10-04 14:36:01 -05:00
phxntxm c2650ace52 Update paginator/help command 2018-10-04 14:14:09 -05:00
phxntxm 4dc82c7b48 Start correct task 2018-10-04 13:01:33 -05:00
phxntxm 7cfe02cc56 Update to conform to new pendulum versions 2018-10-04 12:56:13 -05:00
phxntxm bc63ed810c Update to conform to new pendulum versions 2018-10-04 12:48:59 -05:00
phxntxm 7f05307b78 Update to conform to new pendulum versions 2018-10-04 12:38:38 -05:00
phxntxm 757b49a3eb Change how ownership dict is read 2018-09-23 18:02:19 -05:00
phxntxm a173451164 Correct how the methods are called 2018-09-23 17:51:31 -05:00
phxntxm 297aeb0b6e Correct how the methods are called 2018-09-23 17:47:19 -05:00
phxntxm 182eede186 Use new pendulum now method 2018-09-23 17:41:05 -05:00
phxntxm 6f41b1f500 Remove old check 2018-09-23 17:40:21 -05:00
phxntxm 1cf766820d Correct syntax error in check 2018-09-23 17:38:34 -05:00
phxntxm 26e9e53c80 Correct syntax error in check 2018-09-23 17:37:39 -05:00
phxntxm a96835326c Change to check all requirements in the command checks, not some in on_message 2018-09-23 17:34:14 -05:00
phxntxm ad0b4a6362 PEP8 2018-09-23 14:23:27 -05:00
phxntxm 501e5b9452 Default to empty dict if there's no entry 2018-09-23 13:40:09 -05:00
phxntxm e5b5d30553 Update to change cache lookup from o(n) to o(1) 2018-09-23 13:33:46 -05:00
phxntxm e7c2cdceb7 Change database saving, and command reading to not create tasks each time 2018-09-23 01:04:03 -05:00
phxntxm b897740e76 Change order of channel checking to ensure it exists 2018-09-23 00:18:13 -05:00
phxntxm c7742a8023 Move tasks into their own methods, to try to gc better 2018-09-22 11:17:11 -05:00
phxntxm 259da83950 Don't allow removal of picarto before addition, it bugs the entry in the database 2018-09-21 17:03:34 -05:00
phxntxm 4f8f606609 Catch race condition 2018-09-21 17:03:19 -05:00
phxntxm fbb5873884 Update clientsession line 2018-07-14 23:09:51 -05:00
phxntxm d88a34f1a5 Asyncio changed, need async with now 2018-07-14 23:05:43 -05:00
phxntxm 09d8f0535a Correct nsfw lookup 2018-06-23 12:12:31 -05:00
phxntxm ff672d1f81 Limit urban to nsfw channels 2018-06-23 12:10:37 -05:00
phxntxm 9be23219c5 Correct accuracy 2018-06-11 17:36:10 -05:00
phxntxm cb292ec3a7 Add new discord bots site 2018-05-24 00:09:42 -05:00
phxntxm 9b0e13b744 Include new bot sites 2018-05-21 17:30:47 -05:00
phxntxm b9eb3eddac Correct value sent when listenign to something 2018-05-07 10:02:27 -05:00
phxntxm 9e39888589 Don't make command server only 2018-05-07 10:02:02 -05:00
phxntxm 6145ab3df3 remove carriage returns 2018-05-07 10:01:16 -05:00
phxntxm 750b12f7d9 Clean up cat/e621 api calls 2018-05-07 09:56:23 -05:00
phxntxm 9e46f4c37c Use correct form for games 2018-04-25 22:11:38 -05:00
phxntxm fc2c5b5e5f Correct error to catch when there's no result 2018-04-25 22:11:27 -05:00
phxntxm 8560f29e17 Correct sleep 2018-04-25 22:08:45 -05:00
phxntxm e32c398290 Don't call parent command 2018-04-24 18:16:24 -05:00
phxntxm 011b550ea5 Correct cogs to load 2018-04-24 18:12:04 -05:00
phxntxm bf522d33bd Correct cogs to load 2018-04-24 18:11:30 -05:00
phxntxm 279a3e7116 Remove lingering music calls 2018-04-24 18:11:12 -05:00
phxntxm 40633e9744 Add description to spotify command 2018-04-24 18:04:47 -05:00
phxntxm ad91fa4030 Correct permission required for spotify 2018-04-24 18:00:25 -05:00
phxntxm c933457c72 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2018-04-24 17:58:53 -05:00
phxntxm 1c320b5e6e Don't show colour roles in list 2018-04-24 17:55:47 -05:00
phxntxm 604e427af4 New Activity Type 2018-04-24 17:55:28 -05:00
phxntxm 715274e99e New change_presence use 2018-04-24 17:54:01 -05:00
phxntxm d7b9e04742 Remove music 2018-04-24 17:53:43 -05:00
phxntxm e24c8e3a93 Add Spotify options 2018-04-24 17:52:53 -05:00
phxntxm 09969ada7b remove music 2018-04-24 17:49:36 -05:00
phxntxm 465c9fa316 remove old files/remove music 2018-04-24 17:48:25 -05:00
phxntxm 3f7eaaa248 Add a spotify search for playlists and songs 2018-04-24 17:48:01 -05:00
Dan Hess 76a8896773
Merge pull request #24 from MahxieNoodle/patch-1
New cat api endpoint
2018-03-24 11:55:54 -05:00
MahxieNoodle d15397dad9
New cat api endpoint
Updated cat api url.
2018-03-22 10:21:12 -06:00
Phxntxm d66bee2e38 Correct spacing issue 2018-01-12 16:18:03 -06:00
Phxntxm f36c340f11 Correct spacing issue 2018-01-12 16:16:53 -06:00
Phxntxm 86f9984998 Convert to string 2018-01-12 16:15:21 -06:00
Phxntxm 122461b6e0 Corect logic for showing parameters 2018-01-12 16:14:23 -06:00
Phxntxm 536853b00e Ensure there are aliases before adding them 2018-01-12 15:50:06 -06:00
Phxntxm f942852813 Send as an actual embed object 2018-01-12 15:48:19 -06:00
Phxntxm 58539d85fa Import Discord 2018-01-12 15:45:44 -06:00
Phxntxm 8877f6f2be No need for method to be async 2018-01-12 15:43:09 -06:00
Phxntxm 6858a22a11 Add a tutorial command 2018-01-12 15:41:33 -06:00
Phxntxm 37270d6b6c Add a check if no output is found 2018-01-12 15:41:12 -06:00
Phxntxm 936040a6c8 Use check_output to stop blocking 2018-01-11 11:32:41 -06:00
Phxntxm 0d3ec6f46f Use a shell in the bash command 2018-01-11 11:07:42 -06:00
Phxntxm 11b6366360 Add a bash command 2018-01-11 10:56:11 -06:00
Dan 5fe0e65cb3 Correct typo 2018-01-11 10:43:38 -06:00
Dan 9019426302 Added a horse command/use API for snake command 2018-01-11 10:08:23 -06:00
phxntxm 7ee3725c7e error out if an invalid colour is given 2018-01-01 22:59:19 -06:00
phxntxm c4d0a3b2a9 Add a command to control colours used 2018-01-01 18:20:27 -06:00
phxntxm e97c50edfe Add a command to control colours used 2018-01-01 18:10:57 -06:00
phxntxm a6d0d507d9 Add a command to control colours used 2018-01-01 18:09:10 -06:00
phxntxm fb26b7ffcf Add a command to control colours used 2018-01-01 18:08:11 -06:00
phxntxm 164d62a179 Add a command to control colours used 2018-01-01 18:06:24 -06:00
phxntxm 0ff6704206 Add a coinflip command 2017-12-02 16:21:39 -06:00
phxntxm 4065f63e70 Increase limit to 30 characters 2017-12-02 16:19:54 -06:00
phxntxm 85e34b32ca display command based on parent used 2017-11-28 17:40:19 -06:00
phxntxm b19d080232 Handle key errors in welcome/goodbye messages 2017-11-28 17:38:41 -06:00
phxntxm 4312b5f5ef Stop auto-reconnecting for voice channels 2017-11-11 16:22:04 -06:00
phxntxm 5a28951eb7 Handle if there are no roles 2017-11-08 11:01:02 -06:00
phxntxm 8adde44fb1 Temporarily disable error handling of stuck voice channels 2017-11-08 11:00:22 -06:00
phxntxm 3f13f6af6d Silently handle a few more errors 2017-11-08 11:00:05 -06:00
phxntxm 60cf454122 Correct how to handle when permissions are missing 2017-10-15 15:45:03 -05:00
phxntxm 549f72d660 Work with nonexistant channels 2017-09-25 15:16:41 -05:00
phxntxm 2b4e098d67 Rewrite the hugs example 2017-09-21 18:31:46 -05:00
phxntxm 09e6f17a11 Add what to do for channels that no longer exist 2017-09-21 17:36:09 -05:00
phxntxm 88b8c04117 Ignore errors on error sending message 2017-09-13 03:47:47 -05:00
phxntxm 8a6ec4e681 Handle default channel no longer existing 2017-09-13 03:47:29 -05:00
phxntxm b8153756d4 Remove embed notifs for me 2017-09-13 03:47:05 -05:00
Phxntxm 9b4fe182e0 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-08-10 16:55:06 -05:00
Phxntxm af9eeabe5e Correct implementation of checking hug messages 2017-08-10 16:54:55 -05:00
phxntxm da67359bb3 Correct how to check if default messages should be used 2017-08-08 09:41:52 -05:00
Phxntxm ff60827bdf Use the correct defaulted battles 2017-08-07 13:59:37 -05:00
Phxntxm 7fd4cb5abe Correct how to get the bot attribute 2017-08-06 18:17:31 -05:00
Phxntxm f221a0eee5 Handle an edge case with voice disconnection 2017-08-06 18:15:10 -05:00
Phxntxm 78afa83970 Handle an edge case with voice disconnection 2017-08-06 18:10:59 -05:00
Phxntxm f4ee4e81ae Await the coroutine 2017-08-06 16:26:28 -05:00
Phxntxm 151b3c4ebf Convert the right thing 2017-08-06 16:24:03 -05:00
Phxntxm 0a90b8c05a Convert the right thing 2017-08-06 16:22:14 -05:00
Phxntxm 6facd5794b I don't know how that changed 2017-08-06 16:21:36 -05:00
Phxntxm acc402297b Add the position in the queue to the embed 2017-08-06 16:16:43 -05:00
Phxntxm 43fe8c3a67 Add a message for when everyone is attempted to be hugged/battled 2017-08-06 16:16:30 -05:00
Phxntxm fba57a0747 Allow checking a person's perms 2017-08-06 16:16:14 -05:00
Phxntxm d7f7f326b8 Correct what date to use/correct what colour to use 2017-08-05 14:50:12 -05:00
Phxntxm a6be874cb4 Actually send the embed as an embed 2017-08-05 14:28:11 -05:00
Phxntxm 5affddcd46 Add a userinfo command 2017-08-05 14:27:22 -05:00
Phxntxm aa6f8350db Handle default ExtractionError 2017-08-04 13:59:27 -05:00
Phxntxm 7b2af300c0 Pass required params when erroring 2017-08-04 13:59:10 -05:00
Phxntxm 85d0b8ba74 Check to ensure that the battle/hug message being added is in a valid format 2017-08-04 13:58:57 -05:00
Phxntxm 0fcd51958a Removal of default_channel 2017-07-31 20:39:10 -05:00
Phxntxm 48ac555131 Include AttributeError 2017-07-31 14:23:23 -05:00
Phxntxm fa68cba89a Support for livestreams 2017-07-30 21:48:39 -05:00
Phxntxm d0cacd7334 Ensure settings is an empty dictionary by default 2017-07-30 20:12:36 -05:00
Phxntxm a8d6c0ff31 Correct capitalization of permission 2017-07-30 20:11:32 -05:00
Phxntxm afcbca3e64 Allow server specific battles/hugs 2017-07-30 18:27:19 -05:00
Phxntxm d60ce6122c Correct reference to method 2017-07-29 14:36:52 -05:00
Phxntxm 29bcea3b21 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-07-29 14:36:06 -05:00
Phxntxm c1b13be7e0 Sort birthdays by whose is closets 2017-07-29 14:35:51 -05:00
phxntxm 63a7788030 Refresh the entry 2017-07-28 08:43:58 -05:00
phxntxm cbbec95196 Correct the parameters for overwatch 2017-07-28 08:43:34 -05:00
Phxntxm 7c4c7facaa Set the process originally to avoid error's on failed source's cleanups 2017-07-27 19:18:07 -05:00
Phxntxm af8b3bcf29 Check the extractor in case playlist incorrectly returns None (why does this happen...why can't you just be reliable youtube...this data shouldn't be possible....) 2017-07-27 15:27:58 -05:00
Phxntxm 54967acfbd Check both guild and channel permissions 2017-07-27 13:56:12 -05:00
Phxntxm c3e8e0fac6 Check if voice exists before trying to stop it 2017-07-27 13:56:03 -05:00
phxntxm 762bc7c3dc Correct the primary key for raffles 2017-07-27 01:14:49 -05:00
phxntxm 446990f7e5 Change permissions to use guild permissions 2017-07-25 19:06:29 -05:00
Phxntxm 0c53100e7b Raise limit of dice to 30 2017-07-23 21:15:01 -05:00
Phxntxm d3670b8982 Use the correct URL 2017-07-23 18:49:32 -05:00
Phxntxm 8f98b4ea95 Provide the rest of the settings needed for WrongEntryTypeError 2017-07-23 18:49:12 -05:00
Phxntxm 6e4c4703c2 Continue on the correct loop when server is None 2017-07-23 18:48:46 -05:00
Phxntxm 755d7cde06 Use correct attribute for length of a song 2017-07-23 17:34:28 -05:00
Phxntxm c782ad8727 Don't use defunct id 2017-07-23 14:59:23 -05:00
Phxntxm 7964be7adb Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-07-22 16:43:05 -05:00
Phxntxm fb975f34b0 Paginate the first 5 definitions 2017-07-22 16:42:50 -05:00
Phxntxm ae60ef5eed Paginate the first 5 definitions 2017-07-22 16:42:29 -05:00
Phxntxm 9ed107b997 Add 1v1 alias 2017-07-22 16:42:21 -05:00
phxntxm 6ab976681c Correct birthday channel id lookup 2017-07-19 18:08:03 -05:00
phxntxm f513b92cc7 Use the correct lookup on server settings 2017-07-19 18:02:33 -05:00
phxntxm 55ce47f383 Fix a couple errors with birthdays 2017-07-19 17:53:50 -05:00
phxntxm d3c5332483 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-07-18 17:08:19 -05:00
phxntxm a7a8444e4d Remove unneeded pass_context calls 2017-07-18 17:08:09 -05:00
Phxntxm 041fd8628e Add a check for if the state exists 2017-07-16 19:46:06 -05:00
Phxntxm dec0875e10 Add Unkown Message to ignored errors 2017-07-15 18:00:41 -05:00
Phxntxm aa2a2aa4ee Add ValueError's to the caught errors 2017-07-15 18:00:25 -05:00
Phxntxm caa3854dc2 Ensure the Picarto channel is given when checking online 2017-07-15 14:13:40 -05:00
Phxntxm 8ff72f07bd add the birthday module by default 2017-07-13 21:48:29 -05:00
Phxntxm b13d6510a4 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-07-13 15:44:06 -05:00
phxntxm 45d80bf6b9 Newline corrections 2017-07-11 14:38:12 -05:00
phxntxm 048af76400 Correct attribute lookup on owner 2017-07-11 14:36:24 -05:00
phxntxm fab533c07a Add patreon_link to config 2017-07-11 14:35:44 -05:00
phxntxm 368914a7c7 Rework the info command 2017-07-11 14:34:45 -05:00
phxntxm 5f571567a3 Handle if something that's not a playlist is given 2017-07-11 12:59:24 -05:00
phxntxm c33b16136f Change raffles so they're included in one table, per server 2017-07-11 12:57:21 -05:00
phxntxm 7f261a81fa Save volume entries; handle some playlist quirks 2017-07-11 12:56:55 -05:00
phxntxm e307a5262c Don't log large image errors 2017-07-11 12:54:30 -05:00
phxntxm 9c25e032b5 Rearrange the command and alias since there is already a donator attribute 2017-07-10 00:43:32 -05:00
phxntxm 00b4d82c4e Pep8 2017-07-10 00:39:03 -05:00
phxntxm 46a1280256 Allow the use of forcing content_type for json 2017-07-10 00:20:30 -05:00
phxntxm 28e912728b Include the correct entry name 2017-07-09 23:46:02 -05:00
phxntxm e42381b5b5 Add a donator command 2017-07-09 23:42:58 -05:00
phxntxm 76a6af2383 Include patreon info in config 2017-07-09 23:42:01 -05:00
phxntxm 187c6d3ba7 Force disconnect on stop 2017-07-09 15:04:48 -05:00
phxntxm eaef475410 Use predownloaded info for filename 2017-07-09 15:03:16 -05:00
phxntxm a4a47cded3 Correct accidental override of info 2017-07-09 14:52:09 -05:00
Phxntxm a93432d0b7 Add the birthday module 2017-07-03 21:02:34 -05:00
Phxntxm 29625cb9dc Add the birthday table to require tables 2017-07-03 21:02:22 -05:00
Phxntxm 2c47ab881b Catch data being None 2017-07-03 21:02:07 -05:00
Phxntxm 4f816216f8 Add the ability to turn off birthday announcemtns 2017-07-03 21:01:57 -05:00
phxntxm 13ce1d861e Pillow changes 2017-07-02 23:55:27 -05:00
Phxntxm 9fd3891125 Implement an audio source for youtube_dl entries 2017-07-02 22:20:27 -05:00
Phxntxm 042b2fcaea Change up downloading method 2017-07-02 22:20:01 -05:00
Phxntxm ffa1126f01 Set default searching method to youtube 2017-07-02 22:19:37 -05:00
Phxntxm 0be369fb79 Add an import command for playlists; handle new downloading method of songs 2017-07-02 22:19:20 -05:00
Phxntxm 56ca4dabf2 Use the new method for adding songs 2017-07-02 22:18:51 -05:00
Phxntxm 086b7f660b Remove embed from invite link 2017-07-02 22:18:33 -05:00
Phxntxm 60326c7053 Only allow double on the first action in a turn 2017-07-02 22:18:18 -05:00
Phxntxm 08a31d284b Add the abilty to change whether or not the import command can be used 2017-07-02 22:18:00 -05:00
Phxntxm a752d3a3f7 Ensure current is set to None when no song is next 2017-06-30 19:46:02 -05:00
Phxntxm 61b6c5d0be Add a manual check for admin privileges 2017-06-30 19:30:40 -05:00
Phxntxm 29d6eb4e4c Add a block for icecast radios 2017-06-29 22:38:15 -05:00
Phxntxm b0b6400747 Delete the poll message from the author 2017-06-29 18:57:49 -05:00
Phxntxm 120d95096f Use the name instead of mention, since mentions will sometimes break due to the member leaving the server 2017-06-29 14:50:12 -05:00
Phxntxm 3494066ae7 Change the name of the owner, due to self-hosting 2017-06-29 14:49:52 -05:00
phxntxm ea53c2dbd0 Allow subtract and addition for rolls 2017-06-28 17:43:28 -05:00
phxntxm 9ca6f8996f Handle no playlist being given 2017-06-28 14:54:07 -05:00
phxntxm 049973f026 Handle failures to download 2017-06-28 14:53:48 -05:00
phxntxm 899c02fd77 Handle first time battles 2017-06-28 14:53:28 -05:00
phxntxm 8f078ea0ce Add a help mesage for restrictions 2017-06-28 14:53:12 -05:00
phxntxm 393dc9d6a7 Add a message for if there are no restrictions 2017-06-28 05:30:06 -05:00
phxntxm 773e69e481 Await the coroutine 2017-06-28 05:28:41 -05:00
phxntxm 21d0dc275f Add a restrictions command to show current restrictions 2017-06-28 05:27:35 -05:00
phxntxm 3d43659ae9 Remove erroneous .name attribute lookup 2017-06-28 05:18:09 -05:00
phxntxm 08bfed110d Use already flattened attribute 2017-06-28 05:11:16 -05:00
phxntxm 575ff60590 Use already flattened attribute 2017-06-28 05:11:00 -05:00
phxntxm bca36e095b Ensure that message is given before checking for regex statement 2017-06-28 05:03:36 -05:00
phxntxm 94955be495 Correct erroneous attribute 2017-06-28 05:02:12 -05:00
phxntxm cc35fa377b Add the ability to set custom welcome/goodbye messages 2017-06-28 04:58:53 -05:00
phxntxm fe7f57e9bb Correct how to get from dict 2017-06-27 22:58:38 -05:00
phxntxm f4f4fb881e Use the battlerecord class for battlestats 2017-06-27 22:56:08 -05:00
phxntxm fd8dfb92bc Correct ranking comparisons 2017-06-27 22:48:52 -05:00
phxntxm 1c3cfc7443 Cleanup some of the messages not needed when unrestricting 2017-06-27 22:26:30 -05:00
phxntxm e794c95c50 Correct the conversion for the restriction options 2017-06-27 22:25:33 -05:00
phxntxm 39353ea6e7 Show rank changes when battles end 2017-06-27 22:05:00 -05:00
phxntxm fe25f93e61 Show rank changes when battles end 2017-06-27 22:02:44 -05:00
phxntxm 3c49b5ecd8 Show rank changes when battles end 2017-06-27 22:01:47 -05:00
phxntxm ebc4b0730f Correct match on rating changes 2017-06-27 21:09:16 -05:00
phxntxm c859377c32 Ensure updates after every battle 2017-06-27 21:08:25 -05:00
phxntxm 75dff5d273 Change the order of the db setting 2017-06-27 21:01:36 -05:00
phxntxm d1a1d5974d Add rating changes into the battle outcomes 2017-06-27 20:57:19 -05:00
phxntxm ab93c3f436 Add the restriction checking decorator to all commands 2017-06-27 19:26:32 -05:00
phxntxm da114970fb Return the future and the result of the future 2017-06-27 19:14:58 -05:00
phxntxm 5f6615d370 Add a convert method 2017-06-27 19:14:40 -05:00
phxntxm 49ac1ac6f3 Add a method to check our restrictions 2017-06-27 19:14:00 -05:00
phxntxm ea62d899bf Add the new check_restricted method 2017-06-27 19:13:38 -05:00
phxntxm 9f97f107bc Add a check for if the url given is None 2017-06-27 19:13:14 -05:00
phxntxm a404763c41 Add a way to list the songs in the current playlist 2017-06-27 19:12:49 -05:00
phxntxm a8b48bc718 Correct handling of no data returned 2017-06-27 19:12:29 -05:00
phxntxm 9b8e3f3875 Handle errors when downloading songs 2017-06-27 19:12:01 -05:00
phxntxm 0e5e9ab3d4 Support reasons for kick and bans 2017-06-27 19:11:22 -05:00
phxntxm 90930e547f Correct a couple text issues 2017-06-27 19:10:45 -05:00
phxntxm 7983c07a12 Return the future as well as the result, so that we can check the result of the future (that mouthful...) 2017-06-27 17:57:55 -05:00
phxntxm 7d66f3702f Remove some specifics; ignore all dot files 2017-06-27 16:15:06 -05:00
phxntxm 3c42bc4545 Added the ability to intuitively restrict some things 2017-06-27 16:14:39 -05:00
Phxntxm c1fb35a9d6 Correct a bit of logic when joining a raffle 2017-06-19 13:54:59 -05:00
phxntxm 345ad7a63c This was supposed to be addition not subtraction 2017-06-19 13:04:51 -05:00
Phxntxm 467cdb57d5 Use the correct form of voice 2017-06-18 13:48:10 -05:00
Phxntxm 601561fc94 Move when the response is lowered 2017-06-17 18:05:40 -05:00
Phxntxm 01ebe5bf21 Setup of raffle specific alerts 2017-06-17 18:04:46 -05:00
Phxntxm b769c64e01 Setup of Twitch specific alerts 2017-06-17 18:04:36 -05:00
Phxntxm b9efeefa89 Setup of picarto specific alerts 2017-06-17 18:04:25 -05:00
Phxntxm fd1fddc450 Split state and voice disconnections 2017-06-17 18:04:11 -05:00
Phxntxm d1662e1a82 Use default or welcome notifications overrides 2017-06-17 18:03:54 -05:00
Phxntxm d5c49b2347 Seperation of welcome alerts and default alerts 2017-06-17 18:03:26 -05:00
Phxntxm 0a7cfb42b7 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-06-16 20:58:11 -05:00
Phxntxm 43a289750d Add self to globals; catch when the content is too large to print 2017-06-16 17:04:26 -05:00
Phxntxm 7525bd1927 Silently catch internal server errors 2017-06-16 17:03:35 -05:00
phxntxm ce22d44a86 Change the creation of the refreshing task, since call_later can't be used with coroutines 2017-06-16 03:47:49 -05:00
Phxntxm 6ab89c1ba5 Remove errant class creations/correct conversion of string to int 2017-06-15 21:42:41 -05:00
Phxntxm e6efcf9042 Check if URL provided is None 2017-06-15 21:37:53 -05:00
Phxntxm df37b78718 Ensure the channel given exists 2017-06-15 21:30:12 -05:00
Phxntxm d5a5820983 Remove pending change (shouldn't have been commited) 2017-06-15 21:28:25 -05:00
Phxntxm a09452247e Ensure the channel given exists 2017-06-15 21:27:33 -05:00
phxntxm dc08406739 Use actual_load 2017-06-12 02:10:01 -05:00
phxntxm 9604d5a4ea Correct how the table_filter works 2017-06-12 02:08:31 -05:00
phxntxm e6bdf6c8f3 Add avatar back to picarto embeds 2017-06-12 01:48:54 -05:00
phxntxm 05e6d0404f Stupid aiohttp 2017-06-12 01:41:10 -05:00
phxntxm ec1df2dd3d Where did that go? 2017-06-12 01:32:59 -05:00
phxntxm e98f1504cc Correct a couple key changes 2017-06-12 00:48:31 -05:00
phxntxm 51f3696308 Use Picarto's new API 2017-06-12 00:46:07 -05:00
Phxntxm 6b805faf0f Reorder a few things in the next_song method too ensure we set the song 2017-06-11 14:30:09 -05:00
Phxntxm 0f2f9b628a Reorder a few things in the next_song method too ensure we set the song 2017-06-11 14:28:58 -05:00
phxntxm 14d6cc5e20 Correct syntax error 2017-06-11 13:03:34 -05:00
Phxntxm 1956d6e5c1 Edge cases~ 2017-06-10 21:58:20 -05:00
Phxntxm 8c1f23e59b Include an actual helpful example/result in order to not confuse dummies 2017-06-10 14:25:04 -05:00
Phxntxm 8abe27e85c Catch a few edge cases 2017-06-09 21:50:17 -05:00
Phxntxm eedcd9572a Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-06-09 21:43:24 -05:00
phxntxm 60e9348896 Lower the playlist and option names 2017-06-07 21:19:31 -05:00
phxntxm 3fb4d3f337 Only lower when needed 2017-06-07 20:05:38 -05:00
phxntxm 25c6a89e20 Don't check if current is set 2017-06-07 19:40:50 -05:00
phxntxm a65ffeb221 Check if there was a failure when adding a new song 2017-06-07 19:40:37 -05:00
phxntxm 534dd29f4f Few more edge cases 2017-06-07 15:56:51 -05:00
phxntxm ef531bd2d2 Ensure a response is given for the first question 2017-06-07 15:56:45 -05:00
phxntxm b0a965031f Use actual load as the filter we use is more complex to load from cache currently 2017-06-07 13:54:00 -05:00
phxntxm dbe5cf6110 Fix a few edge cases 2017-06-07 13:53:40 -05:00
phxntxm 7f2c542b3a Reenable battle 2017-06-07 13:53:27 -05:00
phxntxm 898fd8b7f4 Add a second check to ensure we are not playing before actually overwriting the song/dj 2017-06-07 03:53:15 -05:00
phxntxm 6779dfca03 Remove no longer needed print statements 2017-06-07 03:38:30 -05:00
phxntxm e43808f40c Reenable booping 2017-06-07 03:38:08 -05:00
phxntxm efc1a3cf6d Database rewrite/User queue creation 2017-06-07 03:30:19 -05:00
Phxntxm 893673aa94 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-06-01 20:26:54 -05:00
phxntxm 1cc88ce6ab Catch HTTPExceptions as this usually means an explicit word was sent 2017-05-29 16:33:31 -05:00
phxntxm 7c30ed595a PEP8 2017-05-29 16:29:49 -05:00
phxntxm 83e68a4c7f Handle if there aren't any nsfw channels on this server 2017-05-29 16:29:43 -05:00
phxntxm bf5f65b0b7 Handle if the avatar is too large 2017-05-29 16:27:25 -05:00
phxntxm d5b1f0a64d Handle when there are no songs in the queue 2017-05-29 16:27:12 -05:00
phxntxm 74896d921e Move joining a channel to it's own method 2017-05-29 16:17:00 -05:00
Phxntxm e7a4f58229 Ensure start_time exists 2017-05-21 17:16:36 -05:00
Phxntxm 9544ccc425 Set game in initialization; move db_check into a task on startup and not on_ready 2017-05-20 14:45:37 -05:00
Phxntxm 2e159cf7c4 Catch the other, not so common ExtractionError 2017-05-20 13:44:15 -05:00
Phxntxm ce6b831b93 Remoe the colour formatting from errors 2017-05-19 21:28:35 -05:00
Phxntxm 4178298065 Correct exception checking 2017-05-19 13:55:55 -05:00
Phxntxm aebaa9b282 Add another edge case check 2017-05-19 13:55:45 -05:00
Phxntxm 942e1b0a09 Add a method to send a message to the owner when the bot has left/joined a guild 2017-05-18 22:09:00 -05:00
Phxntxm 159efa6dee Use correct variable reference 2017-05-18 21:04:33 -05:00
Phxntxm 19507d06ba Remove old filter 2017-05-18 21:03:36 -05:00
Phxntxm 0f17624b58 Correct description 2017-05-18 21:03:20 -05:00
Phxntxm 13643bd1a2 Couple edge case checks 2017-05-18 21:03:09 -05:00
Phxntxm 32be71d4ec Correctly get examples, results, and the description 2017-05-18 21:03:00 -05:00
Phxntxm 38018674bc Use the qualified name for the commands 2017-05-18 20:45:24 -05:00
Phxntxm 11fae59047 Correct a couple old references 2017-05-18 20:44:28 -05:00
Phxntxm 6f4a5ee0ba Correct order of if statement 2017-05-18 20:42:42 -05:00
Phxntxm fcf88680ce Readd specific command help 2017-05-18 20:41:41 -05:00
Phxntxm 6d18799dd8 Don't try to connect again on failure; normally this is due to timeing out and we don't want to continiously time out 2017-05-18 14:02:29 -05:00
Phxntxm 47f0cfebfa Add a check for if there are no roles 2017-05-18 14:01:56 -05:00
Phxntxm dae3e4761e Reordered if statements to allow nsfw channels in DM's 2017-05-18 13:57:59 -05:00
phxntxm 4db5e57a98 Use the role hierarchy to list the roles in order 2017-05-16 23:02:52 -05:00
phxntxm a5daa026d5 Use the paginator 2017-05-16 22:47:33 -05:00
phxntxm 917ca70901 Added a check to ensure the song exists 2017-05-16 22:47:27 -05:00
Phxntxm 8860cd90a7 Add aliases; sort cogs 2017-05-15 18:36:39 -05:00
Phxntxm ccc70f0c4b Use the guild's voice client to disconnect 2017-05-15 16:19:04 -05:00
Phxntxm 38792b1a0b Make the extensions prettier 2017-05-14 20:08:13 -05:00
Phxntxm 184256c905 Add a polls cog 2017-05-14 20:07:04 -05:00
Phxntxm c519896715 Use the guild's voice client 2017-05-14 20:05:09 -05:00
Phxntxm 84c096df4b Ensure the bot.owner attribute has been set 2017-05-14 20:04:52 -05:00
Phxntxm e9354febc4 Force disconnection if we're stuck between connection states 2017-05-14 14:18:32 -05:00
phxntxm 97ae123c71 Reorder some parts with join, to ensure we detect the right status 2017-05-14 05:10:40 -05:00
Phxntxm dd9b3a4f1b Remove replacing of slashes 2017-05-13 17:16:27 -05:00
Phxntxm 29680724b0 Remove logging as library now logs this 2017-05-13 15:20:40 -05:00
Phxntxm 2a5e6b96b1 Ensure that the duration has been provided when using length 2017-05-13 14:23:32 -05:00
Phxntxm 2e1fd109f1 Add a few extra checks 2017-05-13 14:23:18 -05:00
Phxntxm ca8bcd8adf Make it clear that it is required to rename config.yml.sample to config.yml 2017-05-13 14:23:08 -05:00
Phxntxm a15e2862ee Fuyu approved 👍 2017-05-12 18:24:23 -05:00
Phxntxm 37e493b101 Update volume checking to use the state's property 2017-05-12 18:13:27 -05:00
Phxntxm d0842037f3 Add a second check for misinterpretation for searches as URL's 2017-05-12 17:53:45 -05:00
Phxntxm 7abcbe45d0 Replaced slashes with a space to not confuse URL's 2017-05-12 17:53:25 -05:00
Phxntxm d792f1d648 Add the ability to add a proxy to youtube_dl 2017-05-11 22:06:27 -05:00
Phxntxm 3e14ddfb89 Use a proxy if provided 2017-05-11 22:06:10 -05:00
Phxntxm 79ed786bb6 Remove some no longer required things from config 2017-05-11 21:40:21 -05:00
Phxntxm f1d588688a Correct how to get the github specific requirements 2017-05-11 21:35:36 -05:00
Phxntxm c6b534da0b Setup a requirements.txt file for easy installation use 2017-05-11 19:59:04 -05:00
Phxntxm 6277b5d78b Update readme to provide current installation procudere 2017-05-11 19:58:34 -05:00
Phxntxm 6d2917c31b Move extensions from config.yml to be hardcoded 2017-05-11 19:50:57 -05:00
Phxntxm 645bfef0b6 Use the actual application info to figure out owner 2017-05-11 19:43:43 -05:00
Phxntxm 0549f8134d Check if a playlist is provided 2017-05-11 18:25:37 -05:00
Phxntxm 86de528733 Update error method to match current version 2017-05-11 18:25:22 -05:00
Phxntxm e3e2a312d0 Update Converters to match current version 2017-05-11 18:23:47 -05:00
phxntxm aef72178a8 Add a first page and last page button to the queue 2017-05-09 18:21:12 -05:00
phxntxm 3b469fbe73 Rework debug command (Thanks Danny) 2017-05-09 18:21:05 -05:00
phxntxm 913ca3ce68 Correct which table to save in when updating prefix 2017-05-09 17:20:28 -05:00
Phxntxm 812988f00d Don't check if we're connected when disconnecting 2017-05-08 17:19:01 -05:00
Phxntxm f0ce98e731 Add default error catching, as well as logging these 2017-05-08 16:54:43 -05:00
Phxntxm 622be1e2ae Import discord 2017-05-08 16:54:24 -05:00
Phxntxm bfa859ac1e Add a check for nsfw channels, assume this channel is...nsfw 2017-05-07 21:50:28 -05:00
Phxntxm 75b0d2705f Convert error to string 2017-05-07 21:23:56 -05:00
Phxntxm 9fda74775c Syntax error 2017-05-07 21:22:14 -05:00
Phxntxm d61f2fa472 Get rid of youtube-dl's stupid colour formatting 2017-05-07 21:21:46 -05:00
Phxntxm 80c90f8d80 Use the right song when showing which one failed to download 2017-05-07 21:18:19 -05:00
Phxntxm 8f9e514260 Change entry to print an embed instead of a message 2017-05-07 21:16:51 -05:00
Phxntxm 1e451942be Print the song playing 2017-05-07 21:11:05 -05:00
Phxntxm 4cb478eb8e Send ctx instead of requester 2017-05-07 21:07:24 -05:00
Phxntxm 4ebfcf6613 Catch when videos fail to download 2017-05-07 21:05:37 -05:00
Phxntxm 064e182dde Don't add commands users can't run 2017-05-07 20:42:42 -05:00
Phxntxm e282e1e073 Remove vdebug command 2017-05-07 20:39:42 -05:00
Phxntxm 60b1510c5c Add guild to the environment variables 2017-05-07 20:39:34 -05:00
Phxntxm 5451395e0a Add an exception for some oddly formatted searches that trigger socket.gaierror 2017-05-07 20:38:24 -05:00
phxntxm 92514b6987 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-05-06 00:09:50 -05:00
phxntxm 97f1da001b Correct declining battle 2017-05-05 23:59:16 -05:00
phxntxm 24a7a64b07 Change how to check for pause/resume errors 2017-05-05 23:58:05 -05:00
Phxntxm 77848aef01 Add a full messagee 2017-05-05 15:58:17 -05:00
Phxntxm 0c30aca9d9 Add a check for if value has been provided 2017-05-05 15:55:29 -05:00
Phxntxm 005ac4179b Correct type for channel ID 2017-05-05 15:55:18 -05:00
Phxntxm 1d859e0995 Add a command to send a message to a particular channel 2017-05-05 15:53:12 -05:00
Phxntxm cbd522991a Rework volume to have a default volume set for every song that plays 2017-05-04 17:33:26 -05:00
Phxntxm bd7b5a5ca5 Add songs playing to info embed 2017-05-04 17:33:10 -05:00
Phxntxm 6dcaf2ef99 Remove erroneous comment 2017-05-04 17:33:01 -05:00
phxntxm bfb744bb29 remove erroneous alias 2017-05-04 02:44:51 -05:00
phxntxm 483698ea29 Renaming of assigning commands to make it more clear which does which 2017-05-04 02:44:10 -05:00
phxntxm a793378093 Correct naming of command 2017-05-04 02:34:33 -05:00
phxntxm aa610bd1d8 Add the ability to remove and list self-assignable roles 2017-05-04 02:33:24 -05:00
phxntxm 00b49f7445 Correct requirements for assign me 2017-05-04 01:30:07 -05:00
phxntxm 67dd73c0af Add a message for when connections timeout 2017-05-03 20:12:13 -05:00
phxntxm 7658241913 Add error messages when picarto URL is not saved 2017-05-03 15:31:28 -05:00
phxntxm e4ef3c075b Use better jokes 2017-05-02 19:29:14 -05:00
phxntxm 84746e23bb Correct how to get the font files 2017-05-02 19:19:28 -05:00
phxntxm 38cc78c09e Correct the element passed 2017-05-02 15:15:30 -05:00
phxntxm 85d1cdbe2d Move fonts used to a fonts folder in the repository 2017-05-02 14:38:32 -05:00
Phxntxm ab5d73a742 Move the valid_perms list to the correct cog 2017-05-01 18:31:33 -05:00
Phxntxm 3754fbc565 Put a newline in before the tictactoe board 2017-05-01 17:45:22 -05:00
Phxntxm 6aab8267f9 Correct how to get guild from a player provided 2017-05-01 16:01:53 -05:00
Phxntxm c295265e0b Correct how to get player1 2017-05-01 15:58:58 -05:00
Phxntxm 89129a5d88 Correct how to get player's battles in accept and decline 2017-05-01 15:57:32 -05:00
Phxntxm baa16d6e18 Remove erroneous variable 2017-05-01 15:49:26 -05:00
Phxntxm d7f3307e62 Correct how to get the guild 2017-05-01 15:48:29 -05:00
Phxntxm 86a8d57666 Correct syntax error 2017-05-01 15:46:59 -05:00
Phxntxm 43b32badc4 Correct syntax error 2017-05-01 15:46:36 -05:00
Phxntxm 0d66448338 Reformat battling, to allow more than one to be done at once 2017-05-01 15:45:56 -05:00
Phxntxm d516551de6 Import rethinkdb module 2017-05-01 14:07:39 -05:00
Phxntxm a12a18db14 Import glob 2017-04-30 19:16:42 -05:00
Phxntxm e574e2d532 Use the bot's clientuser's avatar url to not cause issues in PM 2017-04-30 19:05:11 -05:00
Phxntxm cc6c867306 Correct class to call in setup 2017-04-30 18:58:14 -05:00
Phxntxm 863903c765 Add missing init 2017-04-30 18:57:36 -05:00
Phxntxm 5aa87ce653 Reordering of commands to be grouped better 2017-04-30 18:56:02 -05:00
phxntxm 32f195b2f1 Add a check to ensure the source exists 2017-04-26 05:00:43 -05:00
Phxntxm 71772a4e19 Add a check for if the voice channel is None 2017-04-24 16:21:17 -05:00
Phxntxm ca8c3625a4 Send files with a filename 2017-04-24 14:25:05 -05:00
Phxntxm 6704af7508 Update images to use file-like objects to not take up disk space 2017-04-24 14:24:58 -05:00
Phxntxm e7133bc25f Correct volume implementation 2017-04-23 19:09:49 -05:00
Phxntxm ead82dc4aa Handle playing when voice is None 2017-04-23 17:28:13 -05:00
Phxntxm 90598f26bf Handle if the channel returned is None 2017-04-23 15:25:42 -05:00
Phxntxm 99ac6fdd6c Correct when the source is retrieved 2017-04-22 22:12:48 -05:00
Phxntxm 96e17e9cb6 Import required Exception 2017-04-22 22:09:03 -05:00
Phxntxm a30dae0735 Have the playlist handle searching for songs 2017-04-22 22:07:54 -05:00
Phxntxm c821adf87a Have the playlist handle searching for songs 2017-04-22 22:06:45 -05:00
Phxntxm 7e1ca280de Move the LiveStreamError to the exceptions module 2017-04-22 21:45:36 -05:00
Phxntxm 543c82a5c4 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-04-22 17:54:36 -05:00
Phxntxm ccee1dc046 Handle when live streams are attempted to be downloaded 2017-04-22 17:54:23 -05:00
phxntxm 43de52fae7 Change a few cases where voice of a state is still none 2017-04-22 00:29:58 -05:00
Phxntxm 6be0c4c93d Import regex module 2017-04-21 14:45:20 -05:00
Phxntxm 50cca0c7ff Add cpu/memory usage to info command 2017-04-20 22:04:48 -05:00
Phxntxm 6199d867dc Add cpu/memory usage to info command 2017-04-20 22:03:52 -05:00
Phxntxm 62c9c764d6 remove certain symbols that break song lookups 2017-04-20 19:13:57 -05:00
Phxntxm cc470c86db Handle if no there are no server settings 2017-04-20 18:41:35 -05:00
Phxntxm c34532cf09 Correct variable reference 2017-04-20 17:44:21 -05:00
Phxntxm 5d487a7f78 Add a check when creating a tag, to ensure that it doesn't already exist 2017-04-20 17:42:52 -05:00
Phxntxm 694c848517 Changed the orders of the errors for logical preference 2017-04-20 17:28:14 -05:00
phxntxm 34fae41208 Check if we're playing something when setting the volume 2017-04-19 22:45:11 -05:00
phxntxm 40b4977a32 Check if we're already connected to a channel 2017-04-19 22:43:24 -05:00
phxntxm cf4fc12831 Fix tabbing issue 2017-04-19 22:35:06 -05:00
phxntxm 173baaba14 Check for permissions when connecting 2017-04-19 22:32:18 -05:00
phxntxm e011b56873 PEP8 2017-04-19 22:18:26 -05:00
phxntxm 92f62c6ce8 Add a zws in order to not tag other commands 2017-04-19 22:18:20 -05:00
phxntxm b59b57f716 Finish music implementation on rewrite 2017-04-19 22:18:05 -05:00
Phxntxm 60f2b0e2e5 Add a pluck method 2017-04-16 22:49:35 -05:00
Phxntxm d485f64f25 Allow PM's to not be ignored 2017-04-16 20:58:20 -05:00
Phxntxm 7f48f11586 Add a sorted method 2017-04-16 20:58:11 -05:00
Phxntxm 53f9183ab6 Correct what is attempted to be converted to limit 2017-04-16 18:03:32 -05:00
Phxntxm 5ac2d6f8ce Correct syntax error 2017-04-16 17:58:05 -05:00
Phxntxm bfd3f75836 Intuitively handle limit 2017-04-16 17:52:38 -05:00
Phxntxm a21fcdeec5 Update avatar to send as png to support transparancy 2017-04-16 17:43:29 -05:00
Phxntxm ba24b24933 Add server to globals for convenience 2017-04-13 21:55:57 -05:00
Phxntxm 8181eca8dc Remove DA from the examples 2017-04-13 19:51:39 -05:00
Phxntxm 58b8a0d125 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-04-13 18:42:36 -05:00
Phxntxm 46e28b81a1 Update to check if a message should be ignored 2017-04-13 18:30:14 -05:00
phxntxm ac8689881d Check if a message should be ignored 2017-04-11 00:22:11 -05:00
Phxntxm 38133ad8bb Add the ability to ignore channels/members 2017-04-10 20:26:28 -05:00
Phxntxm e531d0ab4d Correct issue where forbidden_tags were put in the wrong place 2017-04-10 19:44:10 -05:00
Phxntxm 71a9512888 Catch when a raffle is running in a server the bot is no longer in 2017-04-09 21:13:34 -05:00
Phxntxm f69b947cfb Correct region retrieving 2017-04-09 17:37:00 -05:00
Phxntxm 78b511c867 Compare the channel instead of the ID 2017-04-09 17:32:50 -05:00
Phxntxm 79994c6cf4 Convert files to the new discord.File format 2017-04-08 22:16:12 -05:00
Phxntxm 74d22f2d8d Update no_pm to use the decorator instead 2017-04-08 22:04:46 -05:00
Phxntxm 6029797f1c Removed strawpoll 2017-04-08 22:04:36 -05:00
Phxntxm 3f2f7e77a3 Catch failures to send messages on exceptions 2017-04-08 21:52:10 -05:00
Phxntxm f99afec44f Catch not being able to send a message 2017-04-08 21:51:39 -05:00
Phxntxm c4b29224a6 Catch twitch returning no information 2017-04-08 21:51:22 -05:00
Phxntxm a45f37324e Catch if a user has no top scores 2017-04-06 17:09:25 -05:00
Phxntxm e0136bce2f Catch if a user has no top scores 2017-04-06 17:08:40 -05:00
Phxntxm 66bafe381a Forbid a couple of things that can get tags and hangman stuck 2017-04-06 17:01:35 -05:00
Phxntxm 063d64e86c Forbid a couple of things that can get tags and hangman stuck 2017-04-06 16:53:37 -05:00
Phxntxm d0f96db9b5 Disallow prefixes over 20 characters 2017-04-06 16:41:31 -05:00
Phxntxm f31dc9c24b Correct issue where an invalid attribute is being referenced 2017-04-01 22:07:47 -05:00
Phxntxm 503f710a4b Set the filename as the correct extension 2017-04-01 16:12:44 -05:00
Phxntxm 66cba8a0a7 Correct how to print the time left 2017-03-27 22:11:36 -05:00
Phxntxm 5631913021 Fix a couple issues in how roulette functions 2017-03-27 22:06:05 -05:00
Phxntxm 18a36ad9a0 Correct reference to the start time 2017-03-27 21:57:31 -05:00
Phxntxm 08980f4cbb Add a setup method 2017-03-27 21:56:20 -05:00
Phxntxm 24a23f1277 Correct syntax error 2017-03-27 21:55:21 -05:00
Phxntxm 7f2db7b3a6 Add a roulette command 2017-03-27 21:54:14 -05:00
Phxntxm e8b725484a Add a check to ensure that we have online channels 2017-03-27 21:27:17 -05:00
Phxntxm 26bd3931a6 Catch if a user has no server tags 2017-03-27 18:33:41 -05:00
Phxntxm dd3742a318 Call strip 2017-03-27 18:31:42 -05:00
Phxntxm bf7326dccb Add a mytags command to show all of a user's tags on the server 2017-03-27 18:30:58 -05:00
Phxntxm bc0aac4009 Add another lower/strip in case old tags aren't 2017-03-27 18:24:53 -05:00
Phxntxm a77d22effc Lower and strip triggers for tags on creation and usage 2017-03-27 18:23:09 -05:00
Phxntxm 1296ab6d39 Disallow tags to be created with the same name as subcommands 2017-03-27 17:48:27 -05:00
Phxntxm 989d909c2f Update how message's are edited to match rewrite 2017-03-27 14:37:58 -05:00
Phxntxm 37a5fb3701 Update the way channels are compared to work with rewrite 2017-03-27 13:57:35 -05:00
Phxntxm 20734739a8 Convert list comp to normal for loop 2017-03-26 22:13:36 -05:00
Phxntxm f6d143dd79 Await the coroutine 2017-03-26 22:12:21 -05:00
Phxntxm 586e0e45f8 Don't include commands that a user can't run 2017-03-26 21:10:08 -05:00
Phxntxm 75029b9abb Import asyncio so timeout's can be handled 2017-03-26 21:00:36 -05:00
Phxntxm 86169750db Update hangman to accept custom created hangman phrases again 2017-03-26 18:57:52 -05:00
Phxntxm 69c7562c58 Correct issue where the type of channel wasn't being checked 2017-03-26 13:46:51 -05:00
Phxntxm 6af06cfa30 Add a message if you try to edit a non-existing tag 2017-03-25 23:02:47 -05:00
Phxntxm b94a2438b1 Correct syntax error 2017-03-25 23:00:30 -05:00
Phxntxm 5e0a4a763a Correct syntax error 2017-03-25 23:00:07 -05:00
Phxntxm e1b8bdcbe9 Added a tag edit command 2017-03-25 22:59:08 -05:00
Phxntxm 5cf232edf6 Don't paginate if there are no tags 2017-03-25 22:46:36 -05:00
Phxntxm 5f9283fff9 Actually remove the tag from the database 2017-03-25 22:45:39 -05:00
Phxntxm 29242efdf0 Correct reference to the tag removed 2017-03-25 22:42:56 -05:00
Phxntxm 2a82a7d78a Change server to guild 2017-03-25 22:41:54 -05:00
Phxntxm 0968284ede Implement the new tag setup 2017-03-25 22:39:18 -05:00
Phxntxm ea8535a647 Add a score requirement to filter out some terrible posts on derpi 2017-03-25 21:06:36 -05:00
Phxntxm 93a245ffa5 Correct subcommands retrieving 2017-03-25 20:27:10 -05:00
Phxntxm 3edd9b64de Update to match the new commands format 2017-03-25 19:20:30 -05:00
Phxntxm 0806cdadb2 Add a repl command 2017-03-25 18:37:12 -05:00
Phxntxm 30df3be94a Catch an invalid response in some cases 2017-03-24 15:37:23 -05:00
Phxntxm a5d8e849aa Catch an invalid nickname being passed 2017-03-24 15:37:03 -05:00
Phxntxm 4a7eaaf4da Correct issue where _request was added to the keys, causing an invalid region to be detected 2017-03-24 15:36:47 -05:00
Phxntxm 8e2c943d24 Correct issue where DM Channels were not detected 2017-03-24 14:14:13 -05:00
Phxntxm e985eae89a Correct issue where DM Channels were not detected 2017-03-24 14:13:46 -05:00
Phxntxm 078f1bbdf4 Add a command to send a random dog picture 2017-03-23 22:11:22 -05:00
Phxntxm 11142f9fa8 Add a command to send a random cat picture 2017-03-23 22:02:56 -05:00
Phxntxm a5b21eaa68 Handle if someone has battled someone then left the server 2017-03-23 22:02:42 -05:00
phxntxm 13f2b89e81 Handle no server settings 2017-03-22 22:21:59 -05:00
phxntxm 788743de25 Update to allow nsfw channel management in DM 2017-03-22 22:21:34 -05:00
Phxntxm 66099fe6b1 Use self to get bot 2017-03-20 17:27:34 -05:00
Phxntxm d0bd554b04 Use get_member instead of a generator that includes every member 2017-03-20 17:26:26 -05:00
Phxntxm e9a4d5aee8 Use bot.users instead of the generator, to stop the blocking call 2017-03-20 17:24:41 -05:00
Phxntxm 9cccbdb8de Comment out blocking call temporarily 2017-03-20 16:56:05 -05:00
Phxntxm 11cee7e7a4 Ensure more than one string is provided 2017-03-20 16:18:29 -05:00
Phxntxm 6de148a1c7 Remove logging for rethinkdb instance open/close 2017-03-19 22:48:22 -05:00
Phxntxm 748bc4f374 If there are no server settings at all, assume no rules 2017-03-19 22:13:14 -05:00
Phxntxm cbc29c226f Fixed an issue where cache would update even on a get request, causing an infinite loop 2017-03-19 22:11:40 -05:00
Phxntxm 67d6c43e96 Fixed an issue where cache would update even on a get request, causing an infinite loop 2017-03-19 22:11:14 -05:00
Phxntxm c0c135fb3d Compare the ID instead of the message instance 2017-03-19 21:47:06 -05:00
Phxntxm 5c90d3e7fa Start logging rethinkdb open/close as there appears to be stuck connections 2017-03-19 21:46:54 -05:00
Phxntxm f2ad84e0c5 Trigger typing for stats that show images as those can take a while to format/send 2017-03-19 18:04:52 -05:00
Phxntxm 8f4421e6a1 Catch if we can't connect to OW API 2017-03-19 18:02:34 -05:00
Phxntxm c2b20a9a29 Correct how to check if a channel is a DM Channel 2017-03-19 17:43:42 -05:00
Phxntxm e158d18c4f Silently catch unable to send message errors 2017-03-19 17:43:28 -05:00
Phxntxm ae46593ec9 Correct issue where a member couldn't be found due to comparing a string and an int 2017-03-19 17:24:10 -05:00
Phxntxm 7d54882549 Convert to string for polls 2017-03-16 15:19:48 -05:00
phxntxm 209e1522f1 Add a possible error that can be caught 2017-03-15 22:36:00 -05:00
phxntxm 0c25f4ab6f Use get in case the member doesn't exist 2017-03-15 22:35:44 -05:00
phxntxm 8f488654c5 Pep8 2017-03-15 22:35:32 -05:00
phxntxm efcd7ad251 Ensure the message responded to is the one we want 2017-03-15 21:29:51 -05:00
phxntxm 2c5c69cfce Accept the correct parameters 2017-03-15 21:22:02 -05:00
phxntxm 94c5ef1873 Merge branch 'rewrite' of https://github.com/Phxntxm/Bonfire into rewrite 2017-03-15 14:10:35 -05:00
Phxntxm a88ee2972b Correct how to check if a channel is online 2017-03-12 21:56:24 -05:00
Phxntxm e0534d1d5b Make a call to get the online channels 2017-03-12 21:54:00 -05:00
Phxntxm c1c1a7c20f Updated picarto to work with rewrite 2017-03-12 21:51:00 -05:00
Phxntxm 9caf781289 Correctly handle str/int conversions 2017-03-12 17:14:15 -05:00
Phxntxm 582e33d907 Correctly handle str/int conversions 2017-03-12 17:08:49 -05:00
Phxntxm 22c4b57991 Handle if no server_settings are set 2017-03-12 17:07:42 -05:00
Phxntxm dcfbc0a3f0 Update some features to work with rewrite 2017-03-12 17:05:47 -05:00
Phxntxm 630a11c1cf Rewrite the channel loop for twitch 2017-03-12 16:59:56 -05:00
Phxntxm 1473dc9e67 Convert id's to int 2017-03-12 16:46:43 -05:00
Phxntxm 1e535a3842 Update checks to not create the deviant art table 2017-03-12 16:21:08 -05:00
Phxntxm 7a41730f81 Not supporting DA anymore 2017-03-12 15:38:43 -05:00
Phxntxm 6007237f10 Added a da list command 2017-03-11 21:35:56 -06:00
Phxntxm 9311ac78a6 Use get in case a notification channel is not set 2017-03-11 21:35:48 -06:00
Phxntxm e3e1474d38 Add scores command 2017-03-10 21:05:49 -06:00
Phxntxm a61f79004e Added a detailed pages paginator 2017-03-10 21:05:42 -06:00
Phxntxm ca7967057d Remove adding a thumbnail because osu hides this behind a base64 encode, and discord handles the image downloading manually 2017-03-10 19:02:41 -06:00
Phxntxm 2a966124c3 Use nice formatting on the embed for osu 2017-03-10 18:40:26 -06:00
Phxntxm 7459ac1955 Correct a couple issues with osu 2017-03-10 18:16:29 -06:00
Phxntxm 45ab645e1a Correct a couple issues with osu 2017-03-10 18:15:23 -06:00
Phxntxm 78a46b2482 Correct a couple issues with osu 2017-03-10 18:11:21 -06:00
Phxntxm 9e86472b64 First implementation for the new osu for rewrite 2017-03-10 18:07:39 -06:00
Phxntxm 8699b0e303 Add the uptime check to the uptime command itself 2017-03-10 16:19:18 -06:00
Phxntxm 67d717cff7 Add a check to ensure uptime exists before adding it 2017-03-10 14:38:29 -06:00
Phxntxm c49bae11a8 Add a check to ensure the message is not deleted as we're waiting for a reaction 2017-03-10 14:25:37 -06:00
Phxntxm 00c8f2509f Trigger typing when looking up twitch/picarto API 2017-03-09 22:37:43 -06:00
Phxntxm 191a48ce9f Pretty print the title 2017-03-09 22:36:29 -06:00
Phxntxm 6a6ae6683d Don't include at all if there is no value 2017-03-09 22:35:22 -06:00
Phxntxm e5e1ef7638 Add a zero-width space if there is no value to use 2017-03-09 22:33:27 -06:00
Phxntxm 6928c13cfd Corrected the name of social links 2017-03-09 22:28:34 -06:00
Phxntxm d6b1b8bc49 Changed picarto to use embeds 2017-03-09 22:27:52 -06:00
Phxntxm 5c20112ff3 Correct issue where the api URL shadowed the picarto URL 2017-03-09 22:22:44 -06:00
Phxntxm 2b1d34707a Update to work with rewrite 2017-03-09 16:28:08 -06:00
phxntxm 606f9c1d27 Fix turning off notifications for twitch 2017-03-08 15:18:45 -06:00
phxntxm b93f104965 Change positional argument to kwarg 2017-03-08 15:13:54 -06:00
phxntxm 78352bea85 Update twitch to use an embed 2017-03-08 15:12:40 -06:00
phxntxm 9e057dbc07 Stopped searching for the one indexed list on get 2017-03-08 15:04:10 -06:00
phxntxm fd5d3f1538 Correct the key searching for twitch 2017-03-08 15:03:13 -06:00
phxntxm 993ad7ce43 Update to match rewrite 2017-03-08 14:59:05 -06:00
phxntxm fa8d03c6c2 removed the message sent, as we're now using typing 2017-03-08 14:50:29 -06:00
phxntxm 4e09b7a737 Correct int/str conversion 2017-03-08 14:04:16 -06:00
phxntxm 1a2a1f070c Correct int/str conversion 2017-03-08 13:43:15 -06:00
phxntxm 81b0500733 Fixed typo 2017-03-08 13:37:46 -06:00
phxntxm 17c853caae Update to work with rewrite 2017-03-08 13:36:11 -06:00
phxntxm d43c854584 Corrected an issue with changing the status/name 2017-03-08 13:25:52 -06:00
phxntxm 16aaf3f01b Fixed rules adding, fixed prune, caused purge to start before the command that triggered it 2017-03-08 02:44:27 -06:00
phxntxm 286af4e356 Looked at the right key... 2017-03-08 02:40:55 -06:00
phxntxm 1800c9ab5c Corrected how to get content from cache 2017-03-08 02:38:26 -06:00
phxntxm b0a6328b57 Corrected how to check if a command can have permissions setup on it 2017-03-08 02:33:18 -06:00
phxntxm 1d3ab2fb20 Updated manage_server to manage_guild 2017-03-08 02:28:57 -06:00
phxntxm a22cf3e414 Corrected some issues with the database 2017-03-08 02:26:50 -06:00
phxntxm ac23687696 Corrected what http client to retrieve 2017-03-08 02:22:26 -06:00
phxntxm 38268f4886 Added a possible exception 2017-03-08 02:19:47 -06:00
phxntxm d8a053eb67 Changed the converter to work with the rewrite 2017-03-08 02:18:28 -06:00
phxntxm 85e70c9a11 Removed unneeded clientsessions 2017-03-08 02:08:01 -06:00
phxntxm ffadb4a634 Removed key from parameters 2017-03-08 02:02:01 -06:00
phxntxm 6ed044c1cc Convert all comparisions to the db to string, as rethinkdb fails to parse ints as keys, or long ints in general 2017-03-08 01:55:22 -06:00
phxntxm 18e5648b41 Cast ID to a str 2017-03-08 01:29:05 -06:00
phxntxm e482f671d8 Stopped trying to pull from a one indexed list, as we no longer use that in key'd tables 2017-03-08 01:20:42 -06:00
phxntxm 17b9e0253b Stopped trying to pull from a one indexed list, as we no longer use that in key'd tables 2017-03-08 01:19:58 -06:00
phxntxm 061562b60d Convert to a string for id's when searching for command stats 2017-03-08 00:51:17 -06:00
phxntxm ca5bcbdc80 Set the content to the cursor if a key is given 2017-03-08 00:44:58 -06:00
phxntxm 58eb623a1a Stopped trying to pull from a one indexed list, as we no longer use that in key'd tables 2017-03-08 00:43:40 -06:00
phxntxm 7a3e7afe33 Don't convert if we're given a dict, aka using get 2017-03-08 00:32:55 -06:00
phxntxm b3976c9d8a Convert int ID to a string, as that is required by rethinkdb 2017-03-08 00:28:43 -06:00
phxntxm 69a92431d8 Removed not required string conversions 2017-03-08 00:27:03 -06:00
phxntxm 0823c19b94 Convert int ID to a string, as that is required by rethinkdb 2017-03-08 00:25:17 -06:00
phxntxm 1e2f44d494 Corrected a ctrl+f incorrect change 2017-03-08 00:23:16 -06:00
phxntxm bb08575b98 Added the rethink connection to run on 2017-03-08 00:22:32 -06:00
phxntxm 365501bef2 Added a possible exception to catch 2017-03-08 00:21:11 -06:00
phxntxm c6d6661204 Added a possible exception to catch 2017-03-08 00:20:23 -06:00
phxntxm 78187197b3 Added a possible exception to catch 2017-03-08 00:19:42 -06:00
phxntxm f91e992d25 Corrected the commenting method to python (stupid php....) 2017-03-08 00:16:11 -06:00
phxntxm 4018db2dac Added a battle outcome 2017-03-08 00:15:52 -06:00
phxntxm 67d8fedef8 Updated for rewrite: Batch 6 2017-03-07 22:28:30 -06:00
phxntxm 2b9d8a2dca Update for rewrite: Batch 5 2017-03-07 17:56:24 -06:00
phxntxm d82127fa38 Update for rewrite: Batch 4 2017-03-07 17:47:00 -06:00
phxntxm 2852576250 Corrected the event to wait for 2017-03-07 17:32:40 -06:00
phxntxm d9c654f73d Check if the returned content is none first 2017-03-07 17:32:33 -06:00
phxntxm c6c7fbf73b Updated server to guild 2017-03-07 17:28:07 -06:00
phxntxm b99318f51b Only accept the context on command completion 2017-03-07 17:26:13 -06:00
phxntxm 16b9dc36c7 Add a check in case there are no custom permissions 2017-03-07 17:24:02 -06:00
phxntxm 82a26df045 Changed the check for a private channel to look for the type 2017-03-07 17:22:10 -06:00
phxntxm 3543d205f2 Update for rewrite: Batch 3 2017-03-07 17:18:18 -06:00
phxntxm a05b768274 Updated for rewrite: Batch 2 2017-03-07 16:35:30 -06:00
Phxntxm 23e69a01c6 Update to work with the rewrite: Batch 1 2017-03-05 20:45:44 -06:00
Phxntxm 4b4649b0de Updated to work with the rewrite 2017-03-05 14:40:00 -06:00
101 changed files with 6826 additions and 5514 deletions

11
.gitignore vendored
View file

@ -1,24 +1,15 @@
config.yml
config.json
.idea
__pycache__
nohup.out
restartcron.sh
GitAutoDeploy.conf.json
.htaccess
autoupdate.php
error_log
images/banner/tmp/*
.ftpquota
bonfire.log
*.sublime-*
cert.pem
.bash*
.cache
.python_history
.config/
audio_tmp/*
.viminfo
docs/_build
.ssh/*
.gitconfig
.*

View file

@ -1,47 +1,36 @@
# Bonfire
This is for a Discord bot using the discord.py wrapper made for fun, used in a couple of my own servers.
![fuyu approved](https://img.shields.io/badge/fuyu-approved-green.svg)
This is for a Discord bot using the discord.py wrapper made for fun, used in a couple of my own servers that somehow got popular I guess?
If you'd like to add this bot to one of your own servers, please visit the following URL:
https://discordapp.com/oauth2/authorize?client_id=183748889814237186&scope=bot&permissions=0
This requires the discord.py library, as well as all of its dependencies.
https://github.com/Rapptz/discord.py
To save the data for the bot, rethinkdb is what is used:
https://www.rethinkdb.com/docs/install/
I also use a few libraries that aren't included by default, which can be installed using pip.
I will not assist with, nor provide instructions on the setup for rethinkdb.
In order to install the requirements for Bonfire you will first need to install python3.5. Once that is installed, run the following (replacing python with the correct executable based on your installation):
```
python3.5 -m pip install discord.py[voice] BeautifulSoup4 youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline
# Or on windows
py -3 -m pip install discord.py[voice] BeautifulSoup4 youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline
#NOTE: To use the requirements.txt file, you need to be in the installation directory for this bot.
python -m pip install --upgrade -r requirements.txt
```
Note: ATM of writing this, Pillow 3.4.2 (the stable version...good job Pillow?) is broken, do not use pip's default to install this. This is why we're using Pillow==3.4.1 above, and not just Pillow
The joke command requires the fortune-mod package, which are installable on Linux and other Unix-based distros.
Debian, Ubuntu, etc.:
```
sudo apt install fortune-mod
```
If you're on Red Hat, Fedora, etc., replace ``apt`` with ``yum``. If you're on another Linux distro, I trust you know what package manager to use. If you're on Windows, you might want to check out [this guide](http://superuser.com/questions/683162/bsd-fortune-for-windows-command-prompt-or-dos), but you're on your own.
The only required file to modify would be the config.yml.sample file. The entries are as follows:
The only required file to modify would be the config.yml.sample file. Copy this file to config.yml and edit the entries as needed; the entries are as follows:
- bot_token: The token that can be retrieved from the [bot's application page](https://discordapp.com/developers/applications/me)
- owner_id: This is your ID, which can be retrieved by right clicking your name in the discord application, when developer mode is on
- description: Self explanotory, the description for the bot
- description: Self explanatory, the description for the bot
- command_prefix: A list of the prefixes you want the bot to respond to, if none is provided in the config file ! will be used
- default_status: The default status to use when the bot is booted up, which will populate the "game" that the bot is playing
- discord_bots_key: The key for the [bots.discord.pw site](https://bots.discord.pw/#g=1), if you don't have a key just leave it blank, it should fail and log the failure
- carbon_key: The key used for the [carbonitex site](https://www.carbonitex.net/discord/bots)
- twitch_key: The twitch token that is used for the API calls
- youtube_dl_proxy: A URL that can be used to proxy the calls youtube_dl goes through. Useful if your server is in a location that has many youtube videos blocked.
- youtube_key: The key used for youtube API calls
- osu_key: The key used for Osu API calls
- shard_count: This is the number of shards the bot is split over. 1 needs to be used if the bot is not being sharded
- shard_id: This will be the ID of the shard in particular, 0 if sharding is not used
- extensions: This is a list of the extensions loaded into the bot (check the cogs folder for the extensions available). The disabled playlist is a special entry....read that file for what its purpose is....most likely you will not need it. Entries in this list need to be separated by ", " like in the example.
- db_*: This is the information for the rethinkdb database. The cert is the certificate used for driver connections
- db_*: This is the information for the rethinkdb database.

211
bot.py
View file

@ -1,4 +1,3 @@
#!/usr/local/bin/python3.5
import discord
import traceback
import logging
@ -10,120 +9,124 @@ import aiohttp
os.chdir(os.path.dirname(os.path.realpath(__file__)))
from discord.ext import commands
from cogs import utils
import utils
opts = {'command_prefix': utils.command_prefix,
'description': utils.bot_description,
'pm_help': None,
'shard_count': utils.shard_count,
'shard_id': utils.shard_id,
'command_not_found': ''}
opts = {
'command_prefix': utils.command_prefix,
'description': utils.bot_description,
'pm_help': None,
'command_not_found': '',
'activity': discord.Activity(name=utils.default_status, type=0)
}
bot = commands.Bot(**opts)
logging.basicConfig(level=logging.WARNING, filename='bonfire.log')
bot = commands.AutoShardedBot(**opts)
logging.basicConfig(level=logging.INFO, filename='bonfire.log')
@bot.event
async def on_ready():
# Change the status upon connection to the default status
await bot.change_presence(game=discord.Game(name=utils.default_status, type=0))
if not hasattr(bot, 'uptime'):
bot.uptime = pendulum.utcnow()
await utils.db_check()
@bot.event
async def on_message(message):
if message.author.bot:
return
await bot.process_commands(message)
@bot.event
async def on_command_completion(command, ctx):
# There's no reason to continue waiting for this to complete, so lets immediately launch this in a new future
bot.loop.create_task(process_command(ctx))
async def process_command(ctx):
author = ctx.message.author
server = ctx.message.server
command = ctx.command
command_usage = await utils.get_content('command_usage', key=command.qualified_name)
if command_usage is None:
command_usage = {'command': command.qualified_name}
# Add one to the total usage for this command, basing it off 0 to start with (obviously)
total_usage = command_usage.get('total_usage', 0) + 1
command_usage['total_usage'] = total_usage
# Add one to the author's usage for this command
total_member_usage = command_usage.get('member_usage', {})
member_usage = total_member_usage.get(author.id, 0) + 1
total_member_usage[author.id] = member_usage
command_usage['member_usage'] = total_member_usage
# Add one to the server's usage for this command
if ctx.message.server is not None:
total_server_usage = command_usage.get('server_usage', {})
server_usage = total_server_usage.get(server.id, 0) + 1
total_server_usage[server.id] = server_usage
command_usage['server_usage'] = total_server_usage
# Save all the changes
if not await utils.update_content('command_usage', command_usage, command.qualified_name):
await utils.add_content('command_usage', command_usage)
@bot.event
async def on_command_error(error, ctx):
if isinstance(error, commands.CommandNotFound):
return
if isinstance(error, commands.DisabledCommand):
return
@bot.before_invoke
async def start_typing(ctx):
try:
if isinstance(error.original, discord.Forbidden):
return
elif isinstance(error.original, discord.HTTPException) and 'empty message' in str(error.original):
return
elif isinstance(error.original, aiohttp.ClientOSError):
return
except AttributeError:
await ctx.trigger_typing()
except (discord.Forbidden, discord.HTTPException):
pass
if isinstance(error, commands.BadArgument):
fmt = "Please provide a valid argument to pass to the command: {}".format(error)
await bot.send_message(ctx.message.channel, fmt)
elif isinstance(error, commands.CheckFailure):
fmt = "You can't tell me what to do!"
await bot.send_message(ctx.message.channel, fmt)
elif isinstance(error, commands.CommandOnCooldown):
m, s = divmod(error.retry_after, 60)
fmt = "This command is on cooldown! Hold your horses! >:c\nTry again in {} minutes and {} seconds" \
.format(round(m), round(s))
await bot.send_message(ctx.message.channel, fmt)
elif isinstance(error, commands.NoPrivateMessage):
fmt = "This command cannot be used in a private message"
await bot.send_message(ctx.message.channel, fmt)
elif isinstance(error, commands.MissingRequiredArgument):
await bot.send_message(ctx.message.channel, error)
else:
now = datetime.datetime.now()
with open("error_log", 'a') as f:
print("In server '{0.message.server}' at {1}\nFull command: `{0.message.content}`".format(ctx, str(now)),
file=f)
try:
traceback.print_tb(error.original.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error.original), file=f)
except:
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
@bot.event
async def on_command_completion(ctx):
author = ctx.author.id
guild = ctx.guild.id if ctx.guild else None
command = ctx.command.qualified_name
await bot.db.execute(
"INSERT INTO command_usage(command, guild, author) VALUES ($1, $2, $3)",
command,
guild,
author
)
# Now add credits to a users amount
# user_credits = bot.db.load('credits', key=ctx.author.id, pluck='credits') or 1000
# user_credits = int(user_credits) + 5
# update = {
# 'member_id': str(ctx.author.id),
# 'credits': user_credits
# }
# await bot.db.save('credits', update)
@bot.event
async def on_command_error(ctx, error):
error = error.original if hasattr(error, "original") else error
ignored_errors = (
commands.CommandNotFound,
commands.DisabledCommand,
discord.Forbidden,
aiohttp.ClientOSError,
commands.CheckFailure,
commands.CommandOnCooldown,
)
if isinstance(error, ignored_errors):
return
elif isinstance(error, discord.HTTPException) and (
'empty message' in str(error) or
'INTERNAL SERVER ERROR' in str(error) or
'REQUEST ENTITY TOO LARGE' in str(error) or
'Unknown Message' in str(error) or
'Origin Time-out' in str(error) or
'Bad Gateway' in str(error) or
'Gateway Time-out' in str(error) or
'Explicit content' in str(error)):
return
elif isinstance(error, discord.NotFound) and 'Unknown Channel' in str(error):
return
try:
if isinstance(error, commands.BadArgument):
fmt = "Please provide a valid argument to pass to the command: {}".format(error)
await ctx.message.channel.send(fmt)
elif isinstance(error, commands.NoPrivateMessage):
fmt = "This command cannot be used in a private message"
await ctx.message.channel.send(fmt)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.message.channel.send(error)
elif isinstance(error, (
commands.InvalidEndOfQuotedStringError,
commands.ExpectedClosingQuoteError,
commands.UnexpectedQuoteError)
):
await ctx.message.channel.send("Quotes must go around the arguments you want to provide to the command,"
" recheck where your quotes are")
else:
if isinstance(bot.error_channel, int):
bot.error_channel = bot.get_channel(bot.error_channel)
if bot.error_channel is None:
now = datetime.datetime.now()
with open("error_log", 'a') as f:
print("In server '{0.message.guild}' at {1}\n"
"Full command: `{0.message.content}`".format(ctx, str(now)), file=f)
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
else:
await bot.error_channel.send(f"""```
Command = {discord.utils.escape_markdown(ctx.message.clean_content).strip()}
{''.join(traceback.format_tb(error.__traceback__)).strip()}
{error.__class__.__name__}: {error}```""")
except discord.HTTPException:
pass
if __name__ == '__main__':
bot.remove_command('help')
# Setup our bot vars, db and cache
bot.db = utils.DB()
bot.cache = utils.Cache(bot.db)
bot.error_channel = utils.error_channel
# Start our startup task (cache sets up the database, so just this)
bot.loop.create_task(bot.cache.setup())
for e in utils.extensions:
bot.load_extension(e)
bot.uptime = pendulum.now(tz="UTC")
bot.run(utils.bot_token)

584
cogs/admin.py Normal file
View file

@ -0,0 +1,584 @@
import discord
import utils
from asyncpg import UniqueViolationError
from discord.ext import commands
valid_perms = list(discord.Permissions.VALID_FLAGS.keys())
class Admin(commands.Cog):
"""These are commands that allow more intuitive configuration, that don't fit into the config command"""
@commands.command()
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def disable(self, ctx, *, command):
"""Disables the use of a command on this server"""
if command == "disable" or command == "enable":
return await ctx.send("You cannot disable `{}`".format(command))
cmd = ctx.bot.get_command(command)
if cmd is None:
return await ctx.send("No command called `{}`".format(command))
try:
await ctx.bot.db.execute(
"INSERT INTO restrictions (source, destination, from_to, guild) VALUES ($1, 'everyone', 'from', $2)",
cmd.qualified_name,
ctx.guild.id
)
except UniqueViolationError:
await ctx.send(f"{cmd.qualified_name} is already disabled")
else:
await ctx.send(f"{cmd.qualified_name} is now disabled")
ctx.bot.cache.add_restriction(
ctx.guild,
"from",
{"source": cmd.qualified_name, "destination": "everyone"}
)
@commands.command()
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def enable(self, ctx, *, command):
"""Enables the use of a command on this server"""
cmd = ctx.bot.get_command(command)
if cmd is None:
await ctx.send("No command called `{}`".format(command))
return
query = f"""
DELETE FROM restrictions WHERE
source=$1 AND
from_to='from' AND
destination='everyone' AND
guild=$2
"""
await ctx.bot.db.execute(query, cmd.qualified_name, ctx.guild.id)
ctx.bot.cache.remove_restriction(
ctx.guild,
"from",
{"source": cmd.qualified_name, "destination": "everyone"}
)
await ctx.send(f"{cmd.qualified_name} is no longer disabled")
@commands.command()
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def notify(self, ctx, role: discord.Role, *, message):
"""
Notify everyone in "role" with "message"
This sets the role to mentionable, mentions the role, then sets it back
"""
if not ctx.me.guild_permissions.manage_roles:
await ctx.send("I do not have permissions to edit roles (this is required to complete this command)")
return
try:
await role.edit(mentionable=True)
except discord.Forbidden:
await ctx.send("I do not have permissions to edit that role. "
"(I either don't have manage roles permissions, or it is higher on the hierarchy)")
else:
fmt = f"{role.mention}\n{message}"
await ctx.send(fmt)
await role.edit(mentionable=False)
await ctx.message.delete()
@commands.command()
@commands.guild_only()
@utils.can_run(kick_members=True)
async def restrictions(self, ctx):
"""Used to list all the current restrictions set
EXAMPLE: !restrictions
RESULT: All the current restrictions"""
restrictions = await ctx.bot.db.fetch(
"SELECT source, destination, from_to FROM restrictions WHERE guild=$1",
ctx.guild.id
)
entries = []
for restriction in restrictions:
# Check whether it's from or to to change what the format looks like
dest = restriction["destination"]
if dest != "everyone":
dest = await utils.convert(ctx, restriction["destination"])
# If it doesn't exist, don't add it
if dest:
entries.append(f"{restriction['source']} {'from' if restriction['from_to'] == 'from' else 'to'} {dest}")
if entries:
# Then paginate
try:
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
await ctx.send("There are no restrictions!")
@commands.command()
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def restrict(self, ctx, *options):
"""
This is an intuitive command to restrict something to/from something
The format is `!restrict what from/to who/where`
For example, `!restrict command to role` will require a user to have `role`
to be able to run `command`
`!restrict command to channel` will only allow `command` to be ran in `channel`
EXAMPLE: !restrict boop from @user
RESULT: This user can no longer use the boop command
"""
# First make sure we're given three options
if len(options) != 3:
await ctx.send("You need to provide 3 options! Such as `command from @User`")
return
elif ctx.message.mention_everyone:
await ctx.send("Please do not use this command to 'disable from everyone'. Use the `disable` command")
return
else:
# Get the three arguments from this list, then make sure the 2nd is either from or to
arg1, arg2, arg3 = options
if arg2.lower() not in ['from', 'to']:
await ctx.send("The 2nd option needs to be either \"to\" or \"from\". Such as: `command from @user` "
"or `command to Role`")
return
else:
# Try to convert the other arguments
arg2 = arg2.lower()
option1 = await utils.convert(ctx, arg1)
option2 = await utils.convert(ctx, arg3)
if option1 is None or option2 is None:
await ctx.send("Sorry, but I don't know how to restrict {} {} {}".format(arg1, arg2, arg3))
return
from_to = arg2
source = None
destination = None
overwrites = None
# The possible options:
# Member
# Role
# Command
# Text/Voice Channel
if isinstance(option1, (commands.core.Command, commands.core.Group)):
# From:
# Users - Command can't be run by this person
# Channels - Command can't be ran in this channel
# Roles - Command can't be ran by anyone in this role (least likely, but still possible uses)
if arg2 == "from":
if isinstance(option2, (discord.Member, discord.Role, discord.TextChannel)):
source = option1.qualified_name
destination = str(option2.id)
# To:
# Channels - Command can only be run in this channel
# Roles - This role is required in order to run this command
else:
if isinstance(option2, (discord.Role, discord.TextChannel)):
source = option1.qualified_name
destination = str(option2.id)
elif isinstance(option1, discord.Member):
# From:
# Channels - Setup an overwrite for this channel so that they cannot read it
# Command - Command cannot be used by this user
if arg2 == "from":
if isinstance(option2, (discord.TextChannel, discord.VoiceChannel)):
ov = discord.utils.find(lambda t: t[0] == option1, option2.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=False)
else:
ov = discord.PermissionOverwrite(read_messages=False)
overwrites = {
'channel': option2,
option1: ov
}
elif isinstance(option2, (commands.core.Command, commands.core.Group)):
source = option2.qualified_name
destination = str(option1.id)
elif isinstance(option1, (discord.TextChannel, discord.VoiceChannel)):
# From:
# Command - Command cannot be used in this channel
# Member - Setup an overwrite for this channel so that they cannot read it
# Role - Setup an overwrite for this channel so that this Role cannot read it
if arg2 == "from":
if isinstance(option2, (discord.Member, discord.Role)):
ov = discord.utils.find(lambda t: t[0] == option2, option1.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=False)
else:
ov = discord.PermissionOverwrite(read_messages=False)
overwrites = {
'channel': option1,
option2: ov
}
elif isinstance(option2, (commands.core.Command, commands.core.Group)) \
and isinstance(option1, discord.TextChannel):
source = option2.qualified_name
destination = str(option1.id)
# To:
# Command - Command can only be used in this channel
# Role - Setup an overwrite so only this role can read this channel
else:
if isinstance(option2, (commands.core.Command, commands.core.Group)) \
and isinstance(option1, discord.TextChannel):
source = option2.qualified_name
destination = str(option1.id)
elif isinstance(option2, (discord.Member, discord.Role)):
ov = discord.utils.find(lambda t: t[0] == option2, option1.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=True)
else:
ov = discord.PermissionOverwrite(read_messages=True)
ov2 = discord.utils.find(lambda t: t[0] == ctx.message.guild.default_role,
option1.overwrites)
if ov2:
ov2 = ov2[1]
ov2.update(read_messages=False)
else:
ov2 = discord.PermissionOverwrite(read_messages=False)
overwrites = {
'channel': option1,
option2: ov,
ctx.message.guild.default_role: ov2
}
elif isinstance(option1, discord.Role):
# From:
# Command - No one with this role can run this command
# Channel - Setup an overwrite for this channel so that this Role cannot read it
if arg2 == "from":
if isinstance(option2, (commands.core.Command, commands.core.Group)):
source = option2.qualified_name
destination = option1.id
elif isinstance(option2, (discord.TextChannel, discord.VoiceChannel)):
ov = discord.utils.find(lambda t: t[0] == option1, option2.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=False)
else:
ov = discord.PermissionOverwrite(read_messages=False)
overwrites = {
'channel': option2,
option1: ov
}
# To:
# Command - You have to have this role to run this command
# Channel - Setup an overwrite so you have to have this role to read this channel
else:
if isinstance(option2, (discord.TextChannel, discord.VoiceChannel)):
ov = discord.utils.find(lambda t: t[0] == option1, option2.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=True)
else:
ov = discord.PermissionOverwrite(read_messages=True)
ov2 = discord.utils.find(lambda t: t[0] == ctx.message.guild.default_role,
option2.overwrites)
if ov2:
ov2 = ov2[1]
ov2.update(read_messages=False)
else:
ov2 = discord.PermissionOverwrite(read_messages=False)
overwrites = {
'channel': option2,
option1: ov,
ctx.message.guild.default_role: ov2
}
elif isinstance(option2, (commands.core.Command, commands.core.Group)):
source = option2.qualified_name
destination = str(option1.id)
if source is not None and destination is not None:
try:
await ctx.bot.db.execute(
"INSERT INTO restrictions (guild, source, destination, from_to) VALUES ($1, $2, $3, $4)",
ctx.guild.id,
source,
destination,
from_to
)
except UniqueViolationError:
# If it's already inserted, then nothing needs to be updated
# It just means this particular restriction is already set
pass
else:
ctx.bot.cache.add_restriction(ctx.guild, from_to, {"source": source, "destination": destination})
elif overwrites:
channel = overwrites.pop('channel')
for target, setting in overwrites.items():
await channel.set_permissions(target, overwrite=setting)
else:
await ctx.send("Sorry but I don't know how to restrict {} {} {}".format(arg1, arg2, arg3))
return
await ctx.send("I have just restricted {} {} {}".format(arg1, arg2, arg3))
@commands.command()
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def unrestrict(self, ctx, *options):
"""
This is an intuitive command to unrestrict something to/from something
The format is `!restrict what from/to who/where`
For example, `!unrestrict command to role` will remove the restriction on this command, requiring role
EXAMPLE: !unrestrict boop from @user
RESULT: The restriction on this user to use boop has been lifted
"""
# First make sure we're given three options
if len(options) != 3:
await ctx.send("You need to provide 3 options! Such as `command from @User`")
return
else:
# Get the three arguments from this list, then make sure the 2nd is either from or to
arg1, arg2, arg3 = options
if arg2.lower() not in ['from', 'to']:
await ctx.send("The 2nd option needs to be either \"to\" or \"from\". Such as: `command from @user` "
"or `command to Role`")
return
else:
# Try to convert the other arguments
arg2 = arg2.lower()
option1 = await utils.convert(ctx, arg1)
option2 = await utils.convert(ctx, arg3)
if option1 is None or option2 is None:
await ctx.send("Sorry, but I don't know how to unrestrict {} {} {}".format(arg1, arg2, arg3))
return
# First check if this is a blacklist/whitelist (by checking if we are unrestricting commands)
if any(isinstance(x, (commands.core.Command, commands.core.Group)) for x in [option1, option2]):
# The source should always be the command, so just set this based on which order is given (either is
# allowed)
if isinstance(option1, (commands.core.Command, commands.core.Group)):
source = option1.qualified_name
destination = str(option2.id)
else:
source = option2.qualified_name
destination = str(option1.id)
# Now just try to remove it
await ctx.bot.db.execute("""
DELETE FROM
restrictions
WHERE
source=$1 AND
destination=$2 AND
from_to=$3 AND
guild=$4""", source, destination, arg2, ctx.guild.id)
ctx.bot.cache.remove_restriction(ctx.guild, arg2, {"source": source, "destination": destination})
# If this isn't a blacklist/whitelist, then we are attempting to remove an overwrite
else:
# Get the source and destination based on whatever order is provided
if isinstance(option1, (discord.TextChannel, discord.VoiceChannel)):
source = option2
destination = option1
else:
source = option1
destination = option2
# See if it's the blacklist that we're removing from
if arg2 == "from":
# Get overwrites if they exist
# If it doesn't, there's nothing to do here
ov = discord.utils.find(lambda t: t[0] == source, destination.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=True)
await destination.set_permissions(source, overwrite=ov)
else:
ov = discord.utils.find(lambda t: t[0] == source, destination.overwrites)
ov2 = discord.utils.find(lambda t: t[0] == ctx.message.guild.default_role, destination.overwrites)
if ov:
ov = ov[1]
ov.update(read_messages=None)
await destination.set_permissions(source, overwrite=ov)
if ov2:
ov2 = ov2[1]
ov2.update(read_messages=True)
await destination.set_permissions(source, overwrite=ov2)
await ctx.send("I have just unrestricted {} {} {}".format(arg1, arg2, arg3))
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def perms(self, ctx, *, command: str):
"""This command can be used to print the current allowed permissions on a specific command
This supports groups as well as subcommands; pass no argument to print a list of available permissions
EXAMPLE: !perms help
RESULT: Hopefully a result saying you just need send_messages permissions; otherwise lol
this server's admin doesn't like me """
cmd = ctx.bot.get_command(command)
if cmd is None:
# If a command wasn't provided, see if a user was
converter = commands.converter.MemberConverter()
try:
member = await converter.convert(ctx, command)
# If we failed to convert, just mention that an invalid command was provided
except commands.converter.BadArgument:
await ctx.send("That is not a valid command!")
return
else:
# Otherwise iterate through the permissions and their values, only including ones that are on
perms = [p for p, value in member.guild_permissions if value]
# Create an embed with their colour
embed = discord.Embed(colour=member.colour)
# Set the author to this user
embed.set_author(name=str(member), icon_url=member.avatar_url)
# Then add their permissions in one field
embed.add_field(name="Allowed permissions", value="\n".join(perms))
await ctx.send(embed=embed)
return
result = await ctx.bot.db.fetchrow(
"SELECT permission FROM custom_permissions WHERE guild = $1 AND command = $2",
ctx.guild.id,
command
)
perms_value = result["permission"] if result else None
if perms_value is None:
# If we don't find custom permissions, get the required permission for a command
# based on what we set in utils.can_run, if can_run isn't found, we'll get an IndexError
try:
can_run = [func for func in cmd.checks if "can_run" in func.__qualname__][0]
except IndexError:
# Loop through and check if there is a check called is_owner
# If we loop through and don't find one, this means that the only other choice is to be
# Able to manage the server (for the utils on perm commands)
for func in cmd.checks:
if "is_owner" in func.__qualname__:
await ctx.send("You need to own the bot to run this command")
return
await ctx.send("You are required to have `manage_guild` permissions to run `{}`".format(
cmd.qualified_name
))
return
# Perms will be an attribute if can_run is found no matter what, so no need to check this
perms = "\n".join(attribute for attribute, setting in can_run.perms.items() if setting)
await ctx.send(
"You are required to have `{}` permissions to run `{}`".format(perms, cmd.qualified_name))
else:
# Permissions are saved as bit values, so create an object based on that value
# Then check which permission is true, that is our required permission
# There's no need to check for errors here, as we ensure a permission is valid when adding it
permissions = discord.Permissions(perms_value)
needed_perm = [perm[0] for perm in permissions if perm[1]][0]
await ctx.send("You need to have the permission `{}` "
"to use the command `{}` in this server".format(needed_perm, command))
@perms.command(name="add", aliases=["setup,create"])
@commands.guild_only()
@commands.has_permissions(manage_guild=True)
async def add_perms(self, ctx, *, msg: str):
"""Sets up custom permissions on the provided command
Format must be 'perms add <command> <permission>'
If you want to open the command to everyone, provide 'none' as the permission
EXAMPLE: !perms add skip ban_members
RESULT: No more random people voting to skip a song"""
# Since subcommands exist, base the last word in the list as the permission, and the rest of it as the command
command, _, permission = msg.rpartition(" ")
if command == "":
await ctx.send("Please provide the permissions you want to setup, the format for this must be in:\n"
"`perms add <command> <permission>`")
return
cmd = ctx.bot.get_command(command)
if cmd is None:
await ctx.send(
"That command does not exist! You can't have custom permissions on a non-existant command....")
return
# If a user can run a command, they have to have send_messages permissions; so use this as the base
if permission.lower() == "none":
permission = "send_messages"
# Convert the string to an int value of the permissions object, based on the required permission
# If we hit an attribute error, that means the permission given was not correct
perm_obj = discord.Permissions.none()
try:
setattr(perm_obj, permission, True)
except AttributeError:
await ctx.send("{} does not appear to be a valid permission! Valid permissions are: ```\n{}```"
.format(permission, "\n".join(valid_perms)))
return
perm_value = perm_obj.value
# Two cases I use should never have custom permissions setup on them, is_owner for obvious reasons
# The other case is if I'm using the default has_permissions case
# Which means I do not want to check custom permissions at all
# Currently the second case is only on adding and removing permissions, to avoid abuse on these
for check in cmd.checks:
if "is_owner" == check.__name__ or "has_permissions" in str(check):
await ctx.send("This command cannot have custom permissions setup!")
return
await ctx.bot.db.execute(
"INSERT INTO custom_permissions (guild, command, permission) VALUES ($1, $2, $3)",
ctx.guild.id,
cmd.qualified_name,
perm_value
)
ctx.bot.cache.update_custom_permission(ctx.guild, cmd.qualified_name, perm_value)
await ctx.send("I have just added your custom permissions; "
"you now need to have `{}` permissions to use the command `{}`".format(permission, command))
@perms.command(name="remove", aliases=["delete"])
@commands.guild_only()
@commands.has_permissions(manage_guild=True)
async def remove_perms(self, ctx, *, command: str):
"""Removes the custom permissions setup on the command specified
EXAMPLE: !perms remove play
RESULT: Freedom!"""
cmd = ctx.bot.get_command(command)
if cmd is None:
await ctx.send(
"That command does not exist! You can't have custom permissions on a non-existant command....")
return
await ctx.bot.db.execute(
"DELETE FROM custom_permissions WHERE guild=$1 AND command=$2", ctx.guild.id, cmd.qualified_name
)
ctx.bot.cache.update_custom_permission(ctx.guild, cmd.qualified_name, None)
await ctx.send("I have just removed the custom permissions for {}!".format(cmd))
@commands.command(aliases=['nick'])
@commands.guild_only()
@utils.can_run(kick_members=True)
async def nickname(self, ctx, *, name=None):
"""Used to set the nickname for Bonfire (provide no nickname and it will reset)
EXAMPLE: !nick Music Bot
RESULT: My nickname is now Music Bot"""
try:
await ctx.message.guild.me.edit(nick=name)
except discord.HTTPException:
await ctx.send("Sorry but I can't change my nickname to {}".format(name))
else:
await ctx.send("\N{OK HAND SIGN}")
def setup(bot):
bot.add_cog(Admin())

226
cogs/birthday.py Normal file
View file

@ -0,0 +1,226 @@
import discord
import datetime
import asyncio
import traceback
import re
import calendar
from discord.ext import commands
from asyncpg import UniqueViolationError
import utils
def parse_string(date):
today = datetime.date.today()
month = None
day = None
month_map = {
"january": 1,
"jan": 1,
"february": 2,
"feb": 2,
"march": 3,
"mar": 3,
"april": 4,
"apr": 4,
"may": 5,
"june": 6,
"jun": 6,
"july": 7,
"jul": 7,
"august": 8,
"aug": 8,
"september": 9,
"sep": 9,
"october": 10,
"oct": 10,
"november": 11,
"nov": 11,
"december": 12,
"dec": 12,
}
num_re = re.compile("^(\d+)[a-z]*$")
for part in [x.lower() for x in date.split()]:
match = num_re.match(part)
if match:
day = int(match.group(1))
elif part in month_map:
month = month_map.get(part)
if month and day:
year = today.year
if month < today.month:
year += 1
elif month == today.month and day <= today.day:
year += 1
return datetime.date(year, month, day)
class Birthday(commands.Cog):
"""Track and announce birthdays"""
def __init__(self, bot):
self.bot = bot
self.task = self.bot.loop.create_task(self.birthday_task())
async def get_birthdays_for_server(self, server, today=False):
members = ", ".join(f"{m.id}" for m in server.members)
query = f"""
SELECT
id, birthday
FROM
users
WHERE
id IN ({members})
"""
if today:
query += """
AND
birthday = CURRENT_DATE
"""
query += """
ORDER BY
birthday
"""
return await self.bot.db.fetch(query)
async def birthday_task(self):
await self.bot.wait_until_ready()
while not self.bot.is_closed():
try:
await self.notify_birthdays()
except Exception as error:
with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f)
print(f"{error.__class__.__name__}: {error}", file=f)
finally:
# Every day
await asyncio.sleep(60 * 60 * 24)
async def notify_birthdays(self):
query = """
SELECT
id, COALESCE(birthday_alerts, default_alerts) AS channel
FROM
guilds
WHERE
birthday_notifications=True
AND
COALESCE(birthday_alerts, default_alerts) IS NOT NULL
"""
servers = await self.bot.db.fetch(query)
update_bds = []
if not servers:
return
for s in servers:
# Get the channel based on the birthday alerts, or default alerts channel
channel = self.bot.get_channel(s['channel'])
if not channel:
continue
bds = await self.get_birthdays_for_server(channel.guild, today=True)
# A list of the id's that will get updated
for bd in bds:
try:
member = channel.guild.get_member(bd["id"])
await channel.send(f"It is {member.mention}'s birthday today! "
"Wish them a happy birthday! \N{SHORTCAKE}")
except (discord.Forbidden, discord.HTTPException):
pass
finally:
update_bds.append(bd['id'])
if not update_bds:
return
query = f"""
UPDATE
users
SET
birthday = birthday + interval '1 year'
WHERE
id IN ({", ".join(f"'{bd}'" for bd in update_bds)})
"""
await self.bot.db.execute(query)
@commands.group(aliases=['birthdays'], invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def birthday(self, ctx, *, member: discord.Member = None):
"""A command used to view the birthdays on this server; or a specific member's birthday
EXAMPLE: !birthdays
RESULT: A printout of the birthdays from everyone on this server"""
if member:
date = await ctx.bot.db.fetchrow("SELECT birthday FROM users WHERE id=$1", member.id)
if date is None or date["birthday"] is None:
await ctx.send(f"I do not have {member.display_name}'s birthday saved!")
else:
date = date['birthday']
await ctx.send(f"{member.display_name}'s birthday is {calendar.month_name[date.month]} {date.day}")
else:
# Get this server's birthdays
bds = await self.get_birthdays_for_server(ctx.guild)
# Create entries based on the user's display name and their birthday
entries = [
f"{ctx.guild.get_member(bd['id']).display_name} ({bd['birthday'].strftime('%B %-d')})"
for bd in bds
if bd['birthday']
]
if not entries:
await ctx.send("I don't know anyone's birthday in this server!")
return
# Create our pages object
try:
pages = utils.Pages(ctx, entries=entries, per_page=5)
pages.title = f"Birthdays for {ctx.guild.name}"
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
@birthday.command(name='add')
@utils.can_run(send_messages=True)
async def _add_bday(self, ctx, *, date):
"""Used to link your birthday to your account
EXAMPLE: !birthday add December 1st
RESULT: I now know your birthday is December 1st"""
if len(date.split()) != 2:
await ctx.send("Please provide date in a valid format, such as December 1st!")
return
try:
date = parse_string(date)
except ValueError:
await ctx.send("Please provide date in a valid format, such as December 1st!")
return
if date is None:
await ctx.send("Please provide date in a valid format, such as December 1st!")
return
await ctx.send(f"I have just saved your birthday as {date}")
try:
await ctx.bot.db.execute("INSERT INTO users (id, birthday) VALUES ($1, $2)", ctx.author.id, date)
except UniqueViolationError:
await ctx.bot.db.execute("UPDATE users SET birthday = $1 WHERE id = $2", date, ctx.author.id)
@birthday.command(name='remove')
@utils.can_run(send_messages=True)
async def _remove_bday(self, ctx):
"""Used to unlink your birthday to your account
EXAMPLE: !birthday remove
RESULT: I have magically forgotten your birthday"""
await ctx.send("I don't know your birthday anymore :(")
await ctx.bot.db.execute("UPDATE users SET birthday=NULL WHERE id=$1", ctx.author.id)
def setup(bot):
bot.add_cog(Birthday(bot))

View file

@ -1,31 +1,19 @@
from . import utils
import utils
from discord.ext import commands
import asyncio
import math
face_map = {
'S': 'spades',
'D': 'diamonds',
'C': 'clubs',
'H': 'hearts'
}
card_map = {
'A': 'Ace',
'K': 'King',
'Q': 'Queen',
'J': 'Jack'
}
class Blackjack(commands.Cog):
"""Pretty self-explanatory"""
class Blackjack:
def __init__(self, bot):
self.bot = bot
self.games = {}
def __unload(self):
def cog_unload(self):
# Simply cancel every task
for game in self.games.values():
game.task.cancel()
@ -34,17 +22,18 @@ class Blackjack:
# When we're done with the game, we need to delete the game itself and remove it's instance from games
# To do this, it needs to be able to access this instance of Blackjack
game = Game(self.bot, message, self)
self.games[message.server.id] = game
self.games[message.guild.id] = game
@commands.group(pass_context=True, no_pm=True, aliases=['bj'], invoke_without_command=True)
@utils.custom_perms(send_messages=True)
@commands.group(aliases=['bj'], invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def blackjack(self, ctx):
"""Creates a game/joins the current running game of blackjack
EXAMPLE: !blackjack
RESULT: A new game of blackjack!"""
# Get this server's game if it exists
game = self.games.get(ctx.message.server.id)
# Get this guild's game if it exists
game = self.games.get(ctx.message.guild.id)
# If it doesn't, start one
if game is None:
self.create_game(ctx.message)
@ -54,53 +43,55 @@ class Blackjack:
# If it worked, they're ready to play
if status:
fmt = "{} has joined the game of blackjack, and will be able to play next round!"
await self.bot.say(fmt.format(ctx.message.author.display_name))
await ctx.send(fmt.format(ctx.message.author.display_name))
else:
# Otherwise, lets check *why* they couldn't join
if game.playing(ctx.message.author):
await self.bot.say("You are already playing! Wait for your turn!")
await ctx.send("You are already playing! Wait for your turn!")
else:
await self.bot.say("There are already a max number of players playing/waiting to play!")
await ctx.send("There are already a max number of players playing/waiting to play!")
@blackjack.command(pass_context=True, no_pm=True, name='leave', aliases=['quit'])
@utils.custom_perms(send_messages=True)
@blackjack.command(name='leave', aliases=['quit'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def blackjack_leave(self, ctx):
"""Leaves the current game of blackjack
EXAMPLE: !blackjack leave
RESULT: You stop losing money in blackjack"""
# Get this server's game if it exists
game = self.games.get(ctx.message.server.id)
# Get this guild's game if it exists
game = self.games.get(ctx.message.guild.id)
if game is None:
await self.bot.say("There are currently no games of Blackjack running!")
await ctx.send("There are currently no games of Blackjack running!")
return
status = game.leave(ctx.message.author)
if status:
await self.bot.say("You have left the game, and will be removed at the end of this round")
await ctx.send("You have left the game, and will be removed at the end of this round")
else:
await self.bot.say("Either you have already bet, or you are not even playing right now!")
await ctx.send("Either you have already bet, or you are not even playing right now!")
@blackjack.command(pass_context=True, no_pm=True, name='forcestop', aliases=['stop'])
@utils.custom_perms(manage_server=True)
@blackjack.command(name='forcestop', aliases=['stop'])
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def blackjack_stop(self, ctx):
"""Forces the game to stop, mostly for use if someone has gone afk
EXAMPLE: !blackjack forcestop
RESULT: No more blackjack spam"""
# Get this server's game if it exists
game = self.games.get(ctx.message.server.id)
# Get this guild's game if it exists
game = self.games.get(ctx.message.guild.id)
if game is None:
await self.bot.say("There are currently no games of Blackjack running!")
await ctx.send("There are currently no games of Blackjack running!")
return
game.task.cancel()
del self.games[ctx.message.server.id]
await self.bot.say("The blackjack game running here has just ended")
del self.games[ctx.message.guild.id]
await ctx.send("The blackjack game running here has just ended")
def FOIL(a, b):
@ -146,16 +137,17 @@ class Player:
for card in self.hand:
# Order is suit, face...so we want the second value
face = card[1]
value = card.value.value
face = card.value.name
if face in ['Q', 'K', 'J']:
if face in ['queen', 'king', 'jack']:
for index, t in enumerate(total):
total[index] += 10
elif face == 'A':
elif face == 'ace':
total = FOIL(total, [1, 11])
else:
for index, t in enumerate(total):
total[index] += int(face)
total[index] += int(value)
# If we have more than one possible total (there is at least one ace) then we do not care about one if it is
# over 21
@ -177,12 +169,7 @@ class Player:
def __str__(self):
# We only care about our hand, for printing wise
fmt = "Hand:\n"
fmt += "\n".join(
"{} of {}".format(
card_map.get(card[1], card[1]),
face_map.get(card[0], card[0]))
for card in self.hand
)
fmt += "\n".join(str(card) for card in self.hand)
fmt += "\n(Total: {})".format(self.count)
return fmt
@ -226,7 +213,7 @@ class Game:
async def game_task(self):
"""The task to handle the entire game"""
while len(self.players) > 0:
await self.bot.send_message(self.channel, "A new round has started!!")
await self.channel.send("A new round has started!!")
# First wait for bets
await self.bet_task()
@ -253,25 +240,25 @@ class Game:
await self.cleanup()
# If we reach the end of this loop, that means there are no more players
del self.bj.games[self.channel.server.id]
del self.bj.games[self.channel.guild.id]
async def dealer_task(self):
"""The task handling the dealer's play after all players have stood"""
fmt = "It is the dealer's turn to play\n\n{}".format(self.dealer)
msg = await self.bot.send_message(self.channel, fmt)
msg = await self.channel.send(fmt)
while True:
await asyncio.sleep(1)
if self.dealer.bust:
fmt = msg.content + "\n\nDealer has busted!!"
await self.bot.edit_message(msg, fmt)
await msg.edit(content=fmt)
return
for num in self.dealer.count:
if num > 16:
return
self.hit(self.dealer)
fmt = "It is the dealer's turn to play\n\n{}".format(self.dealer)
msg = await self.bot.edit_message(msg, fmt)
await msg.edit(content=fmt)
async def round_task(self):
"""The task handling the round itself, asking each person to hit or stand"""
@ -280,59 +267,60 @@ class Game:
# A differen task will handle if a player hit blackjack to start (so they would not be 'playing')
# Our check to make sure a valid 'command' was provided
check = lambda m: m.content.lower() in ['hit', 'stand', 'double']
def check(m):
if m.channel == self.channel and m.author == player.member:
return m.content.lower() in ['hit', 'stand', 'double']
else:
return False
# First lets handle the blackjacks
for entry in [p for p in self.players if p['status'] == 'blackjack']:
player = entry['player']
fmt = "You got a blackjack {0.member.mention}!\n\n{0}".format(player)
await self.bot.send_message(self.channel, fmt)
await self.channel.send(fmt)
# Loop through each player (as long as their status is playing) and they have bet chips
for entry in [p for p in self.players if p['status'] == 'playing' and hasattr(p['player'], 'bet')]:
player = entry['player']
# Let them know it's their turn to play
fmt = "It is your turn to play {0.member.mention}\n\n{0}".format(player)
await self.bot.send_message(self.channel, fmt)
await self.channel.send(fmt)
first = True
# If they're not playing anymore (i.e. they busted, are standing, etc.) then we don't want to keep asking
# them to hit or stand
while entry['status'] not in ['stand', 'bust']:
# Ask if they want to hit or stand
fmt = "Hit, stand, or double?"
await self.bot.send_message(self.channel, fmt)
msg = await self.bot.wait_for_message(timeout=60, author=player.member, channel=self.channel,
check=check)
if first:
fmt = "Hit, stand, or double?"
else:
fmt = "Hit or stand?"
await self.channel.send(fmt)
# If they took to long, make them stand so the next person can play
if msg is None:
await self.bot.send_message(self.channel, "Took to long! You're standing!")
try:
msg = await self.bot.wait_for('message', timeout=60, check=check)
except asyncio.TimeoutError:
await self.channel.send("Took to long! You're standing!")
entry['status'] = 'stand'
# If they want to hit
elif 'hit' in msg.content.lower():
self.hit(player)
await self.bot.send_message(self.channel, player)
# If they want to stand
elif 'stand' in msg.content.lower():
self.stand(player)
elif 'double' in msg.content.lower():
self.double(player)
await self.bot.send_message(self.channel, player)
# TODO: Handle double, split
else:
# If they want to hit
if 'hit' in msg.content.lower():
self.hit(player)
await self.channel.send(player)
# If they want to stand
elif 'stand' in msg.content.lower():
self.stand(player)
elif 'double' in msg.content.lower() and first:
self.double(player)
await self.channel.send(player)
# TODO: Handle double, split
first = False
async def bet_task(self):
"""Performs the task of betting"""
def check(_msg):
"""Makes sure the message provided is within the min and max bets"""
try:
msg_length = int(_msg.content)
return self.min_bet <= msg_length <= self.max_bet
except ValueError:
return _msg.content.lower() == 'skip'
# There is one situation that we want to allow that means we cannot loop through players like normally would
# be the case: Betting has started; while one person is betting, another joins This means our list has
# changed, and neither based on the length or looping through the list itself will handle this To handle
@ -348,28 +336,43 @@ class Game:
entry = players[0]
player = entry['player']
def check(_msg):
"""Makes sure the message provided is within the min and max bets"""
if _msg.channel == self.channel and _msg.author == player.member:
try:
msg_length = int(_msg.content)
return self.min_bet <= msg_length <= self.max_bet
except ValueError:
return _msg.content.lower() == 'skip'
else:
return False
fmt = "Your turn to bet {0.member.mention}, your current chips are: {0.chips}\n" \
"Current min bet is {1}, current max bet is {2}\n" \
"Place your bet now (please provide only the number;" \
"'skip' if you would like to leave this game)".format(player, self.min_bet, self.max_bet)
await self.bot.send_message(self.channel, fmt)
msg = await self.bot.wait_for_message(timeout=60, author=player.member, channel=self.channel, check=check)
if msg is None:
await self.bot.send_message(self.channel, "You took too long! You're sitting this round out")
await self.channel.send(fmt)
try:
msg = await self.bot.wait_for("message", timeout=60, check=check)
except asyncio.TimeoutError:
await self.channel.send("You took too long! You're sitting this round out")
entry['status'] = 'stand'
elif msg.content.lower() == 'skip':
await self.bot.send_message(self.channel, "Alright, you've been removed from the game")
self.leave(player.member)
else:
num = int(msg.content)
# Set the new bet, and remove it from this players chip total
if num <= player.chips:
player.bet = num
player.chips -= num
entry['status'] = 'bet'
if msg.content.lower() == 'skip':
await self.channel.send("Alright, you've been removed from the game")
self.leave(player.member)
else:
await self.bot.send_message(self.channel, "You can't bet more than you have!!")
num = int(msg.content)
# Set the new bet, and remove it from this players chip total
if num <= player.chips:
player.bet = num
player.chips -= num
entry['status'] = 'bet'
else:
await self.channel.send("You can't bet more than you have!!")
# Call this so that we can correct the list, if someone has left or join
self.player_cleanup()
@ -464,7 +467,7 @@ class Game:
# Add that to the main string
fmt += "Blackjacks: {}\n".format(_fmt)
await self.bot.send_message(self.channel, fmt)
await self.channel.send(fmt)
# Do the same for the dealers hand
cards = list(self.dealer.hand.draw(self.dealer.hand.count))
@ -481,10 +484,10 @@ class Game:
self.players.extend(self._added_players)
self._added_players.clear()
# What we want to do is remove any players that are in the game and have left the server
# What we want to do is remove any players that are in the game and have left the guild
for entry in self.players:
m = entry['player'].member
if m not in self.channel.server.members:
if m not in self.channel.guild.members:
self._removed_players.append(entry['player'])
# Remove the players who left

View file

@ -1,18 +1,18 @@
import discord
from discord.ext import commands
from .utils import checks
from utils import checks
import random
import re
from enum import Enum
class Chess:
def __init__(self, bot):
self.bot = bot
# Our format for games is going to be a little different, because we do want to allow multiple games per server
# Format should be {'server_id': [Game, Game, Game]}
self.games = {}
class Chess(commands.Cog):
"""Pretty self-explanatory"""
# Our format for games is going to be a little different, because we do want to allow multiple games per guild
# Format should be {'server_id': [Game, Game, Game]}
games = {}
def play(self, player, notation):
"""Our task to handle a player making their actual move"""
@ -91,18 +91,18 @@ class Chess:
return MoveStatus.invalid
def get_game(self, player):
"""Provides the game this player is playing, in this server"""
server_games = self.games.get(player.server.id, [])
for game in server_games:
"""Provides the game this player is playing, in this guild"""
guild_games = self.games.get(player.guild.id, [])
for game in guild_games:
if player in game.challengers.values():
return game
return None
def in_game(self, player):
"""Checks to see if a player is playing in a game right now, in this server"""
server_games = self.games.get(player.server.id, [])
for game in server_games:
"""Checks to see if a player is playing in a game right now, in this guild"""
guild_games = self.games.get(player.guild.id, [])
for game in guild_games:
if player in game.challengers.values():
return True
@ -111,14 +111,14 @@ class Chess:
def start_game(self, player1, player2):
game = Game(player1, player2)
try:
self.games[player1.server.id].append(game)
self.games[player1.guild.id].append(game)
except KeyError:
self.games[player1.server.id] = [game]
self.games[player1.guild.id] = [game]
return game
@commands.group(pass_contxt=True, invoke_without_command=True)
@checks.custom_perms(send_messages=True)
@commands.group(invoke_without_command=True)
@checks.can_run(send_messages=True)
async def chess(self, ctx, *, move):
"""Moves a piece based on the notation provided
Notation for normal moves are {piece} to {position} based on the algebraic notation of the board (This is on the picture)
@ -142,47 +142,48 @@ class Chess:
EXAMPLE: !rook to d4"""
result = self.play(ctx.message.author, move)
if result is MoveStatus.invalid:
await self.bot.say("That was an invalid move!")
await ctx.send("That was an invalid move!")
elif result is MoveStatus.wrong_turn:
await self.bot.say("It is not your turn to play on your game in this server!")
await ctx.send("It is not your turn to play on your game in this guild!")
elif result is MoveStatus.no_game:
await self.bot.say("You are not currently playing a game on this server!")
await ctx.send("You are not currently playing a game on this guild!")
elif result is MoveStatus.valid:
game = self.get_game(ctx.message.author)
link = game.draw_board()
await self.bot.upload(link)
await ctx.bot.upload(link)
@commands.command(pass_context=True)
@checks.custom_perms(send_messages=True)
@commands.command()
@checks.can_run(send_messages=True)
async def chess_start(self, ctx, player2: discord.Member):
"""Starts a chess game with another player
You can play one game on a single server at a time
You can play one game on a single guild at a time
EXAMPLE: !chess start @Victim
RESULT: A new chess game! Good luck~"""
# Lets first check our permissions; we're not going to create a text based board
# So we need to be able to attach files in order to send the board
if not ctx.message.channel.permissions_for(ctx.message.server.me).attach_files:
await self.bot.say(
if not ctx.message.channel.permissions_for(ctx.message.guild.me).attach_files:
await ctx.send(
"I need to be able to send pictures to provide the board! Please ask someone with mange roles permission, to grant me attach files permission if you want to play this")
return
# Make sure the author and player 2 are not in a game already
if self.in_game(ctx.message.author):
await self.bot.say("Sorry, but you can only be in one game per server at a time")
await ctx.send("Sorry, but you can only be in one game per guild at a time")
return
if self.in_game(player2):
await self.bot.say("Sorry, but {} is already in a game on this server!".format(player2.display_name))
await ctx.send("Sorry, but {} is already in a game on this guild!".format(player2.display_name))
return
# Start the game
game = self.start_game(ctx.message.author, player2)
player1 = game.challengers.get('white')
await self.bot.say(
"{} you have started a chess game with {}\n\n{} will be white this game, and is going first.\nIf you need information about the notation used to play, run {}help chess".format(
ctx.message.author.display_name, player2.display_name, ctx.prefix))
await ctx.send(
"{} you have started a chess game with {}\n\n{} will be white this game, and is going first.\nIf you need "
"information about the notation used to play, run {}help chess".format(
ctx.message.author.display_name, player2.display_name, player1, ctx.prefix))
class MoveStatus(Enum):
@ -209,7 +210,7 @@ class Game:
self.reset_board()
# The point of chess revolves around the king's position
# Due to this, we're going to use the king's position a lot, so lets save this variable
# Due to this, we're going to use the king's position a lot, so lets save this variable
self.w_king_pos = (0, 4)
self.b_king_pos = (7, 4)
@ -262,7 +263,7 @@ class Game:
colour = 'black'
king_pos = self.b_king_pos
if not can_castle[colour][pos]:
if not self.can_castle[colour][pos]:
return False
# During castling, the row should never change
@ -388,7 +389,7 @@ class Game:
def check(self):
"""Checks our current board, and checks (based on whose turn it is) if we're in a 'check' state"""
# To check for a check, what we should do is loop through the board
# Then check if it's the the current players turn's piece, and compare to moving to the king's position
# Then check if it's the the current players turn's piece, and compare to moving to the king's position
for x, row in enumerate(self.board):
for y, piece in enumerate(row):
if self.white_turn and re.search('B.', piece) and self.valid_move((x, y), self.b_king_pos):

771
cogs/config.py Normal file
View file

@ -0,0 +1,771 @@
from discord.ext import commands
from asyncpg import UniqueViolationError
import utils
import discord
class ConfigException(Exception):
pass
class WrongSettingType(ConfigException):
def __init__(self, message):
self.message = message
class MessageFormatError(ConfigException):
def __init__(self, original, keys):
self.original = original
self.keys = keys
# noinspection PyMethodMayBeStatic,PyUnusedLocal
class GuildConfiguration(commands.Cog):
"""Handles configuring the different settings that can be used on the bot"""
keys = {
}
def _str_to_bool(self, opt, setting):
setting = setting.title()
if setting.title() not in ["True", "False"]:
raise WrongSettingType(
f"The {opt} setting requires either 'True' or 'False', not {setting}"
)
return setting.title() == "True"
async def _get_channel(self, ctx, setting):
converter = commands.converter.TextChannelConverter()
return await converter.convert(ctx, setting)
async def _set_db_guild_opt(self, opt, setting, ctx):
if opt == "prefix":
ctx.bot.cache.update_prefix(ctx.guild, setting)
try:
return await ctx.bot.db.execute(f"INSERT INTO guilds (id, \"{opt}\") VALUES ($1, $2)", ctx.guild.id, setting)
except UniqueViolationError:
return await ctx.bot.db.execute(f"UPDATE guilds SET {opt} = $1 WHERE id = $2", setting, ctx.guild.id)
async def _show_bool_options(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id = $1", ctx.guild.id)
return f"`{opt}` are currently {'enabled' if result is not None and result[opt] else 'disabled'}"
async def _show_channel_options(self, ctx, opt):
"""For showing options that rely on a certain channel"""
result = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id = $1", ctx.guild.id)
if result is None:
return f"You do not have a channel set for {opt}"
channel_id = result[opt]
if channel_id:
channel = ctx.guild.get_channel(channel_id)
if channel:
return f"Your {opt} alerts channel is currently set to {channel.mention}"
else:
return "It looks like you used to have a channel set for this," \
"however the channel has since been deleted"
else:
return f"You do not have a channel set for {opt}"
# These are handles for each setting type
# Just the bool ones, they're all handled the exact same way
_handle_show_birthday_notifications = _show_bool_options
_handle_show_welcome_notifications = _show_bool_options
_handle_show_goodbye_notifications = _show_bool_options
_handle_show_colour_roles = _show_bool_options
_handle_show_include_default_battles = _show_bool_options
_handle_show_include_default_hugs = _show_bool_options
# The channel ones
_handle_show_default_alerts = _show_channel_options
_handle_show_welcome_alerts = _show_channel_options
_handle_show_goodbye_alerts = _show_channel_options
_handle_show_picarto_alerts = _show_channel_options
_handle_show_birthday_alerts = _show_channel_options
_handle_show_raffle_alerts = _show_channel_options
async def _handle_show_welcome_msg(self, ctx, setting):
result = await ctx.bot.db.fetchrow("SELECT welcome_msg FROM guilds WHERE id = $1", ctx.guild.id)
try:
msg = result["welcome_msg"].format(server=ctx.guild.name, member=ctx.author.mention)
return f"Your current welcome message will appear like this:\n\n"
except (AttributeError, TypeError):
return "You currently have no welcome message setup"
async def _handle_show_goodbye_msg(self, ctx, setting):
result = await ctx.bot.db.fetchrow("SELECT goodbye_msg FROM guilds WHERE id = $1", ctx.guild.id)
try:
msg = result["goodbye_msg"].format(server=ctx.guild.name, member=ctx.author.mention)
return f"Your current goodbye message will appear like this:\n\n"
except (AttributeError, TypeError):
return "You currently have no goodbye message setup"
async def _handle_show_prefix(self, ctx, setting):
result = await ctx.bot.db.fetchrow("SELECT prefix FROM guilds WHERE id = $1", ctx.guild.id)
if result is not None:
prefix = result["prefix"]
if prefix is not None:
return f"Your current prefix is `{prefix}`"
return "You do not have a custom prefix set, you are using the default prefix"
async def _handle_show_followed_picarto_channels(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT followed_picarto_channels FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["followed_picarto_channels"]:
try:
pages = utils.Pages(ctx, entries=result["followed_picarto_channels"])
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server is not following any picarto channels"
async def _handle_show_ignored_channels(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT ignored_channels FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["ignored_channels"]:
try:
entries = [
ctx.guild.get_channel(ch).mention
for ch in result["ignored_members"]
if ctx.guild.get_channel(ch) is not None
]
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server is not ignoring any channels"
async def _handle_show_ignored_members(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT ignored_members FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["ignored_members"]:
try:
entries = [
ctx.guild.get_member(m).display_name
for m in result["ignored_members"]
if ctx.guild.get_member(m) is not None
]
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server is not ignoring any members"
async def _handle_show_rules(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT rules FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["rules"]:
try:
pages = utils.Pages(ctx, entries=result["rules"])
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server has no rules"
async def _handle_show_assignable_roles(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT assignable_roles FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["assignable_roles"]:
try:
entries = [
ctx.guild.get_role(r).name
for r in result["assignable_roles"]
if ctx.guild.get_role(r) is not None
]
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server has no assignable roles"
async def _handle_show_custom_battles(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT custom_battles FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["custom_battles"]:
try:
pages = utils.Pages(ctx, entries=result["custom_battles"])
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server has no custom battles"
async def _handle_show_custom_hugs(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT custom_hugs FROM guilds WHERE id = $1", ctx.guild.id)
if result and result["custom_hugs"]:
try:
pages = utils.Pages(ctx, entries=result["custom_hugs"])
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
else:
return "This server has no custom hugs"
async def _handle_show_join_role(self, ctx, opt):
result = await ctx.bot.db.fetchrow("SELECT join_role FROM guilds WHERE id = $1", ctx.guild.id)
if result and result['join_role']:
role = ctx.guild.get_role(result['join_role'])
if role is None:
return "You had a role set, but I can't find it...it's most likely been deleted afterwords!"
else:
return f"When people join I will give them the role {role.name}"
else:
return "You have no join_role setting for when people join this server"
async def _handle_set_birthday_notifications(self, ctx, setting):
opt = "birthday_notifications"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_welcome_notifications(self, ctx, setting):
opt = "welcome_notifications"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_goodbye_notifications(self, ctx, setting):
opt = "goodbye_notifications"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_colour_roles(self, ctx, setting):
opt = "colour_roles"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_include_default_battles(self, ctx, setting):
opt = "include_default_battles"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_include_default_hugs(self, ctx, setting):
opt = "include_default_hugs"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_welcome_msg(self, ctx, setting):
try:
setting.format(member='test', server='test')
except KeyError as e:
raise MessageFormatError(e, ["member", "server"])
else:
return await self._set_db_guild_opt("welcome_msg", setting, ctx)
async def _handle_set_goodbye_msg(self, ctx, setting):
try:
setting.format(member='test', server='test')
except KeyError as e:
raise MessageFormatError(e, ["member", "server"])
else:
return await self._set_db_guild_opt("goodbye_msg", setting, ctx)
async def _handle_set_prefix(self, ctx, setting):
if len(setting) > 20:
raise WrongSettingType("Please keep the prefix under 20 characters")
if setting.lower().strip() == "none":
setting = None
return await self._set_db_guild_opt("prefix", setting, ctx)
async def _handle_set_default_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("default_alerts", channel.id, ctx)
async def _handle_set_welcome_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("welcome_alerts", channel.id, ctx)
async def _handle_set_goodbye_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("goodbye_alerts", channel.id, ctx)
async def _handle_set_picarto_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("picarto_alerts", channel.id, ctx)
async def _handle_set_birthday_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("birthday_alerts", channel.id, ctx)
async def _handle_set_raffle_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("raffle_alerts", channel.id, ctx)
async def _handle_set_followed_picarto_channels(self, ctx, setting):
user = await utils.request(f"http://api.picarto.tv/v1/channel/name/{setting}")
if user is None:
raise WrongSettingType(f"Could not find a picarto user with the username {setting}")
query = """
UPDATE
guilds
SET
followed_picarto_channels = array_append(followed_picarto_channels, $1)
WHERE
id=$2 AND
NOT $1 = ANY(followed_picarto_channels);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_ignored_channels(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
query = """
UPDATE
guilds
SET
ignored_channels = array_append(ignored_channels, $1)
WHERE
id=$2 AND
NOT $1 = ANY(ignored_channels);
"""
ctx.bot.loop.create_task(ctx.bot.cache.load_ignored())
return await ctx.bot.db.execute(query, channel.id, ctx.guild.id)
async def _handle_set_ignored_members(self, ctx, setting):
# We want to make it possible to have members that aren't in the server ignored
# So first check if it's a digit (the id)
if not setting.isdigit():
converter = commands.converter.MemberConverter()
member = await converter.convert(ctx, setting)
setting = member.id
query = """
UPDATE
guilds
SET
ignored_members = array_append(ignored_members, $1)
WHERE
id=$2 AND
NOT $1 = ANY(ignored_members);
"""
ctx.bot.loop.create_task(ctx.bot.cache.load_ignored())
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_rules(self, ctx, setting):
query = """
UPDATE
guilds
SET
rules = array_append(rules, $1)
WHERE
id=$2 AND
NOT $1 = ANY(rules);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_assignable_roles(self, ctx, setting):
converter = commands.converter.RoleConverter()
role = await converter.convert(ctx, setting)
query = """
UPDATE
guilds
SET
assignable_roles = array_append(assignable_roles, $1)
WHERE
id=$2 AND
NOT $1 = ANY(assignable_roles);
"""
return await ctx.bot.db.execute(query, role.id, ctx.guild.id)
async def _handle_set_custom_battles(self, ctx, setting):
try:
setting.format(loser="player1", winner="player2")
except (KeyError, ValueError) as e:
raise MessageFormatError(e, ["loser", "winner"])
else:
query = """
UPDATE
guilds
SET
custom_battles = array_append(custom_battles, $1)
WHERE
id=$2 AND
NOT $1 = ANY(custom_battles);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_custom_hugs(self, ctx, setting):
try:
setting.format(user="user")
except (KeyError, ValueError)as e:
raise MessageFormatError(e, ["user"])
else:
query = """
UPDATE
guilds
SET
custom_hugs = array_append(custom_hugs, $1)
WHERE
id=$2 AND
NOT $1 = ANY(custom_hugs);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_join_role(self, ctx, setting):
converter = commands.converter.RoleConverter()
role = await converter.convert(ctx, setting)
query = """
UPDATE
guilds
SET
join_role = $1
where
ID=$2
"""
return await ctx.bot.db.execute(query, role.id, ctx.guild.id)
async def _handle_remove_birthday_notifications(self, ctx, setting=None):
return await self._set_db_guild_opt("birthday_notifications", False, ctx)
async def _handle_remove_welcome_notifications(self, ctx, setting=None):
return await self._set_db_guild_opt("welcome_notifications", False, ctx)
async def _handle_remove_goodbye_notifications(self, ctx, setting=None):
return await self._set_db_guild_opt("goodbye_notifications", False, ctx)
async def _handle_remove_colour_roles(self, ctx, setting=None):
return await self._set_db_guild_opt("colour_roles", False, ctx)
async def _handle_remove_include_default_battles(self, ctx, setting=None):
return await self._set_db_guild_opt("include_default_battles", False, ctx)
async def _handle_remove_include_default_hugs(self, ctx, setting=None):
return await self._set_db_guild_opt("include_default_hugs", False, ctx)
async def _handle_remove_welcome_msg(self, ctx, setting=None):
return await self._set_db_guild_opt("welcome_msg", None, ctx)
async def _handle_remove_goodbye_msg(self, ctx, setting=None):
return await self._set_db_guild_opt("goodbye_msg", None, ctx)
async def _handle_remove_prefix(self, ctx, setting=None):
return await self._set_db_guild_opt("prefix", None, ctx)
async def _handle_remove_default_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("default_alerts", None, ctx)
async def _handle_remove_welcome_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("welcome_alerts", None, ctx)
async def _handle_remove_goodbye_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("goodbye_alerts", None, ctx)
async def _handle_remove_picarto_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("picarto_alerts", None, ctx)
async def _handle_remove_birthday_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("birthday_alerts", None, ctx)
async def _handle_remove_raffle_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("raffle_alerts", None, ctx)
async def _handle_remove_join_role(self, ctx, setting=None):
return await self._set_db_guild_opt("join_role", None, ctx)
async def _handle_remove_followed_picarto_channels(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
query = """
UPDATE
guilds
SET
followed_picarto_channels = array_remove(followed_picarto_channels, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_ignored_channels(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
channel = await self._get_channel(ctx, setting)
query = """
UPDATE
guilds
SET
ignored_channels = array_remove(ignored_channels, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, channel.id, ctx.guild.id)
async def _handle_remove_ignored_members(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
# We want to make it possible to have members that aren't in the server ignored
# So first check if it's a digit (the id)
if not setting.isdigit():
converter = commands.converter.MemberConverter()
member = await converter.convert(ctx, setting)
setting = member.id
else:
setting = int(setting)
query = """
UPDATE
guilds
SET
ignored_members = array_remove(ignored_members, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_rules(self, ctx, setting=None):
if setting is None or not setting.isdigit():
raise WrongSettingType("Please provide the number of the rule you want to remove")
query = """
UPDATE
guilds
SET
rules = array_remove(rules, rules[$1])
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, int(setting), ctx.guild.id)
async def _handle_remove_assignable_roles(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
if not setting.isdigit():
converter = commands.converter.RoleConverter()
role = await converter.convert(ctx, setting)
setting = role.id
else:
setting = int(setting)
query = """
UPDATE
guilds
SET
assignable_roles = array_remove(assignable_roles, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_custom_battles(self, ctx, setting=None):
if setting is None or not setting.isdigit():
raise WrongSettingType("Please provide the number of the custom battle you want to remove")
else:
setting = int(setting)
query = """
UPDATE
guilds
SET
custom_battles = array_remove(custom_battles, custom_battles[$1])
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, int(setting), ctx.guild.id)
async def _handle_remove_custom_hugs(self, ctx, setting=None):
if setting is None or not setting.isdigit():
raise WrongSettingType("Please provide the number of the custom hug you want to remove")
else:
setting = int(setting)
query = """
UPDATE
guilds
SET
custom_hugs = array_remove(custom_hugs, custom_hugs[$1])
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, int(setting), ctx.guild.id)
async def cog_after_invoke(self, ctx):
"""Here we will facilitate cleaning up settings, will remove channels/roles that no longer exist, etc."""
pass
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def config(self, ctx, *, opt=None):
"""Handles the configuration of the bot for this server"""
if opt:
try:
coro = getattr(self, f"_handle_show_{opt}")
except AttributeError:
await ctx.send(f"{opt} is not a valid config option. Use {ctx.prefix}config to list all config options")
else:
try:
msg = await coro(ctx, opt)
except WrongSettingType as exc:
await ctx.send(exc.message)
except commands.BadArgument:
pass
else:
return await ctx.send(msg)
settings = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
# For convenience, if it's None, just create it and return the default values
if settings is None:
await ctx.bot.db.execute("INSERT INTO guilds (id) VALUES ($1)", ctx.guild.id)
settings = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
alerts = {}
# This is dirty I know, but oh well...
for alert_type in ["default", "welcome", "goodbye", "picarto", "birthday", "raffle"]:
channel = ctx.guild.get_channel(settings.get(f"{alert_type}_alerts"))
name = channel.name if channel else None
alerts[alert_type] = name
fmt = f"""
**Notification Settings**
birthday_notifications
*Notify on the birthday that users in this guild have saved*
**{settings.get("birthday_notifications")}**
welcome_notifications
*Notify when someone has joined this guild*
**{settings.get("welcome_notifications")}**
goodbye_notifications
*Notify when someone has left this guild
**{settings.get("goodbye_notifications")}**
welcome_msg
*A message that can be customized and used when someone joins the server*
**{"Set" if settings.get("welcome_msg") is not None else "Not set"}**
goodbye_msg
*A message that can be customized and used when someone leaves the server*
**{"Set" if settings.get("goodbye_msg") is not None else "Not set"}**
**Alert Channels**
default_alerts
*The channel to default alert messages to*
**{alerts.get("default_alerts")}**
welcome_alerts
*The channel to send welcome alerts to (when someone joins the server)*
**{alerts.get("welcome_alerts")}**
goodbye_alerts
*The channel to send goodbye alerts to (when someone leaves the server)*
**{alerts.get("goodbye_alerts")}**
picarto_alerts
*The channel to send Picarto alerts to (when a channel the server follows goes on/offline)*
**{alerts.get("picarto_alerts")}**
birthday_alerts
*The channel to send birthday alerts to (on the day of someone's birthday)*
**{alerts.get("birthday_alerts")}**
raffle_alerts
*The channel to send alerts for server raffles to*
**{alerts.get("raffle_alerts")}**
**Misc Settings**
followed_picarto_channels
*Channels for the bot to "follow" and notify this server when they go live*
**{len(settings.get("followed_picarto_channels"))}**
ignored_channels
*Channels that the bot ignores*
**{len(settings.get("ignored_channels"))}**
ignored_members
*Members that the bot ignores*
**{len(settings.get("ignored_members"))}**
rules
*Rules for this server*
**{len(settings.get("rules"))}**
assignable_roles
*Roles that can be self-assigned by users*
**{len(settings.get("assignable_roles"))}**
custom_battles
*Possible outcomes to battles that can be received on this server*
**{len(settings.get("custom_battles"))}**
custom_hugs
*Possible outcomes to hugs that can be received on this server*
**{len(settings.get("custom_hugs"))}**
""".strip()
embed = discord.Embed(title=f"Configuration for {ctx.guild.name}", description=fmt)
embed.set_image(url=ctx.guild.icon_url)
await ctx.send(embed=embed)
@config.command(name="set", aliases=["add"])
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def _set_setting(self, ctx, option, *, setting):
"""Sets one of the configuration settings for this server"""
try:
coro = getattr(self, f"_handle_set_{option}")
except AttributeError:
await ctx.send(f"{option} is not a valid config option. Use {ctx.prefix}config to list all config options")
else:
# First make sure there's an entry for this guild before doing anything
try:
await ctx.bot.db.execute("INSERT INTO guilds(id) VALUES ($1)", ctx.guild.id)
except UniqueViolationError:
pass
try:
await coro(ctx, setting=setting)
except WrongSettingType as exc:
await ctx.send(exc.message)
except MessageFormatError as exc:
fmt = f"""
Failed to parse the format string provided, possible keys are: {', '.join(k for k in exc.keys)}
Extraneous args provided: {', '.join(k for k in exc.original.args)}
"""
await ctx.send(fmt)
except commands.BadArgument as exc:
await ctx.send(exc)
else:
await ctx.invoke(ctx.bot.get_command("config"), opt=option)
@config.command(name="unset", aliases=["remove"])
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def _remove_setting(self, ctx, option, *, setting=None):
"""Unsets/removes an option from one of the settings."""
try:
coro = getattr(self, f"_handle_remove_{option}")
except AttributeError:
await ctx.send(f"{option} is not a valid config option. Use {ctx.prefix}config to list all config options")
else:
try:
await coro(ctx, setting=setting)
except WrongSettingType as exc:
await ctx.send(exc.message)
except commands.BadArgument:
pass
else:
await ctx.invoke(ctx.bot.get_command("config"), opt=option)
def setup(bot):
bot.add_cog(GuildConfiguration())

View file

@ -1,357 +0,0 @@
import discord
from discord.ext import commands
from . import utils
import subprocess
import glob
import random
import re
import calendar
import pendulum
import datetime
import math
class Core:
"""Core commands, these are the miscallaneous commands that don't fit into other categories'"""
def __init__(self, bot):
self.bot = bot
# This is a dictionary used to hold information about which page a certain help message is on
self.help_embeds = {}
self.results_per_page = 10
self.commands = None
def find_command(self, command):
# This method ensures the command given is valid. We need to loop through commands
# As self.bot.commands only includes parent commands
# So we are splitting the command in parts, looping through the commands
# And getting the subcommand based on the next part
# If we try to access commands of a command that isn't a group
# We'll hit an AttributeError, meaning an invalid command was given
# If we loop through and don't find anything, cmd will still be None
# And we'll report an invalid was given as well
cmd = None
for part in command.split():
try:
if cmd is None:
cmd = self.bot.commands.get(part)
else:
cmd = cmd.commands.get(part)
except AttributeError:
cmd = None
break
return cmd
@commands.command(pass_context=True)
@utils.custom_perms(send_messages=True)
async def help(self, ctx, *, message=None):
"""This command is used to provide a link to the help URL.
This can be called on a command to provide more information about that command
You can also provide a page number to pull up that page instead of the first page
EXAMPLE: !help help
RESULT: This information"""
cmd = None
page = 1
if message is not None:
# If something is provided, it can either be the page number or a command
# Try to convert to an int (the page number), if not, then a command should have been provided
try:
page = int(message)
except:
cmd = self.find_command(message)
if cmd is None:
entries = sorted(utils.get_all_commands(self.bot))
try:
pages = utils.Pages(self.bot, message=ctx.message, entries=entries)
await pages.paginate(start_page=page)
except utils.CannotPaginate as e:
await self.bot.say(str(e))
else:
# Get the description for a command
description = cmd.help
if description is not None:
# Split into examples, results, and the description itself based on the string
example = [x.replace('EXAMPLE: ', '') for x in description.split('\n') if 'EXAMPLE:' in x]
result = [x.replace('RESULT: ', '') for x in description.split('\n') if 'RESULT:' in x]
description = [x for x in description.split('\n') if x and 'EXAMPLE:' not in x and 'RESULT:' not in x]
else:
example = None
result = None
# Also get the subcommands for this command, if they exist
subcommands = [x for x in utils.get_subcommands(cmd) if x != cmd.qualified_name]
# The rest is simple, create the embed, set the thumbail to me, add all fields if they exist
embed = discord.Embed(title=cmd.qualified_name)
embed.set_thumbnail(url=self.bot.user.avatar_url)
if description:
embed.add_field(name="Description", value="\n".join(description), inline=False)
if example:
embed.add_field(name="Example", value="\n".join(example), inline=False)
if result:
embed.add_field(name="Result", value="\n".join(result), inline=False)
if subcommands:
embed.add_field(name='Subcommands', value="\n".join(subcommands), inline=False)
await self.bot.say(embed=embed)
@commands.command()
@utils.custom_perms(send_messages=True)
async def motd(self, *, date=None):
"""This command can be used to print the current MOTD (Message of the day)
This will most likely not be updated every day, however messages will still be pushed to this every now and then
EXAMPLE: !motd
RESULT: 'This is an example message of the day!'"""
if date is None:
motd = await utils.get_content('motd')
try:
# Lets set this to the first one in the list first
latest_motd = motd[0]
for entry in motd:
d = pendulum.parse(entry['date'])
# Check if the date for this entry is newer than our currently saved latest entry
if d > pendulum.parse(latest_motd['date']):
latest_motd = entry
date = latest_motd['date']
motd = latest_motd['motd']
# This will be hit if we do not have any entries for motd
except TypeError:
await self.bot.say("No message of the day!")
else:
fmt = "Last updated: {}\n\n{}".format(date, motd)
await self.bot.say(fmt)
else:
try:
motd = await utils.get_content('motd', str(pendulum.parse(date).date()))
date = motd['date']
motd = motd['motd']
fmt = "Message of the day for {}:\n\n{}".format(date, motd)
await self.bot.say(fmt)
# This one will be hit if we return None for that day
except TypeError:
await self.bot.say("No message of the day for {}!".format(date))
# This will be hit if pendulum fails to parse the date passed
except ValueError:
now = pendulum.utcnow().to_date_string()
await self.bot.say("Invalid date format! Try like {}".format(now))
@commands.command()
@utils.custom_perms(send_messages=True)
async def calendar(self, month: str = None, year: int = None):
"""Provides a printout of the current month's calendar
Provide month and year to print the calendar of that year and month
EXAMPLE: !calendar january 2011"""
# calendar takes in a number for the month, not the words
# so we need this dictionary to transform the word to the number
months = {
"january": 1,
"february": 2,
"march": 3,
"april": 4,
"may": 5,
"june": 6,
"july": 7,
"august": 8,
"september": 9,
"october": 10,
"november": 11,
"december": 12
}
# In month was not passed, use the current month
if month is None:
month = datetime.date.today().month
else:
month = months.get(month.lower())
if month is None:
await self.bot.say("Please provide a valid Month!")
return
# If year was not passed, use the current year
if year is None:
year = datetime.datetime.today().year
# Here we create the actual "text" calendar that we are printing
cal = calendar.TextCalendar().formatmonth(year, month)
await self.bot.say("```\n{}```".format(cal))
@commands.command(enabled=False)
@utils.custom_perms(send_messages=True)
async def info(self):
"""This command can be used to print out some of my information"""
# fmt is a dictionary so we can set the key to it's output, then print both
# The only real use of doing it this way is easier editing if the info
# in this command is changed
bot_data = await utils.get_content('bot_data')
total_data = {'member_count': 0,
'server_count': 0}
for entry in bot_data:
total_data['member_count'] += entry['member_count']
total_data['server_count'] += entry['server_count']
# Create the original embed object
opts = {'title': 'Dev Server',
'description': 'Join the server above for any questions/suggestions about me.',
'url': utils.dev_server}
embed = discord.Embed(**opts)
# Add the normal values
embed.add_field(name='Total Servers', value=total_data['server_count'])
embed.add_field(name='Total Members', value=total_data['member_count'])
# Count the variable values; hangman, tictactoe, etc.
hm_games = len(self.bot.get_cog('Hangman').games)
ttt_games = len(self.bot.get_cog('TicTacToe').boards)
bj_games = len(self.bot.get_cog('Blackjack').games)
count_battles = 0
for battles in self.bot.get_cog('Interaction').battles.values():
count_battles += len(battles)
if hm_games:
embed.add_field(name='Total Hangman games running', value=hm_games)
if ttt_games:
embed.add_field(name='Total TicTacToe games running', value=ttt_games)
if count_battles:
embed.add_field(name='Total battles games running', value=count_battles)
if bj_games:
embed.add_field(name='Total blackjack games running', value=bj_games)
embed.add_field(name='Uptime', value=(pendulum.utcnow() - self.bot.uptime).in_words())
embed.set_footer(text=self.bot.description)
await self.bot.say(embed=embed)
@commands.command()
@utils.custom_perms(send_messages=True)
async def uptime(self):
"""Provides a printout of the current bot's uptime
EXAMPLE: !uptime
RESULT: A BAJILLION DAYS"""
await self.bot.say("Uptime: ```\n{}```".format((pendulum.utcnow() - self.bot.uptime).in_words()))
@commands.command(aliases=['invite'])
@utils.custom_perms(send_messages=True)
async def addbot(self):
"""Provides a link that you can use to add me to a server
EXAMPLE: !addbot
RESULT: http://discord.gg/yo_mama"""
perms = discord.Permissions.none()
perms.read_messages = True
perms.send_messages = True
perms.manage_roles = True
perms.ban_members = True
perms.kick_members = True
perms.manage_messages = True
perms.embed_links = True
perms.read_message_history = True
perms.attach_files = True
app_info = await self.bot.application_info()
await self.bot.say("Use this URL to add me to a server that you'd like!\n{}"
.format(discord.utils.oauth_url(app_info.id, perms)))
@commands.command()
@utils.custom_perms(send_messages=True)
async def doggo(self):
"""Use this to print a random doggo image.
EXAMPLE: !doggo
RESULT: A beautiful picture of a dog o3o"""
# Find a random image based on how many we currently have
f = random.SystemRandom().choice(glob.glob('images/doggo*'))
with open(f, 'rb') as f:
await self.bot.upload(f)
@commands.command()
@utils.custom_perms(send_messages=True)
async def snek(self):
"""Use this to print a random snek image.
EXAMPLE: !snek
RESULT: A beautiful picture of a snek o3o"""
# Find a random image based on how many we currently have
f = random.SystemRandom().choice(glob.glob('images/snek*'))
with open(f, 'rb') as f:
await self.bot.upload(f)
@commands.command()
@utils.custom_perms(send_messages=True)
async def joke(self):
"""Prints a random riddle
EXAMPLE: !joke
RESULT: An absolutely terrible joke."""
# Use the fortune riddles command because it's funny, I promise
fortune_command = "/usr/bin/fortune riddles"
while True:
try:
fortune = subprocess.check_output(
fortune_command.split()).decode("utf-8")
await self.bot.say(fortune)
except discord.HTTPException:
pass
else:
break
@commands.command(pass_context=True)
@utils.custom_perms(send_messages=True)
async def roll(self, ctx, notation: str = "d6"):
"""Rolls a die based on the notation given
Format should be #d#
EXAMPLE: !roll d50
RESULT: 51 :^)"""
# Use regex to get the notation based on what was provided
try:
# We do not want to try to convert the dice, because we want d# to
# be a valid notation
dice = re.search("(\d*)d(\d*)", notation).group(1)
num = int(re.search("(\d*)d(\d*)", notation).group(2))
# Check if something like ed3 was provided, or something else entirely
# was provided
except (AttributeError, ValueError):
await self.bot.say("Please provide the die notation in #d#!")
return
# Dice will be None if d# was provided, assume this means 1d#
dice = dice or 1
# Since we did not try to convert to int before, do it now after we
# have it set
dice = int(dice)
if dice > 10:
await self.bot.say("I'm not rolling more than 10 dice, I have tiny hands")
return
if num > 100:
await self.bot.say("What die has more than 100 sides? Please, calm down")
return
if num <= 1:
await self.bot.say("A {} sided die? You know that's impossible right?".format(num))
return
nums = [random.SystemRandom().randint(1, num) for i in range(0, int(dice))]
total = sum(nums)
value_str = ", ".join("{}".format(x) for x in nums)
if dice == 1:
fmt = '{0.message.author.name} has rolled a {2} sided die and got the number {3}!'
else:
fmt = '{0.message.author.name} has rolled {1}, {2} sided dice and got the numbers {3}, for a total of {4}!'
await self.bot.say(fmt.format(ctx, dice, num, value_str, total))
def setup(bot):
bot.add_cog(Core(bot))

View file

@ -1,165 +0,0 @@
from .utils import checks
import discord
from discord.ext import commands
class Music:
"""
This cog is simply created in order to add all commands in the playlist cog
in case 'this' instance of the bot has not loaded the playlist cog.
This is useful to have the possiblity to split the music and text commands,
And still use commands that require another command to be passed
from the instance that hasn't loaded the playlist cog
"""
def __init__(self, bot):
self.bot = bot
async def on_voice_state_update(self, before, after):
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def progress(self, ctx):
"""Provides the progress of the current song
EXAMPLE: !progress
RESULT: 532 minutes! (Hopefully not)"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def join(self, ctx, *, channel: discord.Channel):
"""Joins a voice channel.
EXAMPLE: !join Music
RESULT: I'm in the Music voice channel!"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def summon(self, ctx):
"""Summons the bot to join your voice channel.
EXAMPLE: !summon
RESULT: I'm in your voice channel!"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def play(self, ctx, *, song: str):
"""Plays a song.
If there is a song currently in the queue, then it is
queued until the next song is done playing.
This command automatically searches as well from YouTube.
The list of supported sites can be found here:
https://rg3.github.io/youtube-dl/supportedsites.html
EXAMPLE: !play Song by Band
RESULT: Song by Band will be queued to play!
"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(kick_members=True)
async def volume(self, ctx, value: int = None):
"""Sets the volume of the currently playing song.
EXAMPLE: !volume 50
RESULT: My volume is now set to 50"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(kick_members=True)
async def pause(self, ctx):
"""Pauses the currently played song.
EXAMPLE: !pause
RESULT: I'm paused!"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(kick_members=True)
async def resume(self, ctx):
"""Resumes the currently played song.
EXAMPLE: !resume
RESULT: Ain't paused no more!"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(kick_members=True)
async def stop(self, ctx):
"""Stops playing audio and leaves the voice channel.
This also clears the queue.
EXAMPLE: !stop
RESULT: No more music"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def eta(self, ctx):
"""Provides an ETA on when your next song will play
EXAMPLE: !eta
RESULT: 5,000 days! Lol have fun"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def queue(self, ctx):
"""Provides a printout of the songs that are in the queue.
\N{LEFTWARDS BLACK ARROW}: Goes to the previous page
\N{BLACK RIGHTWARDS ARROW}: Goes to the next page
\N{DOWNWARDS BLACK ARROW}: Moves the current song showing back in the queue
\N{UPWARDS BLACK ARROW}: Moves the current song showing up in the queue
\N{CROSS MARK}: Removes the current song showing from the queue
EXAMPLE: !queue
RESULT: A list of shitty songs you probably don't wanna listen to"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def queuelength(self, ctx):
"""Prints the length of the queue
EXAMPLE: !queuelength
RESULT: Probably 10 songs"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def skip(self, ctx):
"""Vote to skip a song. The song requester can automatically skip.
approximately 1/3 of the members in the voice channel
are required to vote to skip for the song to be skipped.
EXAMPLE: !skip
RESULT: You probably still have to wait for others to skip...have fun listening still
"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(kick_members=True)
async def modskip(self, ctx):
"""Forces a song skip, can only be used by a moderator
EXAMPLE: !modskip
RESULT: No more terrible song :D"""
pass
@commands.command(pass_context=True, no_pm=True, enabled=False)
@checks.custom_perms(send_messages=True)
async def playing(self, ctx):
"""Shows info about the currently played song.
EXAMPLE: !playing
RESULT: Information about the song that's currently playing!"""
pass
def setup(bot):
bot.add_cog(Music(bot))

View file

@ -1,33 +1,32 @@
from .utils import config
import aiohttp
import logging
import json
import discord
from utils import config
from discord.ext import commands
log = logging.getLogger()
discord_bots_url = 'https://bots.discord.pw/api'
discord_bots_url = 'https://bots.discord.pw/api/bots/{}/stats'
discordbots_url = "https://discordbots.org/api/bots/{}/stats"
carbonitex_url = 'https://www.carbonitex.net/discord/data/botdata.php'
class StatsUpdate:
"""This is used purely to update stats information for carbonitex and botx.discord.pw"""
class StatsUpdate(commands.Cog):
"""This is used purely to update stats information for the bot sites"""
def __init__(self, bot):
self.bot = bot
self.session = aiohttp.ClientSession()
def __unload(self):
def cog_unload(self):
self.bot.loop.create_task(self.session.close())
async def update(self):
# Currently disabled
return
server_count = 0
data = await config.get_content('bot_data')
for entry in data:
server_count += entry.get('server_count')
server_count = len(self.bot.guilds)
# Carbonitex request
carbon_payload = {
'key': config.carbon_key,
'servercount': server_count
@ -36,6 +35,7 @@ class StatsUpdate:
async with self.session.post(carbonitex_url, data=carbon_payload) as resp:
log.info('Carbonitex statistics returned {} for {}'.format(resp.status, carbon_payload))
# Discord.bots.pw request
payload = json.dumps({
'server_count': server_count
})
@ -45,74 +45,89 @@ class StatsUpdate:
'content-type': 'application/json'
}
url = '{}/bots/{}/stats'.format(discord_bots_url, self.bot.user.id)
url = discord_bots_url.format(self.bot.user.id)
async with self.session.post(url, data=payload, headers=headers) as resp:
log.info('bots.discord.pw statistics returned {} for {}'.format(resp.status, payload))
async def on_server_join(self, server):
return
r_filter = {'shard_id': config.shard_id}
server_count = len(self.bot.servers)
member_count = len(set(self.bot.get_all_members()))
entry = {'server_count': server_count, 'member_count': member_count, "shard_id": config.shard_id}
# Check if this was successful, if it wasn't, that means a new shard was added and we need to add that entry
if not await config.update_content('bot_data', entry, r_filter):
await config.add_content('bot_data', entry, r_filter)
self.bot.loop.create_task(self.update())
# discordbots.com request
url = discordbots_url.format(self.bot.user.id)
payload = {
"server_count": server_count
}
async def on_server_leave(self, server):
return
r_filter = {'shard_id': config.shard_id}
server_count = len(self.bot.servers)
member_count = len(set(self.bot.get_all_members()))
entry = {'server_count': server_count, 'member_count': member_count, "shard_id": config.shard_id}
# Check if this was successful, if it wasn't, that means a new shard was added and we need to add that entry
if not await config.update_content('bot_data', entry, r_filter):
await config.add_content('bot_data', entry, r_filter)
self.bot.loop.create_task(self.update())
headers = {
"Authorization": config.discordbots_key
}
async with self.session.post(url, data=payload, headers=headers) as resp:
log.info('discordbots.com statistics retruned {} for {}'.format(resp.status, payload))
@commands.Cog.listener()
async def on_guild_join(self, _):
await self.update()
@commands.Cog.listener()
async def on_guild_leave(self, _):
await self.update()
@commands.Cog.listener()
async def on_ready(self):
return
r_filter = {'shard_id': config.shard_id}
server_count = len(self.bot.servers)
member_count = len(set(self.bot.get_all_members()))
entry = {'server_count': server_count, 'member_count': member_count, "shard_id": config.shard_id}
# Check if this was successful, if it wasn't, that means a new shard was added and we need to add that entry
if not await config.update_content('bot_data', entry, r_filter):
await config.add_content('bot_data', entry, r_filter)
self.bot.loop.create_task(self.update())
await self.update()
@commands.Cog.listener()
async def on_member_join(self, member):
server = member.server
server_settings = await config.get_content('server_settings', server.id)
query = """
SELECT
COALESCE(welcome_alerts, default_alerts) AS channel,
welcome_msg AS msg,
join_role as role,
welcome_notifications as notify
FROM
guilds
WHERE
id = $1
"""
settings = await self.bot.db.fetchrow(query, member.guild.id)
if settings:
message = settings['msg'] or "Welcome to the '{server}' server {member}!"
if settings["notify"]:
try:
channel = member.guild.get_channel(settings['channel'])
await channel.send(message.format(server=member.guild.name, member=member.mention))
# Forbidden for if the channel has send messages perms off
# HTTP Exception to catch any weird happenings
# Attribute Error catches when a channel is set, but that channel doesn't exist any more
except (discord.Forbidden, discord.HTTPException, AttributeError):
pass
try:
join_leave_on = server_settings['join_leave']
if join_leave_on:
channel_id = server_settings.get('notification_channel') or member.server.id
else:
return
except (IndexError, TypeError, KeyError):
return
channel = server.get_channel(channel_id)
await self.bot.send_message(channel, "Welcome to the '{0.server.name}' server {0.mention}!".format(member))
try:
role = member.guild.get_role(settings['role'])
await member.add_roles(role)
except (discord.Forbidden, discord.HTTPException, AttributeError):
pass
@commands.Cog.listener()
async def on_member_remove(self, member):
server = member.server
server_settings = await config.get_content('server_settings', server.id)
try:
join_leave_on = server_settings['join_leave']
if join_leave_on:
channel_id = server_settings.get('notification_channel') or member.server.id
else:
return
except (IndexError, TypeError, KeyError):
return
channel = server.get_channel(channel_id)
await self.bot.send_message(channel, "{0} has left the server, I hope it wasn't because of something I said :c".format(member.display_name))
query = """
SELECT
COALESCE(goodbye_alerts, default_alerts) AS channel,
goodbye_msg AS msg
FROM
guilds
WHERE
goodbye_notifications = True
AND
id = $1
AND
COALESCE(goodbye_alerts, default_alerts) IS NOT NULL
"""
settings = await self.bot.db.fetchrow(query, member.guild.id)
if settings:
message = settings['msg'] or "{member} has left the server, I hope it wasn't because of something I said :c"
channel = member.guild.get_channel(settings['channel'])
try:
await channel.send(message.format(server=member.guild.name, member=member.mention))
except (discord.Forbidden, discord.HTTPException, AttributeError):
pass
def setup(bot):

View file

@ -1,43 +1,11 @@
from discord.ext import commands
from discord.ext.commands.cooldowns import BucketType
import discord
from .utils import checks
from utils import checks
import re
import random
phrases = ["Eat My Hat", "Par For the Course", "Raining Cats and Dogs", "Roll With the Punches",
"Curiosity Killed The Cat", "Man of Few Words", "Cry Over Spilt Milk", "Scot-free", "Rain on Your Parade",
"Go For Broke", "Shot In the Dark", "Mountain Out of a Molehill", "Jaws of Death", "A Dime a Dozen",
"Jig Is Up", "Elvis Has Left The Building", "Wake Up Call", "Jumping the Gun", "Up In Arms",
"Beating Around the Bush", "Flea Market", "Playing For Keeps", "Cut To The Chase", "Fight Fire With Fire",
"Keep Your Shirt On", "Poke Fun At", "Everything But The Kitchen Sink", "Jaws of Life",
"What Goes Up Must Come Down", "Give a Man a Fish", "Plot Thickens - The",
"Not the Sharpest Tool in the Shed", "Needle In a Haystack", "Right Off the Bat", "Throw In the Towel",
"Down To Earth", "Lickety Split", "I Smell a Rat", "Long In The Tooth",
"You Can't Teach an Old Dog New Tricks", "Back To the Drawing Board", "Down For The Count",
"On the Same Page", "Under Your Nose", "Cut The Mustard",
"If You Can't Stand the Heat, Get Out of the Kitchen", "Knock Your Socks Off", "Playing Possum",
"No-Brainer", "Money Doesn't Grow On Trees", "In a Pickle", "In the Red", "Fit as a Fiddle", "Hear, Hear",
"Hands Down", "Off One's Base", "Wild Goose Chase", "Keep Your Eyes Peeled", "A Piece of Cake",
"Foaming At The Mouth", "Go Out On a Limb", "Quick and Dirty", "Hit Below The Belt",
"Birds of a Feather Flock Together", "Wouldn't Harm a Fly", "Son of a Gun",
"Between a Rock and a Hard Place", "Down And Out", "Cup Of Joe", "Down To The Wire",
"Don't Look a Gift Horse In The Mouth", "Talk the Talk", "Close But No Cigar",
"Jack of All Trades Master of None", "High And Dry", "A Fool and His Money are Soon Parted",
"Every Cloud Has a Silver Lining", "Tough It Out", "Under the Weather", "Happy as a Clam",
"An Arm and a Leg", "Read 'Em and Weep", "Right Out of the Gate", "Know the Ropes",
"It's Not All It's Cracked Up To Be", "On the Ropes", "Burst Your Bubble", "Mouth-watering",
"Swinging For the Fences", "Fool's Gold", "On Cloud Nine", "Fish Out Of Water", "Ring Any Bells?",
"There's No I in Team", "Ride Him, Cowboy!", "Top Drawer", "No Ifs, Ands, or Buts",
"You Can't Judge a Book By Its Cover", "Don't Count Your Chickens Before They Hatch", "Cry Wolf",
"Beating a Dead Horse", "Goody Two-Shoes", "Heads Up", "Drawing a Blank", "Keep On Truckin'", "Tug of War",
"Short End of the Stick", "Hard Pill to Swallow", "Back to Square One", "Love Birds", "Dropping Like Flies",
"Break The Ice", "Knuckle Down", "Lovey Dovey", "Greased Lightning", "Let Her Rip", "All Greek To Me",
"Two Down, One to Go", "What Am I, Chopped Liver?", "It's Not Brain Surgery", "Like Father Like Son",
"Easy As Pie", "Elephant in the Room", "Quick On the Draw", "Barking Up The Wrong Tree",
"A Chip on Your Shoulder", "Put a Sock In It", "Quality Time", "Yada Yada", "Head Over Heels",
"My Cup of Tea", "Ugly Duckling", "Drive Me Nuts", "When the Rubber Hits the Road"]
import asyncio
class Game:
@ -96,29 +64,35 @@ class Game:
return fmt
class Hangman:
def __init__(self, bot):
self.bot = bot
self.games = {}
class Hangman(commands.Cog):
"""Pretty self-explanatory"""
games = {}
pending_games = []
def create(self, word, ctx):
# Create a new game, then save it as the server's game
game = Game(word)
self.games[ctx.message.server.id] = game
self.games[ctx.message.guild.id] = game
game.author = ctx.message.author.id
return game
@commands.group(aliases=['hm'], pass_context=True, no_pm=True, invoke_without_command=True)
@commands.group(aliases=['hm'], invoke_without_command=True)
@commands.guild_only()
@commands.cooldown(1, 7, BucketType.user)
@checks.custom_perms(send_messages=True)
@checks.can_run(send_messages=True)
async def hangman(self, ctx, *, guess):
"""Makes a guess towards the server's currently running hangman game
EXAMPLE: !hangman e (or) !hangman The Phrase!
RESULT: Hopefully a win!"""
game = self.games.get(ctx.message.server.id)
game = self.games.get(ctx.message.guild.id)
if not game:
ctx.command.reset_cooldown(ctx)
await self.bot.say("There are currently no hangman games running!")
await ctx.send("There are currently no hangman games running!")
return
if game.author == ctx.message.author.id:
await ctx.send("You cannot guess on your own hangman game!")
return
# Check if we are guessing a letter or a phrase. Only one letter can be guessed at a time
@ -126,9 +100,9 @@ class Hangman:
# We're creating a fmt variable, so that we can add a message for if a guess was correct or not
# And also add a message for a loss/win
if len(guess) == 1:
if guess in game.guessed_letters:
if guess.lower() in game.guessed_letters:
ctx.command.reset_cooldown(ctx)
await self.bot.say("That letter has already been guessed!")
await ctx.send("That letter has already been guessed!")
# Return here as we don't want to count this as a failure
return
if game.guess_letter(guess):
@ -142,18 +116,19 @@ class Hangman:
fmt = "Sorry that's not the correct phrase..."
if game.win():
fmt += " You guys got it! The word was `{}`".format(game.word)
del self.games[ctx.message.server.id]
fmt += " You guys got it! The phrase was `{}`".format(game.word)
del self.games[ctx.message.guild.id]
elif game.failed():
fmt += " Sorry, you guys failed...the word was `{}`".format(game.word)
del self.games[ctx.message.server.id]
fmt += " Sorry, you guys failed...the phrase was `{}`".format(game.word)
del self.games[ctx.message.guild.id]
else:
fmt += str(game)
await self.bot.say(fmt)
await ctx.send(fmt)
@hangman.command(name='create', aliases=['start'], no_pm=True, pass_context=True)
@checks.custom_perms(send_messages=True)
@hangman.command(name='create', aliases=['start'])
@commands.guild_only()
@checks.can_run(send_messages=True)
async def create_hangman(self, ctx):
"""This is used to create a new hangman game
A predefined phrase will be randomly chosen as the phrase to use
@ -163,17 +138,55 @@ class Hangman:
# Only have one hangman game per server, since anyone
# In a server (except the creator) can guess towards the current game
if self.games.get(ctx.message.server.id) is not None:
await self.bot.say("Sorry but only one Hangman game can be running per server!")
if self.games.get(ctx.message.guild.id) is not None:
await ctx.send("Sorry but only one Hangman game can be running per server!")
return
if ctx.guild.id in self.pending_games:
await ctx.send("Someone has already started one, and I'm now waiting for them...")
return
game = self.create(random.SystemRandom().choice(phrases), ctx)
try:
msg = await ctx.message.author.send(
"Please respond with a phrase you would like to use for your hangman game in **{}**\n\nPlease keep "
"phrases less than 31 characters".format(
ctx.message.guild.name))
except discord.Forbidden:
await ctx.send(
"I can't message you {}! Please allow DM's so I can message you and ask for the hangman phrase you "
"want to use!".format(ctx.message.author.display_name))
return
await ctx.send("I have DM'd you {}, please respond there with the phrase you would like to setup".format(
ctx.message.author.display_name))
def check(m):
return m.channel == msg.channel and len(m.content) <= 30
self.pending_games.append(ctx.guild.id)
try:
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError:
self.pending_games.remove(ctx.guild.id)
await ctx.send(
"You took too long! Please look at your DM's as that's where I'm asking for the phrase you want to use")
return
else:
self.pending_games.remove(ctx.guild.id)
forbidden_phrases = ['stop', 'delete', 'remove', 'end', 'create', 'start']
if msg.content in forbidden_phrases:
await ctx.send("Detected forbidden hangman phrase; current forbidden phrases are: \n{}".format(
"\n".join(forbidden_phrases)))
return
game = self.create(msg.content, ctx)
# Let them know the game has started, then print the current game so that the blanks are shown
await self.bot.say(
await ctx.send(
"Alright, a hangman game has just started, you can start guessing now!\n{}".format(str(game)))
@hangman.command(name='delete', aliases=['stop', 'remove', 'end'], pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
@hangman.command(name='delete', aliases=['stop', 'remove', 'end'])
@commands.guild_only()
@checks.can_run(kick_members=True)
async def stop_game(self, ctx):
"""Force stops a game of hangman
This should realistically only be used in a situation like one player leaves
@ -181,12 +194,12 @@ class Hangman:
EXAMPLE: !hangman stop
RESULT: No more men being hung"""
if self.games.get(ctx.message.server.id) is None:
await self.bot.say("There are no Hangman games running on this server!")
if self.games.get(ctx.message.guild.id) is None:
await ctx.send("There are no Hangman games running on this server!")
return
del self.games[ctx.message.server.id]
await self.bot.say("I have just stopped the game of Hangman, a new should be able to be started now!")
del self.games[ctx.message.guild.id]
await ctx.send("I have just stopped the game of Hangman, a new should be able to be started now!")
def setup(bot):

291
cogs/images.py Normal file
View file

@ -0,0 +1,291 @@
from discord.ext import commands
import discord
import itertools
import random
import re
import math
import utils
class Images(commands.Cog):
"""Commands that post images, or look up images"""
async def horse_noodle_api(self, ctx, animal):
data = await utils.request(f"http://hrsendl.com/api/{animal}")
try:
url = data["data"]["file_url_size_large"]
filename = data["data"]["file_name"]
except (KeyError, TypeError):
return await ctx.send(f"I couldn't connect! Sorry no {animal}s right now ;w;")
else:
image = await utils.download_image(url)
f = discord.File(image, filename=filename)
try:
await ctx.send(file=f)
except discord.HTTPException:
await ctx.send(
f"File to large to send as attachment, here is the URL: {url}"
)
@commands.command(aliases=['rc'])
@utils.can_run(send_messages=True)
async def cat(self, ctx):
"""Use this to print a random cat image.
EXAMPLE: !cat
RESULT: A beautiful picture of a cat o3o"""
url = "http://thecatapi.com/api/images/get"
opts = {"format": "src"}
result = await utils.request(url, attr='url', payload=opts)
try:
image = await utils.download_image(result)
except (ValueError, AttributeError):
await ctx.send("I couldn't connect! Sorry no cats right now ;w;")
f = discord.File(image, filename=result.name)
try:
await ctx.send(file=f)
except discord.HTTPException:
await ctx.send(
f"File to large to send as attachment, here is the URL: {url}"
)
@commands.command(aliases=['dog', 'rd'])
@utils.can_run(send_messages=True)
async def doggo(self, ctx):
"""Use this to print a random doggo image.
EXAMPLE: !doggo
RESULT: A beautiful picture of a dog o3o"""
result = await utils.request('https://random.dog/woof.json')
try:
url = result.get("url")
filename = re.match("https://random.dog/(.*)", url).group(1)
except AttributeError:
await ctx.send("I couldn't connect! Sorry no dogs right now ;w;")
return
image = await utils.download_image(url)
f = discord.File(image, filename=filename)
try:
await ctx.send(file=f)
except discord.HTTPException:
await ctx.send(
f"File to large to send as attachment, here is the URL: {url}"
)
@commands.command(aliases=['snake'])
@utils.can_run(send_messages=True)
async def snek(self, ctx):
"""Use this to print a random snek image.
EXAMPLE: !snek
RESULT: A beautiful picture of a snek o3o"""
await self.horse_noodle_api(ctx, "snake")
@commands.command()
@utils.can_run(send_messages=True)
async def horse(self, ctx):
"""Use this to print a random horse image.
EXAMPLE: !horse
RESULT: A beautiful picture of a horse o3o"""
await self.horse_noodle_api(ctx, "horse")
@commands.command()
@utils.can_run(send_messages=True)
async def duck(self, ctx):
"""Use this to print a random duck image.
EXAMPLE: !duck
RESULT: A beautiful picture of a duck o3o"""
await self.horse_noodle_api(ctx, "duck")
@commands.command()
@utils.can_run(send_messages=True)
async def snail(self, ctx):
"""Use this to print a random snail image.
EXAMPLE: !snail
RESULT: A beautiful picture of a snail o3o"""
await self.horse_noodle_api(ctx, "snail")
@commands.command()
@utils.can_run(send_messages=True)
async def pleco(self, ctx):
"""Use this to print a random pleco image.
EXAMPLE: !pleco
RESULT: A beautiful picture of a pleco o3o"""
await self.horse_noodle_api(ctx, "pleco")
@commands.command()
@utils.can_run(send_messages=True)
async def moth(self, ctx):
"""Use this to print a random moth image.
EXAMPLE: !moth
RESULT: A beautiful picture of a moth o3o"""
await self.horse_noodle_api(ctx, "moth")
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def avatar(self, ctx, member: discord.Member = None):
"""Provides an image for the provided person's avatar (yours if no other member is provided)
EXAMPLE: !avatar @person
RESULT: A full image of that person's avatar"""
if member is None:
member = ctx.message.author
url = str(member.avatar_url)
if '.gif' not in url:
url = str(member.avatar_url_as(format='png'))
filename = 'avatar.png'
else:
filename = 'avatar.gif'
if ctx.message.guild.me.permissions_in(ctx.message.channel).attach_files:
filedata = await utils.download_image(url)
if filedata is None:
await ctx.send(url)
else:
try:
f = discord.File(filedata, filename=filename)
await ctx.send(file=f)
except discord.HTTPException:
await ctx.send("Sorry but that avatar is too large for me to send!")
else:
await ctx.send(url)
@commands.command()
@utils.can_run(send_messages=True)
async def derpi(self, ctx, *search: str):
"""Provides a random image from the first page of derpibooru.org for the following term
EXAMPLE: !derpi Rainbow Dash
RESULT: A picture of Rainbow Dash!"""
if len(search) > 0:
url = 'https://derpibooru.org/search.json'
# Ensure a filter was not provided, as we either want to use our own, or none (for safe pics)
query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value))
params = {'q': query}
nsfw = utils.channel_is_nsfw(ctx.message.channel)
# If this is a nsfw channel, we just need to tack on 'explicit' to the terms
# Also use the custom filter that I have setup, that blocks some certain tags
# If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit
if nsfw:
params['q'] += ", (explicit OR suggestive)"
params['filter_id'] = 95938
else:
params['q'] += ", safe"
# Lets filter out some of the "crap" that's on derpibooru by requiring an image with a score higher than 15
params['q'] += ', score.gt:15'
try:
# Get the response from derpibooru and parse the 'search' result from it
data = await utils.request(url, payload=params)
if data is None:
await ctx.send("Sorry but I failed to connect to Derpibooru!")
return
results = data['search']
except KeyError:
await ctx.send("No results with that search term, {0}!".format(ctx.message.author.mention))
return
# The first request we've made ensures there are results
# Now we can get the total count from that, and make another request based on the number of pages as well
if len(results) > 0:
# Get the total number of pages
pages = math.ceil(data['total'] / len(results))
# Set a new paramater to set which page to use, randomly based on the number of pages
params['page'] = random.SystemRandom().randint(1, pages)
data = await utils.request(url, payload=params)
if data is None:
await ctx.send("Sorry but I failed to connect to Derpibooru!")
return
# Now get the results again
results = data['search']
# Get the image link from the now random page'd and random result from that page
index = random.SystemRandom().randint(0, len(results) - 1)
# image_link = 'https://derpibooru.org/{}'.format(results[index]['id'])
image_link = 'https:{}'.format(results[index]['image'])
else:
await ctx.send("No results with that search term, {0}!".format(ctx.message.author.mention))
return
else:
# If no search term was provided, search for a random image
# .url will be the URL we end up at, not the one requested.
# https://derpibooru.org/images/random redirects to a random image, so this is exactly what we want
image_link = await utils.request('https://derpibooru.org/images/random', attr='url')
await ctx.send(image_link)
@commands.command()
@utils.can_run(send_messages=True)
async def e621(self, ctx, *, tags: str):
"""Searches for a random image from e621.net
Format for the search terms need to be 'search term 1, search term 2, etc.'
If the channel the command is ran in, is registered as a nsfw channel, this image will be explicit
EXAMPLE: !e621 dragon
RESULT: A picture of a dragon (hopefully, screw your tagging system e621)"""
# This changes the formatting for queries, so we don't
# Have to use e621's stupid formatting when using the command
tags = tags.replace(' ', '_')
tags = tags.replace(',_', ' ')
url = 'https://e621.net/posts.json'
params = {
'login': utils.config.e621_user,
'api_key': utils.config.e621_key,
'limit': 5,
'tags': tags
}
headers = {'User-Agent': utils.config.user_agent}
nsfw = utils.channel_is_nsfw(ctx.message.channel)
# e621 by default does not filter explicit content, so tack on
# safe/explicit based on if this channel is nsfw or not
params['tags'] += " rating:explicit" if nsfw else " rating:safe"
# Tack on a random order
params['tags'] += " order:random"
data = await utils.request(url, payload=params, headers=headers)
if data is None:
await ctx.send("Sorry, I had trouble connecting at the moment; please try again later")
return
# Try to find an image from the list. If there were no results, we're going to attempt to find
# A number between (0,-1) and receive an error.
# The response should be in a list format, so we'll end up getting a key error if the response was in json
# i.e. it responded with a 404/504/etc.
try:
for image in data["posts"]:
# Will support in the future
blacklist = []
tags = itertools.chain.from_iterable(image["tags"].values())
# Check if any of the tags are in the blacklist
if any(tag in tags for tag in blacklist):
continue
# If this image is fine, then send this and break
await ctx.send(image["file"]["url"])
return
except (ValueError, KeyError):
await ctx.send("No results with that tag {}".format(ctx.message.author.mention))
return
def setup(bot):
bot.add_cog(Images(bot))

View file

@ -1,186 +1,223 @@
from discord.ext import commands
from discord.ext.commands.cooldowns import BucketType
from collections import defaultdict
from . import utils
import utils
import discord
import random
import functools
battle_outcomes = \
["A meteor fell on {1}, {0} is left standing and has been declared the victor!",
"{0} has bucked {1} into a tree, even Big Mac would be impressed at that kick!",
"As they were battling, {1} was struck by lightning! {0} you lucked out this time!",
"{1} tried to dive at {0} while fighting, somehow they missed and landed in quicksand."
"Try paying more attention next time {1}",
"{1} got a little...heated during the battle and ended up getting set on fire. {0} wins by remaining cool",
"Princess Celestia came in and banished {1} to the moon. Good luck getting into any battles up there",
"{1} took an arrow to the knee, they are no longer an adventurer. Keep on adventuring {0}",
"Common sense should make it obvious not to get into battle with {0}. Apparently {1} didn't get the memo",
"{0} had a nice cup of tea with {1} over their conflict, and mutually agreed that {0} was Best Pony",
"{0} and {1} had an intense staring contest. "
"Sadly, {1} forgot to breathe and lost much morethan the staring contest",
"It appears {1} is actually a pacifist, they ran away screaming and crying. "
["A meteor fell on {loser}, {winner} is left standing and has been declared the victor!",
"{loser} was shot through the heart, and {winner} is to blame",
"{winner} has bucked {loser} into a tree, even Big Mac would be impressed at that kick!",
"As they were battling, {loser} was struck by lightning! {winner} you lucked out this time!",
"{loser} tried to dive at {winner} while fighting, somehow they missed and landed in quicksand."
"Try paying more attention next time {loser}",
"{loser} got a little...heated during the battle and ended up getting set on fire. "
"{winner} wins by remaining cool",
"Princess Celestia came in and banished {loser} to the moon. Good luck getting into any battles up there",
"{loser} took an arrow to the knee, they are no longer an adventurer. Keep on adventuring {winner}",
"Common sense should make it obvious not to get into battle with {winner}. Apparently {loser} didn't get the memo",
"{winner} had a nice cup of tea with {loser} over their conflict, and mutually agreed that {winner} was Best Pony",
"{winner} and {loser} had an intense staring contest. "
"Sadly, {loser} forgot to breathe and lost much morethan the staring contest",
"It appears {loser} is actually a pacifist, they ran away screaming and crying. "
"Maybe you should have thought of that before getting in a fight?",
"A bunch of parasprites came in and ate up the jetpack while {1} was flying with it. Those pesky critters...",
"{0} used their charm to seduce {1} to surrender.",
"{1} slipped on a banana peel and fell into a pit of spikes. That's actually impressive.",
"{0} realized it was high noon, {1} never even saw it coming.",
"{1} spontaneously combusted...lol rip",
"after many turns {0} summons exodia and {1} is sent to the shadow realm",
"{0} and {1} sit down for an intense game of chess, in the heat of the moment {0} forgot they were playing a "
"A bunch of parasprites came in and ate up the jetpack while {loser} was flying with it. Those pesky critters...",
"{winner} used their charm to seduce {loser} to surrender.",
"{loser} slipped on a banana peel and fell into a pit of spikes. That's actually impressive.",
"{winner} realized it was high noon, {loser} never even saw it coming.",
"{loser} spontaneously combusted...lol rip",
"after many turns {winner} summons exodia and {loser} is sent to the shadow realm",
"{winner} and {loser} sit down for an intense game of chess, "
"in the heat of the moment {winner} forgot they were playing a "
"game and summoned a real knight",
"{0} challenges {1} to rock paper scissors, unfortunately for {1}, {0} chose scissors and stabbed them",
"{0} goes back in time and becomes {1}'s best friend, winning without ever throwing a punch",
"{1} trips down some stairs on their way to the battle with {0}",
"{0} books {1} a one way ticket to Flugendorf prison",
"{1} was already dead",
"{1} was crushed under the weight of expectations",
"{1} was wearing a redshirt and it was their first day",
"{0} and {1} were walking along when suddenly {1} got kidnapped by a flying monkey; hope they had water with them",
"{0} brought an army to a fist fight, {1} never saw their opponent once",
"{0} used multiple simultaneous devestating defensive deep strikes to overwhelm {1}",
"{0} and {1} engage in a dance off; {0} wiped the floor with {1}",
"{1} tried to hide in the sand to catch {0} off guard, unfortunately looks like a Giant Antlion had the same "
"{winner} challenges {loser} to rock paper scissors, "
"unfortunately for {loser}, {winner} chose scissors and stabbed them",
"{winner} goes back in time and becomes {loser}'s best friend, winning without ever throwing a punch",
"{loser} trips down some stairs on their way to the battle with {winner}",
"{winner} books {loser} a one way ticket to Flugendorf prison",
"{loser} was already dead",
"{loser} was crushed under the weight of expectations",
"{loser} was wearing a redshirt and it was their first day",
"{winner} and {loser} were walking along when suddenly {loser} "
"got kidnapped by a flying monkey; hope they had water with them",
"{winner} brought an army to a fist fight, {loser} never saw their opponent once",
"{winner} used multiple simultaneous devestating defensive deep strikes to overwhelm {loser}",
"{winner} and {loser} engage in a dance off; {winner} wiped the floor with {loser}",
"{loser} tried to hide in the sand to catch {winner} off guard, "
"unfortunately looks like a Giant Antlion had the same "
"idea for him",
"{1} was busy playing trash videogames the night before the fight and collapsed before {0}",
"{0} threw a sick meme and {1} totally got PRANK'D",
"{0} and {1} go on a skiing trip together, turns out {1} forgot how to pizza french-fry",
"{0} is the cure and {1} is the disease....well {1} was the disease",
"{1} talked their mouth off at {0}...literally...",
"Looks like {1} didn't put enough points into kazoo playing, who knew they would have needed it",
"{1} was too scared by the illuminati and extra-dimensional talking horses to show up",
"{1} didn't press x enough to not die",
"{0} and {1} go fishing to settle their debate, {0} caught a sizeable fish and {1} caught a boot older than time",
"{0} did a hero landing and {1} was so surprised they gave up immediately"]
"{loser} was busy playing trash videogames the night before the fight and collapsed before {winner}",
"{winner} threw a sick meme and {loser} totally got PRANK'D",
"{winner} and {loser} go on a skiing trip together, turns out {loser} forgot how to pizza french-fry",
"{winner} is the cure and {loser} is the disease....well {loser} was the disease",
"{loser} talked their mouth off at {winner}...literally...",
"Looks like {loser} didn't put enough points into kazoo playing, who knew they would have needed it",
"{loser} was too scared by the illuminati and extra-dimensional talking horses to show up",
"{loser} didn't press x enough to not die",
"{winner} and {loser} go fishing to settle their debate, "
"{winner} caught a sizeable fish and {loser} caught a boot older than time",
"{winner} did a hero landing and {loser} was so surprised they gave up immediately"]
hugs = \
["*hugs {}.*",
"*tackles {} for a hug.*",
"*drags {} into her dungeon where hugs ensue*",
"*pulls {} to the side for a warm hug*",
"*goes out to buy a big enough blanket to embrace {}*",
"*hard codes an electric hug to {}*",
"*hires mercenaries to take {} out....to a nice dinner*",
"*pays $10 to not touch {}*",
"*clones herself to create a hug pile with {}*",
"*orders an airstrike of hugs {}*",
"*glomps {}*",
"*hears a knock at her door, opens it, finds {} and hugs them excitedly*",
"*goes in for a punch but misses and ends up hugging {}*",
"*hugs {} from behind*",
"*denies a hug from {}*",
"*does a hug to {}*",
"*lets {} cuddle nonchalantly*",
"*cuddles {}*",
"*burrows underground and pops up underneath {} she hugs their legs.*",
"*approaches {} after having gone to the gym for several months and almost crushes them.*"]
["*hugs {user}.*",
"*tackles {user} for a hug.*",
"*drags {user} into her dungeon where hugs ensue*",
"*pulls {user} to the side for a warm hug*",
"*goes out to buy a big enough blanket to embrace {user}*",
"*hard codes an electric hug to {user}*",
"*hires mercenaries to take {user} out....to a nice dinner*",
"*pays $10 to not touch {user}*",
"*clones herself to create a hug pile with {user}*",
"*orders an airstrike of hugs {user}*",
"*glomps {user}*",
"*hears a knock at her door, opens it, finds {user} and hugs them excitedly*",
"*goes in for a punch but misses and ends up hugging {user}*",
"*hugs {user} from behind*",
"*denies a hug from {user}*",
"*does a hug to {user}*",
"*lets {user} cuddle nonchalantly*",
"*cuddles {user}*",
"*burrows underground and pops up underneath {user} she hugs their legs.*",
"*approaches {user} after having gone to the gym for several months and almost crushes them.*"]
class Interaction:
class Interaction(commands.Cog):
"""Commands that interact with another user"""
def __init__(self, bot):
self.bot = bot
# Format for battles: {'serverid': {'player1': 'player2', 'player1': 'player2'}}
self.battles = {}
battles = defaultdict(list)
def user_battling(self, ctx, player2=None):
battling = self.battles.get(ctx.message.server.id)
def get_receivers_battle(self, receiver):
for battle in self.battles.get(receiver.guild.id, []):
if battle.is_receiver(receiver):
return battle
# If no one is battling, obviously the user is not battling
if battling is None:
return False
# Check if the author is battling
if ctx.message.author.id in battling.values() or ctx.message.author.id in battling.keys():
return True
# Check if the player2 was provided, if they are check if they're in the list
if player2 and (player2.id in battling.values() or player2.id in battling.keys()):
return True
# If neither are found, no one is battling
return False
def can_initiate_battle(self, player):
for battle in self.battles.get(player.guild.id, []):
if battle.is_initiator(player):
return False
return True
def can_receive_battle(self, player):
for battle in self.battles.get(player.guild.id, []):
if battle.is_receiver(player):
return False
return True
def start_battle(self, initiator, receiver):
battle = Battle(initiator, receiver)
self.battles[initiator.guild.id].append(battle)
return battle
# Handles removing the author from the dictionary of battles
def battling_off(self, ctx):
battles = self.battles.get(ctx.message.server.id) or {}
player_id = ctx.message.author.id
# Create a new dictionary, exactly the way the last one was setup
# But don't include any that have the author's ID
self.battles[ctx.message.server.id] = {p1: p2 for p1, p2 in battles.items() if
not p2 == player_id and not p1 == player_id}
def battling_off(self, battle):
for guild, battles in self.battles.items():
if battle in battles:
battles.remove(battle)
return
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def hug(self, ctx, user: discord.Member = None):
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def hug(self, ctx, user=None):
"""Makes me hug a person!
EXAMPLE: !hug @Someone
RESULT: I hug the shit out of that person"""
if ctx.message.mention_everyone:
await ctx.send("Your arms aren't big enough")
return
if user is None:
user = ctx.message.author
fmt = random.SystemRandom().choice(hugs)
await self.bot.say(fmt.format(user.display_name))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def avatar(self, ctx, member: discord.Member = None):
"""Provides an image for the provided person's avatar (yours if no other member is provided)
EXAMPLE: !avatar @person
RESULT: A full image of that person's avatar"""
if member is None:
member = ctx.message.author
url = member.avatar_url
if ctx.message.server.me.permissions_in(ctx.message.channel).attach_files:
file = await utils.download_image(url)
if file is None:
await self.bot.say(url)
else:
if '.gif' in url:
filename = 'avatar.gif'
else:
filename = 'avatar.webp'
file = utils.convert_to_jpeg(file)
await self.bot.upload(file, filename=filename)
user = ctx.author
else:
await self.bot.say(url)
converter = commands.converter.MemberConverter()
try:
user = await converter.convert(ctx, user)
except commands.converter.BadArgument:
await ctx.send("Error: Could not find user: {}".format(user))
return
@commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
@commands.cooldown(1, 180, BucketType.user)
@utils.custom_perms(send_messages=True)
async def battle(self, ctx, player2: discord.Member):
settings = await ctx.bot.db.fetchrow(
"SELECT custom_hugs, include_default_hugs FROM guilds WHERE id = $1",
ctx.guild.id
)
msgs = hugs.copy()
if settings:
custom_msgs = settings["custom_hugs"]
default_on = settings["include_default_hugs"]
if custom_msgs:
if default_on or default_on is None:
msgs += custom_msgs
else:
msgs = custom_msgs
# Otherwise we simply just want to use the default, no matter what the default setting is
fmt = random.SystemRandom().choice(msgs)
await ctx.send(fmt.format(user=user.display_name))
@commands.command(aliases=['1v1'])
@commands.guild_only()
@commands.cooldown(1, 20, BucketType.user)
@utils.can_run(send_messages=True)
async def battle(self, ctx, player2=None):
"""Challenges the mentioned user to a battle
EXAMPLE: !battle @player2
RESULT: A battle to the death"""
if ctx.message.author.id == player2.id:
ctx.command.reset_cooldown(ctx)
await self.bot.say("Why would you want to battle yourself? Suicide is not the answer")
# First check if everyone was mentioned
if ctx.message.mention_everyone:
await ctx.send("You want to battle {} people? Good luck with that...".format(
len(ctx.channel.members) - 1)
)
return
if self.bot.user.id == player2.id:
ctx.command.reset_cooldown(ctx)
await self.bot.say("I always win, don't even try it.")
# Then check if nothing was provided
if player2 is None:
await ctx.send("Who are you trying to battle...?")
return
if self.user_battling(ctx, player2):
else:
# Otherwise, try to convert to an actual member
converter = commands.converter.MemberConverter()
try:
player2 = await converter.convert(ctx, player2)
except commands.converter.BadArgument:
await ctx.send("Error: Could not find user: {}".format(player2))
return
# Then check if the person used is the author
if ctx.author.id == player2.id:
ctx.command.reset_cooldown(ctx)
await self.bot.say("You or the person you are trying to battle is already in a battle!")
await ctx.send("Why would you want to battle yourself? Suicide is not the answer")
return
# Check if the person battled is me
if ctx.bot.user.id == player2.id:
ctx.command.reset_cooldown(ctx)
await ctx.send("I always win, don't even try it.")
return
# Next two checks are to see if the author or person battled can be battled
if not self.can_initiate_battle(ctx.author):
ctx.command.reset_cooldown(ctx)
await ctx.send("You are already battling someone!")
return
if not self.can_receive_battle(player2):
ctx.command.reset_cooldown(ctx)
await ctx.send("{} is already being challenged to a battle!".format(player2))
return
# Add the author and player provided in a new battle
battles = self.battles.get(ctx.message.server.id) or {}
battles[ctx.message.author.id] = player2.id
self.battles[ctx.message.server.id] = battles
battle = self.start_battle(ctx.author, player2)
fmt = "{0.message.author.mention} has challenged you to a battle {1.mention}\n" \
"{0.prefix}accept or {0.prefix}decline"
fmt = f"{ctx.author.mention} has challenged you to a battle {player2.mention}\n" \
f"{ctx.prefix}accept or {ctx.prefix}decline"
# Add a call to turn off battling, if the battle is not accepted/declined in 3 minutes
self.bot.loop.call_later(180, self.battling_off, ctx)
await self.bot.say(fmt.format(ctx, player2))
part = functools.partial(self.battling_off, battle)
ctx.bot.loop.call_later(180, part)
await ctx.send(fmt)
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def accept(self, ctx):
"""Accepts the battle challenge
@ -188,31 +225,136 @@ class Interaction:
RESULT: Hopefully the other person's death"""
# This is a check to make sure that the author is the one being BATTLED
# And not the one that started the battle
battles = self.battles.get(ctx.message.server.id) or {}
p1 = [p1_id for p1_id, p2_id in battles.items() if p2_id == ctx.message.author.id]
if len(p1) == 0:
await self.bot.say("You are not currently being challenged to a battle!")
battle = self.get_receivers_battle(ctx.author)
if battle is None:
await ctx.send("You are not currently being challenged to a battle!")
return
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
battleP2 = ctx.message.author
if ctx.guild.get_member(battle.initiator.id) is None:
await ctx.send("The person who challenged you to a battle has apparently left the server....why?")
self.battling_off(battle)
return
# Get a random win message from our list
fmt = random.SystemRandom().choice(battle_outcomes)
# Lets get the settings
settings = await ctx.bot.db.fetchrow(
"SELECT custom_battles, include_default_battles FROM guilds WHERE id = $1",
ctx.guild.id
)
msgs = battle_outcomes
if settings:
custom_msgs = settings["custom_battles"]
default_on = settings["include_default_battles"]
# if they exist, then we want to see if we want to use default as well
if custom_msgs:
if default_on or default_on is None:
msgs += custom_msgs
else:
msgs = custom_msgs
fmt = random.SystemRandom().choice(msgs)
# Due to our previous checks, the ID should only be in the dictionary once, in the current battle we're checking
self.battling_off(ctx)
self.battling_off(battle)
# Randomize the order of who is printed/sent to the update system
# All we need to do is change what order the challengers are printed/added as a paramater
if random.SystemRandom().randint(0, 1):
await self.bot.say(fmt.format(battleP1.mention, battleP2.mention))
await utils.update_records('battle_records', battleP1, battleP2)
# Randomize the winner/loser
winner, loser = battle.choose()
member_list = [m.id for m in ctx.guild.members]
query = """
SELECT id, rank, battle_rating, battle_wins, battle_losses
FROM
(SELECT
id,
ROW_NUMBER () OVER (ORDER BY battle_rating DESC) as "rank",
battle_rating,
battle_wins,
battle_losses
FROM
users
WHERE
id = any($1::bigint[]) AND
battle_rating IS NOT NULL
) AS sub
WHERE id = any($2)
"""
results = await ctx.bot.db.fetch(query, member_list, [winner.id, loser.id])
old_winner = old_loser = None
for result in results:
if result['id'] == loser.id:
old_loser = result
else:
old_winner = result
winner_rating, loser_rating, = utils.update_rating(
old_winner["battle_rating"] if old_winner else 1000,
old_loser["battle_rating"] if old_loser else 1000,
)
update_query = """
UPDATE
users
SET
battle_rating = $1,
battle_wins = $2,
battle_losses = $3
WHERE
id=$4
"""
insert_query = """
INSERT INTO
users (id, battle_rating, battle_wins, battle_losses)
VALUES
($1, $2, $3, $4)
"""
if old_loser:
await ctx.bot.db.execute(
update_query,
loser_rating,
old_loser['battle_wins'],
old_loser['battle_losses'] + 1,
loser.id
)
else:
await self.bot.say(fmt.format(battleP2.mention, battleP1.mention))
await utils.update_records('battle_records', battleP2, battleP1)
await ctx.bot.db.execute(insert_query, loser.id, loser_rating, 0, 1)
if old_winner:
await ctx.bot.db.execute(
update_query,
winner_rating,
old_winner['battle_wins'] + 1,
old_winner['battle_losses'],
winner.id
)
else:
await ctx.bot.db.execute(insert_query, winner.id, winner_rating, 1, 0)
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
results = await ctx.bot.db.fetch(query, member_list, [winner.id, loser.id])
new_winner_rank = new_loser_rank = None
for result in results:
if result['id'] == loser.id:
new_loser_rank = result['rank']
else:
new_winner_rank = result['rank']
fmt = fmt.format(winner=winner.display_name, loser=loser.display_name)
if old_winner:
fmt += "\n{} - Rank: {} ( +{} )".format(
winner.display_name, new_winner_rank, old_winner["rank"] - new_winner_rank
)
else:
fmt += "\n{} - Rank: {}".format(winner.display_name, new_winner_rank)
if old_loser:
fmt += "\n{} - Rank: {} ( -{} )".format(
loser.display_name, new_loser_rank, new_loser_rank - old_loser["rank"]
)
else:
fmt += "\n{} - Rank: {}".format(loser.display_name, new_loser_rank)
await ctx.send(fmt)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def decline(self, ctx):
"""Declines the battle challenge
@ -220,62 +362,74 @@ class Interaction:
RESULT: You chicken out"""
# This is a check to make sure that the author is the one being BATTLED
# And not the one that started the battle
battles = self.battles.get(ctx.message.server.id) or {}
p1 = [p1_id for p1_id, p2_id in battles.items() if p2_id == ctx.message.author.id]
if len(p1) == 0:
await self.bot.say("You are not currently being challenged to a battle!")
battle = self.get_receivers_battle(ctx.author)
if battle is None:
await ctx.send("You are not currently being challenged to a battle!")
return
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
battleP2 = ctx.message.author
self.battling_off(battle)
await ctx.send("{} has chickened out! What a loser~".format(ctx.author.mention))
# There's no need to update the stats for the members if they declined the battle
self.battling_off(ctx)
await self.bot.say("{0} has chickened out! What a loser~".format(battleP2.mention, battleP1.mention))
@commands.command(pass_context=True, no_pm=True)
@commands.cooldown(1, 180, BucketType.user)
@utils.custom_perms(send_messages=True)
async def boop(self, ctx, boopee: discord.Member = None, *, message = ""):
@commands.command()
@commands.guild_only()
@commands.cooldown(1, 10, BucketType.user)
@utils.can_run(send_messages=True)
async def boop(self, ctx, boopee: discord.Member = None, *, message=""):
"""Boops the mentioned person
EXAMPLE: !boop @OtherPerson
RESULT: You do a boop o3o"""
booper = ctx.message.author
booper = ctx.author
if boopee is None:
ctx.command.reset_cooldown(ctx)
await self.bot.say("You try to boop the air, the air boops back. Be afraid....")
await ctx.send("You try to boop the air, the air boops back. Be afraid....")
return
# To keep formatting easier, keep it either "" or the message with a space in front
if message is not None:
message = " " + message
if boopee.id == booper.id:
ctx.command.reset_cooldown(ctx)
await self.bot.say("You can't boop yourself! Silly...")
await ctx.send("You can't boop yourself! Silly...")
return
if boopee.id == self.bot.user.id:
if boopee.id == ctx.bot.user.id:
ctx.command.reset_cooldown(ctx)
await self.bot.say("Why the heck are you booping me? Get away from me >:c")
await ctx.send("Why the heck are you booping me? Get away from me >:c")
return
key = booper.id
boops = await utils.get_content('boops', key)
if boops is not None:
boops = boops['boops']
# If the booper has never booped the member provided, assure it's 0
amount = boops.get(boopee.id, 0) + 1
boops[boopee.id] = amount
await utils.update_content('boops', {'boops': boops}, key)
else:
entry = {'member_id': booper.id,
'boops': {boopee.id: 1}}
await utils.add_content('boops', entry)
query = "SELECT amount FROM boops WHERE booper = $1 AND boopee = $2"
amount = await ctx.bot.db.fetchrow(query, booper.id, boopee.id)
if amount is None:
amount = 1
replacement_query = "INSERT INTO boops (booper, boopee, amount) VALUES($1, $2, $3)"
else:
replacement_query = "UPDATE boops SET amount=$3 WHERE booper=$1 AND boopee=$2"
amount = amount['amount'] + 1
fmt = "{0.mention} has just booped {1.mention}{3}! That's {2} times now!"
await self.bot.say(fmt.format(booper, boopee, amount, message))
await ctx.send(f"{booper.mention} has just booped {boopee.mention}{message}! That's {amount} times now!")
await ctx.bot.db.execute(replacement_query, booper.id, boopee.id, amount)
class Battle:
def __init__(self, initiator, receiver):
self.initiator = initiator
self.receiver = receiver
self.rand = random.SystemRandom()
def is_initiator(self, player):
return player.id == self.initiator.id and player.guild.id == self.initiator.guild.id
def is_receiver(self, player):
return player.id == self.receiver.id and player.guild.id == self.receiver.guild.id
def is_battling(self, player):
return self.is_initiator(player) or self.is_receiver(player)
def choose(self):
"""Returns the two users in the order winner, loser"""
choices = [self.initiator, self.receiver]
self.rand.shuffle(choices)
return choices
def setup(bot):

View file

@ -1,24 +1,19 @@
from discord.ext import commands
from . import utils
import utils
from bs4 import BeautifulSoup as bs
import discord
import random
import re
import math
class Links:
class Links(commands.Cog):
"""This class contains all the commands that make HTTP requests
In other words, all commands here rely on other URL's to complete their requests"""
def __init__(self, bot):
self.bot = bot
@commands.command(pass_context=True, aliases=['g'])
@utils.custom_perms(send_messages=True)
@commands.command(aliases=['g'])
@utils.can_run(send_messages=True)
async def google(self, ctx, *, query: str):
"""Searches google for a provided query
@ -27,7 +22,7 @@ class Links:
url = "https://www.google.com/search"
# Turn safe filter on or off, based on whether or not this is a nsfw channel
nsfw = await utils.channel_is_nsfw(ctx.message.channel)
nsfw = utils.channel_is_nsfw(ctx.message.channel)
safe = 'off' if nsfw else 'on'
params = {'q': query,
@ -42,7 +37,7 @@ class Links:
data = await utils.request(url, payload=params, attr='text')
if data is None:
await self.bot.send_message(ctx.message.channel, "I failed to connect to google! (That can happen??)")
await ctx.send("I failed to connect to google! (That can happen??)")
return
# Convert to a BeautifulSoup element and loop through each result clasified by h3 tags with a class of 'r'
@ -54,23 +49,23 @@ class Links:
try:
result_url = re.search('(?<=q=).*(?=&sa=)', element.find('a').get('href')).group(0)
except AttributeError:
await self.bot.say("I couldn't find any results for {}!".format(query))
await ctx.send("I couldn't find any results for {}!".format(query))
return
# Get the next sibling, find the span where the description is, and get the text from this
try:
description = element.next_sibling.find('span', class_='st').text
except:
except Exception:
description = ""
# Add this to our text we'll use to send
fmt += '\n\n**URL**: <{}>\n**Description**: {}'.format(result_url, description)
fmt = "**Top 3 results for the query** _{}_:{}".format(query, fmt)
await self.bot.say(fmt)
await ctx.send(fmt)
@commands.command(aliases=['yt'], pass_context=True)
@utils.custom_perms(send_messages=True)
@commands.command(aliases=['yt'])
@utils.can_run(send_messages=True)
async def youtube(self, ctx, *, query: str):
"""Searches youtube for a provided query
@ -86,13 +81,13 @@ class Links:
data = await utils.request(url, payload=params)
if data is None:
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to youtube!")
await ctx.send("Sorry but I failed to connect to youtube!")
return
try:
result = data['items'][0]
except IndexError:
await self.bot.say("I could not find any results with the search term {}".format(query))
await ctx.send("I could not find any results with the search term {}".format(query))
return
result_url = "https://youtube.com/watch?v={}".format(result['id']['videoId'])
@ -100,10 +95,10 @@ class Links:
description = result['snippet']['description']
fmt = "**Title:** {}\n\n**Description:** {}\n\n**URL:** <{}>".format(title, description, result_url)
await self.bot.say(fmt)
await ctx.send(fmt)
@commands.command(pass_context=True)
@utils.custom_perms(send_messages=True)
@commands.command()
@utils.can_run(send_messages=True)
async def wiki(self, ctx, *, query: str):
"""Pulls the top match for a specific term from wikipedia, and returns the result
@ -119,11 +114,11 @@ class Links:
data = await utils.request(base_url, payload=params)
if data is None:
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to Wikipedia!")
await ctx.send("Sorry but I failed to connect to Wikipedia!")
return
if len(data['query']['search']) == 0:
await self.bot.say("I could not find any results with that term, I tried my best :c")
await ctx.send("I could not find any results with that term, I tried my best :c")
return
# Wiki articles' URLs are in the format https://en.wikipedia.org/wiki/[Titlehere]
# Replace spaces with %20
@ -135,148 +130,44 @@ class Links:
snippet = re.sub('</span>', '', snippet)
snippet = re.sub('&quot;', '"', snippet)
await self.bot.say(
await ctx.send(
"Here is the best match I found with the query `{}`:\nURL: <{}>\nSnippet: \n```\n{}```".format(query, url,
snippet))
@commands.command(pass_context=True)
@utils.custom_perms(send_messages=True)
@commands.command()
@utils.can_run(send_messages=True)
async def urban(self, ctx, *, msg: str):
"""Pulls the top urbandictionary.com definition for a term
EXAMPLE: !urban a normal phrase
RESULT: Probably something lewd; this is urban dictionary we're talking about"""
url = "http://api.urbandictionary.com/v0/define"
params = {"term": msg}
try:
data = await utils.request(url, payload=params)
if data is None:
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to urban dictionary!")
return
# List is the list of definitions found, if it's empty then nothing was found
if len(data['list']) == 0:
await self.bot.say("No result with that term!")
# If the list is not empty, use the first result and print it's defintion
else:
await self.bot.say(data['list'][0]['definition'])
# Urban dictionary has some long definitions, some might not be able to be sent
except discord.HTTPException:
await self.bot.say('```\nError: Definition is too long for me to send```')
except KeyError:
await self.bot.say("Sorry but I failed to connect to urban dictionary!")
@commands.command(pass_context=True)
@utils.custom_perms(send_messages=True)
async def derpi(self, ctx, *search: str):
"""Provides a random image from the first page of derpibooru.org for the following term
EXAMPLE: !derpi Rainbow Dash
RESULT: A picture of Rainbow Dash!"""
if len(search) > 0:
url = 'https://derpibooru.org/search.json'
# Ensure a filter was not provided, as we either want to use our own, or none (for safe pics)
query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value))
params = {'q': query}
nsfw = await utils.channel_is_nsfw(ctx.message.channel)
# If this is a nsfw channel, we just need to tack on 'explicit' to the terms
# Also use the custom filter that I have setup, that blocks some certain tags
# If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit
if nsfw:
params['q'] += ", (explicit OR suggestive)"
params['filter_id'] = 95938
else:
params['q'] += ", safe"
await self.bot.say("Looking up an image with those tags....")
if utils.channel_is_nsfw(ctx.message.channel):
url = "http://api.urbandictionary.com/v0/define"
params = {"term": msg}
try:
# Get the response from derpibooru and parse the 'search' result from it
data = await utils.request(url, payload=params)
if data is None:
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to Derpibooru!")
await ctx.send("Sorry but I failed to connect to urban dictionary!")
return
results = data['search']
# List is the list of definitions found, if it's empty then nothing was found
if len(data['list']) == 0:
await ctx.send("No result with that term!")
# If the list is not empty, use the first result and print it's defintion
else:
entries = [x['definition'] for x in data['list']]
try:
pages = utils.Pages(ctx, entries=entries[:5], per_page=1)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
# Urban dictionary has some long definitions, some might not be able to be sent
except discord.HTTPException:
await ctx.send('```\nError: Definition is too long for me to send```')
except KeyError:
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
return
# The first request we've made ensures there are results
# Now we can get the total count from that, and make another request based on the number of pages as well
if len(results) > 0:
# Get the total number of pages
pages = math.ceil(data['total'] / len(results))
# Set a new paramater to set which page to use, randomly based on the number of pages
params['page'] = random.SystemRandom().randint(1, pages)
data = await utils.request(url, payload=params)
if data is None:
await self.bot.say("Sorry but I failed to connect to Derpibooru!")
return
# Now get the results again
results = data['search']
# Get the image link from the now random page'd and random result from that page
index = random.SystemRandom().randint(0, len(results) - 1)
image_link = 'https://derpibooru.org/{}'.format(results[index]['id'])
else:
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
return
await ctx.send("Sorry but I failed to connect to urban dictionary!")
else:
# If no search term was provided, search for a random image
# .url will be the URL we end up at, not the one requested.
# https://derpibooru.org/images/random redirects to a random image, so this is exactly what we want
image_link = await utils.request('https://derpibooru.org/images/random', attr='url')
await self.bot.say(image_link)
@commands.command(pass_context=True)
@utils.custom_perms(send_messages=True)
async def e621(self, ctx, *, tags: str):
"""Searches for a random image from e621.net
Format for the search terms need to be 'search term 1, search term 2, etc.'
If the channel the command is ran in, is registered as a nsfw channel, this image will be explicit
EXAMPLE: !e621 dragon
RESULT: A picture of a dragon (hopefully, screw your tagging system e621)"""
# This changes the formatting for queries, so we don't
# Have to use e621's stupid formatting when using the command
tags = tags.replace(' ', '_')
tags = tags.replace(',_', ' ')
url = 'https://e621.net/post/index.json'
params = {'limit': 320,
'tags': tags}
# e621 provides a way to change how many images can be shown on one request
# This gives more of a chance of random results, however it causes the lookup to take longer than most
# Due to this, send a message saying we're looking up the information first
await self.bot.say("Looking up an image with those tags....")
nsfw = await utils.channel_is_nsfw(ctx.message.channel)
# e621 by default does not filter explicit content, so tack on
# safe/explicit based on if this channel is nsfw or not
params['tags'] += " rating:explicit" if nsfw else " rating:safe"
data = await utils.request(url, payload=params)
if data is None:
await self.bot.send_message(ctx.message.channel,
"Sorry, I had trouble connecting at the moment; please try again later")
return
# Try to find an image from the list. If there were no results, we're going to attempt to find
# A number between (0,-1) and receive an error.
# The response should be in a list format, so we'll end up getting a key error if the response was in json
# i.e. it responded with a 404/504/etc.
try:
rand_image = data[random.SystemRandom().randint(0, len(data) - 1)]['file_url']
await self.bot.say(rand_image)
except (ValueError, KeyError):
await self.bot.say("No results with that tag {}".format(ctx.message.author.mention))
return
await ctx.send("This command is limited to nsfw channels")
def setup(bot):

405
cogs/misc.py Normal file
View file

@ -0,0 +1,405 @@
import discord
from discord.ext import commands
import utils
import random
import re
import calendar
import pendulum
import datetime
import psutil
def _command_signature(cmd):
result = [cmd.qualified_name]
if cmd.usage:
result.append(cmd.usage)
return ' '.join(result)
params = cmd.clean_params
if not params:
return ' '.join(result)
for name, param in params.items():
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append(f'[{name}={param.default!r}]')
else:
result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL:
result.append(f'[{name}...]')
else:
result.append(f'<{name}>')
return ' '.join(result)
class Miscallaneous(commands.Cog):
"""Core commands, these are the miscallaneous commands that don't fit into other categories'"""
process = psutil.Process()
process.cpu_percent()
@commands.command()
@commands.cooldown(1, 3, commands.cooldowns.BucketType.user)
@utils.can_run(send_messages=True)
async def help(self, ctx, *, command: str = None):
"""Shows help about a command or the bot"""
try:
if command is None:
p = await utils.HelpPaginator.from_bot(ctx)
else:
entity = ctx.bot.get_cog(command) or ctx.bot.get_command(command)
if entity is None:
clean = command.replace('@', '@\u200b')
return await ctx.send(f'Command or category "{clean}" not found.')
elif isinstance(entity, commands.Command):
p = await utils.HelpPaginator.from_command(ctx, entity)
else:
p = await utils.HelpPaginator.from_cog(ctx, entity)
await p.paginate()
except Exception as e:
await ctx.send(e)
async def _help(self, ctx, *, entity: str = None):
chunks = []
if entity:
entity = ctx.bot.get_cog(entity) or ctx.bot.get_command(entity)
if entity is None:
fmt = "Hello! Here is a list of the sections of commands that I have " \
"(there are a lot of commands so just start with the sections...I know, I'm pretty great)\n"
fmt += "To use a command's paramaters, you need to know the notation for them:\n"
fmt += "\t<argument> This means the argument is __**required**__.\n"
fmt += "\t[argument] This means the argument is __**optional**__.\n"
fmt += "\t[A|B] This means the it can be __**either A or B**__.\n"
fmt += "\t[argument...] This means you can have multiple arguments.\n"
fmt += "\n**Type `{}help section` to get help on a specific section**\n".format(ctx.prefix)
fmt += "**CASE MATTERS** Sections are in `Title Case` and commands are in `lower case`\n\n"
chunks.append(fmt)
cogs = sorted(ctx.bot.cogs.values(), key=lambda c: c.__class__.__name__)
for cog in cogs:
tmp = "**{}**\n".format(cog.__class__.__name__)
if cog.__doc__:
tmp += "\t{}\n".format(cog.__doc__)
if len(chunks[len(chunks) - 1] + tmp) > 2000:
chunks.append(tmp)
else:
chunks[len(chunks) - 1] += tmp
elif isinstance(entity, (commands.core.Command, commands.core.Group)):
tmp = "**{}**".format(_command_signature(entity))
tmp += "\n{}".format(entity.help)
chunks.append(tmp)
else:
cmds = sorted(entity.get_commands(), key=lambda c: c.name)
fmt = "Here are a list of commands under the section {}\n".format(entity.__class__.__name__)
fmt += "Type `{}help command` to get more help on a specific command\n\n".format(ctx.prefix)
chunks.append(fmt)
for command in cmds:
for subcommand in command.walk_commands():
tmp = "**{}**\n\t{}\n".format(subcommand.qualified_name, subcommand.short_doc)
if len(chunks[len(chunks) - 1] + tmp) > 2000:
chunks.append(tmp)
else:
chunks[len(chunks) - 1] += tmp
if utils.dev_server:
tmp = "\n\nIf I'm having issues, then please visit the dev server and ask for help. {}".format(
utils.dev_server)
if len(chunks[len(chunks) - 1] + tmp) > 2000:
chunks.append(tmp)
else:
chunks[len(chunks) - 1] += tmp
if len(chunks) == 1 and len(chunks[0]) < 1000:
destination = ctx.channel
else:
destination = ctx.author
try:
for chunk in chunks:
await destination.send(chunk)
except (discord.Forbidden, discord.HTTPException):
await ctx.send("I cannot DM you, please allow DM's from this server to run this command")
else:
if ctx.guild and destination == ctx.author:
await ctx.send("I have just DM'd you some information about me!")
@commands.command()
@utils.can_run(send_messages=True)
async def ping(self, ctx):
"""Returns the latency between the server websocket, and between reading messages"""
msg_latency = datetime.datetime.utcnow() - ctx.message.created_at
fmt = "Message latency {0:.2f} seconds".format(msg_latency.seconds + msg_latency.microseconds / 1000000)
fmt += "\nWebsocket latency {0:.2f} seconds".format(ctx.bot.latency)
await ctx.send(fmt)
@commands.command(aliases=["coin"])
@utils.can_run(send_messages=True)
async def coinflip(self, ctx):
"""Flips a coin and responds with either heads or tails
EXAMPLE: !coinflip
RESULT: Heads!"""
result = "Heads!" if random.SystemRandom().randint(0, 1) else "Tails!"
await ctx.send(result)
@commands.command()
@utils.can_run(send_messages=True)
async def say(self, ctx, *, msg: str):
"""Tells the bot to repeat what you say
EXAMPLE: !say I really like orange juice
RESULT: I really like orange juice"""
fmt = "\u200B{}".format(msg)
await ctx.send(fmt)
try:
await ctx.message.delete()
except Exception:
pass
@commands.command()
@utils.can_run(send_messages=True)
async def calendar(self, ctx, month: str = None, year: int = None):
"""Provides a printout of the current month's calendar
Provide month and year to print the calendar of that year and month
EXAMPLE: !calendar january 2011"""
# calendar takes in a number for the month, not the words
# so we need this dictionary to transform the word to the number
months = {
"january": 1,
"february": 2,
"march": 3,
"april": 4,
"may": 5,
"june": 6,
"july": 7,
"august": 8,
"september": 9,
"october": 10,
"november": 11,
"december": 12
}
# In month was not passed, use the current month
if month is None:
month = datetime.date.today().month
else:
month = months.get(month.lower())
if month is None:
await ctx.send("Please provide a valid Month!")
return
# If year was not passed, use the current year
if year is None:
year = datetime.datetime.today().year
# Here we create the actual "text" calendar that we are printing
cal = calendar.TextCalendar().formatmonth(year, month)
await ctx.send("```\n{}```".format(cal))
@commands.command(aliases=['about'])
@utils.can_run(send_messages=True)
async def info(self, ctx):
"""This command can be used to print out some of my information"""
# fmt is a dictionary so we can set the key to it's output, then print both
# The only real use of doing it this way is easier editing if the info
# in this command is changed
# Create the original embed object
# Set the description include dev server (should be required) and the optional patreon link
description = "[Dev Server]({})".format(utils.dev_server)
if utils.patreon_link:
description += "\n[Patreon]({})".format(utils.patreon_link)
# Now creat the object
opts = {'title': 'Bonfire',
'description': description,
'colour': discord.Colour.green()}
# Set the owner
embed = discord.Embed(**opts)
if hasattr(ctx.bot, 'owner'):
embed.set_author(name=str(ctx.bot.owner), icon_url=ctx.bot.owner.avatar_url)
# Setup the process statistics
name = "Process statistics"
value = ""
memory_usage = self.process.memory_full_info().uss / 1024 ** 2
cpu_usage = self.process.cpu_percent() / psutil.cpu_count()
value += 'Memory: {:.2f} MiB'.format(memory_usage)
value += '\nCPU: {}%'.format(cpu_usage)
if hasattr(ctx.bot, 'uptime'):
value += "\nUptime: {}".format((pendulum.now(tz="UTC") - ctx.bot.uptime).in_words())
embed.add_field(name=name, value=value, inline=False)
# Setup the user and guild statistics
name = "User/Guild statistics"
value = ""
value += "Channels: {}".format(len(list(ctx.bot.get_all_channels())))
value += "\nUsers: {}".format(len(ctx.bot.users))
value += "\nServers: {}".format(len(ctx.bot.guilds))
embed.add_field(name=name, value=value, inline=False)
# The game statistics
name = "Game statistics"
# To get the newlines right, since we're not sure what will and won't be included
# Lets make this one a list and join it at the end
value = []
hm = ctx.bot.get_cog('Hangman')
ttt = ctx.bot.get_cog('TicTacToe')
bj = ctx.bot.get_cog('Blackjack')
interaction = ctx.bot.get_cog('Interaction')
if hm:
value.append("Hangman games: {}".format(len(hm.games)))
if ttt:
value.append("TicTacToe games: {}".format(len(ttt.boards)))
if bj:
value.append("Blackjack games: {}".format(len(bj.games)))
if interaction:
count_battles = 0
for battles in ctx.bot.get_cog('Interaction').battles.values():
count_battles += len(battles)
value.append("Battles running: {}".format(len(bj.games)))
embed.add_field(name=name, value="\n".join(value), inline=False)
await ctx.send(embed=embed)
@commands.command()
@utils.can_run(send_messages=True)
async def uptime(self, ctx):
"""Provides a printout of the current bot's uptime
EXAMPLE: !uptime
RESULT: A BAJILLION DAYS"""
if hasattr(ctx.bot, 'uptime'):
await ctx.send("Uptime: ```\n{}```".format((pendulum.now(tz="UTC") - ctx.bot.uptime).in_words()))
else:
await ctx.send("I've just restarted and not quite ready yet...gimme time I'm not a morning pony :c")
@commands.command(aliases=['invite'])
@utils.can_run(send_messages=True)
async def addbot(self, ctx):
"""Provides a link that you can use to add me to a server
EXAMPLE: !addbot
RESULT: http://discord.gg/yo_mama"""
perms = discord.Permissions.none()
perms.read_messages = True
perms.send_messages = True
perms.manage_roles = True
perms.ban_members = True
perms.kick_members = True
perms.manage_messages = True
perms.embed_links = True
perms.read_message_history = True
perms.attach_files = True
perms.speak = True
perms.connect = True
perms.attach_files = True
perms.add_reactions = True
app_info = await ctx.bot.application_info()
await ctx.send("Use this URL to add me to a server that you'd like!\n<{}>"
.format(discord.utils.oauth_url(app_info.id, perms)))
@commands.command(enabled=False)
@utils.can_run(send_messages=True)
async def joke(self, ctx):
"""Prints a random riddle
EXAMPLE: !joke
RESULT: An absolutely terrible joke."""
# Currently disabled until I can find a free API
pass
@commands.command()
@utils.can_run(send_messages=True)
async def roll(self, ctx, *, notation: str = "d6"):
"""Rolls a die based on the notation given
Format should be #d#
EXAMPLE: !roll d50
RESULT: 51 :^)"""
# Use regex to get the notation based on what was provided
try:
# We do not want to try to convert the dice, because we want d# to
# be a valid notation
dice = re.search("(\d*)d(\d*)", notation).group(1)
num = int(re.search("(\d*)d(\d*)", notation).group(2))
# Attempt to get addition/subtraction
add = re.search("\+ ?(\d+)", notation)
subtract = re.search("- ?(\d+)", notation)
# Check if something like ed3 was provided, or something else entirely
# was provided
except (AttributeError, ValueError):
await ctx.send("Please provide the die notation in #d#!")
return
# Dice will be None if d# was provided, assume this means 1d#
dice = dice or 1
# Since we did not try to convert to int before, do it now after we
# have it set
dice = int(dice)
if dice > 30:
await ctx.send("I'm not rolling more than 30 dice, I have tiny hands")
return
if num > 100:
await ctx.send("What die has more than 100 sides? Please, calm down")
return
if num <= 1:
await ctx.send("A {} sided die? You know that's impossible right?".format(num))
return
nums = [random.SystemRandom().randint(1, num) for _ in range(0, int(dice))]
subtotal = total = sum(nums)
# After totalling, if we have add/subtract seperately, apply them
if add:
add = int(add.group(1))
total += add
if subtract:
subtract = int(subtract.group(1))
total -= subtract
value_str = ", ".join("{}".format(x) for x in nums)
if dice == 1:
fmt = '{0.message.author.name} has rolled a {1} sided die and got the number {2}!'.format(
ctx, num, value_str
)
if add or subtract:
fmt += "\nTotal: {} ({}".format(total, subtotal)
if add:
fmt += " + {}".format(add)
if subtract:
fmt += " - {}".format(subtract)
fmt += ")"
else:
fmt = '{0.message.author.name} has rolled {1}, {2} sided dice and got the numbers {3}!'.format(
ctx, dice, num, value_str
)
if add or subtract:
fmt += "\nTotal: {} ({}".format(total, subtotal)
if add:
fmt += " + {}".format(add)
if subtract:
fmt += " - {}".format(subtract)
fmt += ")"
else:
fmt += "\nTotal: {}".format(total)
await ctx.send(fmt)
def setup(bot):
bot.add_cog(Miscallaneous(bot))

View file

@ -1,69 +1,31 @@
from discord.ext import commands
from . import utils
import utils
import discord
import re
import asyncio
import rethinkdb as r
valid_perms = [p for p in dir(discord.Permissions) if isinstance(getattr(discord.Permissions, p), property)]
class Mod:
"""Commands that can be used by a or an admin, depending on the command"""
class Moderation(commands.Cog):
"""Moderation commands, things that help control a server...but not the settings of the server"""
def __init__(self, bot):
self.bot = bot
def find_command(self, command):
# This method ensures the command given is valid. We need to loop through commands
# As self.bot.commands only includes parent commands
# So we are splitting the command in parts, looping through the commands
# And getting the subcommand based on the next part
# If we try to access commands of a command that isn't a group
# We'll hit an AttributeError, meaning an invalid command was given
# If we loop through and don't find anything, cmd will still be None
# And we'll report an invalid was given as well
cmd = None
for part in command.split():
try:
if cmd is None:
cmd = self.bot.commands.get(part)
else:
cmd = cmd.commands.get(part)
except AttributeError:
cmd = None
break
return cmd
@commands.command(pass_context=True, no_pm=True, aliases=['nick'])
@utils.custom_perms(kick_members=True)
async def nickname(self, ctx, *, name=None):
"""Used to set the nickname for Bonfire (provide no nickname and it will reset)
EXAMPLE: !nick Music Bot
RESULT: My nickname is now music bot"""
await self.bot.change_nickname(ctx.message.server.me, name)
await self.bot.say("\N{OK HAND SIGN}")
@commands.command(no_pm=True)
@utils.custom_perms(kick_members=True)
async def kick(self, member: discord.Member):
@commands.command()
@commands.guild_only()
@utils.can_run(kick_members=True)
async def kick(self, ctx, member: discord.Member, *, reason=None):
"""Used to kick a member from this server
EXAMPLE: !kick @Member
RESULT: They're kicked from the server?"""
try:
await self.bot.kick(member)
await self.bot.say("\N{OK HAND SIGN}")
await member.kick(reason=reason)
await ctx.send("\N{OK HAND SIGN}")
except discord.Forbidden:
await self.bot.say("But I can't, muh permissions >:c")
await ctx.send("But I can't, muh permissions >:c")
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(ban_members=True)
@commands.command()
@commands.guild_only()
@utils.can_run(ban_members=True)
async def unban(self, ctx, member_id: int):
"""Used to unban a member from this server
Due to the fact that I cannot find a user without being in a server with them
@ -74,18 +36,18 @@ class Mod:
# Lets only accept an int for this method, in order to ensure only an ID is provided
# Due to that though, we need to ensure a string is passed as the member's ID
member = discord.Object(id=str(member_id))
try:
await self.bot.unban(ctx.message.server, member)
await self.bot.say("\N{OK HAND SIGN}")
await ctx.bot.http.unban(member_id, ctx.guild.id)
await ctx.send("\N{OK HAND SIGN}")
except discord.Forbidden:
await self.bot.say("But I can't, muh permissions >:c")
await ctx.send("But I can't, muh permissions >:c")
except discord.HTTPException:
await self.bot.say("Sorry, I failed to unban that user!")
await ctx.send("Sorry, I failed to unban that user!")
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(ban_members=True)
async def ban(self, ctx, *, member):
@commands.command()
@commands.guild_only()
@utils.can_run(ban_members=True)
async def ban(self, ctx, member, *, reason=None):
"""Used to ban a member
This can be used to ban someone preemptively as well.
Provide the ID of the user and this should ban them without them being in the server
@ -95,310 +57,60 @@ class Mod:
# Lets first check if a user ID was provided, as that will be the easiest case to ban
if member.isdigit():
# First convert it to a discord object based on the ID that was given
member = discord.Object(id=member)
# Next, to ban from the server the API takes a server obejct and uses that ID
# So set "this" server as the member's server. This creates the "fake" member we need
member.server = ctx.message.server
try:
await ctx.bot.http.ban(member, ctx.guild.id, reason=reason)
await ctx.send("\N{OK HAND SIGN}")
except discord.Forbidden:
await ctx.send("But I can't, muh permissions >:c")
except discord.HTTPException:
await ctx.send("Sorry, I failed to ban that user!")
finally:
return
else:
# If no ID was provided, lets try to convert what was given using the internal coverter
converter = commands.converter.UserConverter(ctx, member)
converter = commands.converter.MemberConverter()
try:
member = converter.convert()
member = await converter.convert(ctx, member)
except commands.converter.BadArgument:
await self.bot.say(
await ctx.send(
'{} does not appear to be a valid member. If this member is not in this server, please provide '
'their ID'.format(member))
return
# Now lets try actually banning the member we've been given
try:
await self.bot.ban(member)
await self.bot.say("\N{OK HAND SIGN}")
except discord.Forbidden:
await self.bot.say("But I can't, muh permissions >:c")
except discord.HTTPException:
await self.bot.say("Sorry, I failed to ban that user!")
@commands.command(no_pm=True, aliases=['alerts'], pass_context=True)
@utils.custom_perms(kick_members=True)
async def notifications(self, ctx, channel: discord.Channel):
"""This command is used to set a channel as the server's 'notifications' channel
Any notifications (like someone going live on Twitch, or Picarto) will go to that channel
EXAMPLE: !alerts #alerts
RESULT: No more alerts spammed in #general!"""
if str(channel.type) != "text":
await self.bot.say("The notifications channel must be a text channel!")
return
key = ctx.message.server.id
entry = {'server_id': key,
'notification_channel': channel.id}
if not await utils.update_content('server_settings', entry, key):
await utils.add_content('server_settings', entry)
await self.bot.say("I have just changed this server's 'notifications' channel"
"\nAll notifications will now go to `{}`".format(channel))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(kick_members=True)
async def usernotify(self, ctx, on_off: str):
"""This command can be used to set whether or not you want user notificaitons to show
Provide on, yes, or true to set it on; otherwise it will be turned off
EXAMPLE: !usernotify on
RESULT: Annying join/leave notifications! Yay!"""
# Join/Leave notifications can be kept separate from normal alerts
# So we base this channel on it's own and not from alerts
# When mod logging becomes available, that will be kept to it's own channel if wanted as well
on_off = True if re.search("(on|yes|true)", on_off.lower()) else False
key = ctx.message.server.id
entry = {'server_id': key,
'join_leave': on_off}
if not await utils.update_content('server_settings', entry, key):
await utils.add_content('server_settings', entry)
fmt = "notify" if on_off else "not notify"
await self.bot.say("This server will now {} if someone has joined or left".format(fmt))
@commands.group(pass_context=True)
async def nsfw(self, ctx):
"""Handles adding or removing a channel as a nsfw channel"""
pass
@nsfw.command(name="add", pass_context=True)
@utils.custom_perms(kick_members=True)
async def nsfw_add(self, ctx):
"""Registers this channel as a 'nsfw' channel
EXAMPLE: !nsfw add
RESULT: ;)"""
key = ctx.message.server.id
entry = {'server_id': key,
'nsfw_channels': [ctx.message.channel.id]}
update = {'nsfw_channels': r.row['nsfw_channels'].append(ctx.message.channel.id)}
server_settings = await utils.get_content('server_settings', key)
if server_settings and 'nsfw_channels' in server_settings.keys():
await utils.update_content('server_settings', update, key)
elif server_settings:
await utils.update_content('server_settings', entry, key)
else:
await utils.add_content('server_settings', entry)
await self.bot.say("This channel has just been registered as 'nsfw'! Have fun you naughties ;)")
@nsfw.command(name="remove", aliases=["delete"], pass_context=True)
@utils.custom_perms(kick_members=True)
async def nsfw_remove(self, ctx):
"""Removes this channel as a 'nsfw' channel
EXAMPLE: !nsfw remove
RESULT: ;("""
key = ctx.message.server.id
server_settings = await utils.get_content('server_settings', key)
channel = ctx.message.channel.id
try:
channels = server_settings['nsfw_channels']
if channel in channels:
channels.remove(channel)
entry = {'nsfw_channels': channels}
await utils.update_content('server_settings', entry, key)
await self.bot.say("This channel has just been unregistered as a nsfw channel")
return
except (TypeError, IndexError):
pass
await self.bot.say("This channel is not registered as a 'nsfw' channel!")
@commands.command(pass_context=True)
@utils.custom_perms(kick_members=True)
async def say(self, ctx, *, msg: str):
"""Tells the bot to repeat what you say
EXAMPLE: !say I really like orange juice
RESULT: I really like orange juice"""
fmt = "\u200B{}".format(msg)
await self.bot.say(fmt)
try:
await self.bot.delete_message(ctx.message)
except:
pass
@commands.group(pass_context=True, invoke_without_command=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def perms(self, ctx, *, command: str = None):
"""This command can be used to print the current allowed permissions on a specific command
This supports groups as well as subcommands; pass no argument to print a list of available permissions
EXAMPLE: !perms help RESULT: Hopefully a result saying you just need send_messages permissions; otherwise lol
this server's admin doesn't like me """
if command is None:
await self.bot.say(
"Valid permissions are: ```\n{}```".format("\n".join("{}".format(i) for i in valid_perms)))
return
cmd = utils.find_command(self.bot, command)
if cmd is None:
await self.bot.say("That is not a valid command!")
return
server_settings = await utils.get_content('server_settings', ctx.message.server.id)
try:
server_perms = server_settings['permissions']
except (TypeError, IndexError, KeyError):
server_perms = {}
perms_value = server_perms.get(cmd.qualified_name)
if perms_value is None:
# If we don't find custom permissions, get the required permission for a command
# based on what we set in utils.custom_perms, if custom_perms isn't found, we'll get an IndexError
# Now lets try actually banning the member we've been given
try:
custom_perms = [func for func in cmd.checks if "custom_perms" in func.__qualname__][0]
except IndexError:
# Loop through and check if there is a check called is_owner
# If we loop through and don't find one, this means that the only other choice is to be
# Able to manage the server (for the utils on perm commands)
for func in cmd.checks:
if "is_owner" in func.__qualname__:
await self.bot.say("You need to own the bot to run this command")
return
await self.bot.say(
"You are required to have `manage_server` permissions to run `{}`".format(cmd.qualified_name))
return
await member.ban(reason=reason)
await ctx.send("\N{OK HAND SIGN}")
except discord.Forbidden:
await ctx.send("But I can't, muh permissions >:c")
except discord.HTTPException:
await ctx.send("Sorry, I failed to ban that user!")
# Perms will be an attribute if custom_perms is found no matter what, so no need to check this
perms = "\n".join(attribute for attribute, setting in custom_perms.perms.items() if setting)
await self.bot.say(
"You are required to have `{}` permissions to run `{}`".format(perms, cmd.qualified_name))
else:
# Permissions are saved as bit values, so create an object based on that value
# Then check which permission is true, that is our required permission
# There's no need to check for errors here, as we ensure a permission is valid when adding it
permissions = discord.Permissions(perms_value)
needed_perm = [perm[0] for perm in permissions if perm[1]][0]
await self.bot.say("You need to have the permission `{}` "
"to use the command `{}` in this server".format(needed_perm, command))
@perms.command(name="add", aliases=["setup,create"], pass_context=True, no_pm=True)
@commands.has_permissions(manage_server=True)
async def add_perms(self, ctx, *msg: str):
"""Sets up custom permissions on the provided command
Format must be 'perms add <command> <permission>'
If you want to open the command to everyone, provide 'none' as the permission
EXAMPLE: !perms add skip ban_members
RESULT: No more random people voting to skip a song"""
# Since subcommands exist, base the last word in the list as the permission, and the rest of it as the command
command = " ".join(msg[0:len(msg) - 1])
try:
permissions = msg[len(msg) - 1]
except IndexError:
await self.bot.say("Please provide the permissions you want to setup, the format for this must be in:\n"
"`perms add <command> <permission>`")
return
cmd = utils.find_command(self.bot, command)
if cmd is None:
await self.bot.say(
"That command does not exist! You can't have custom permissions on a non-existant command....")
return
# If a user can run a command, they have to have send_messages permissions; so use this as the base
if permissions.lower() == "none":
permissions = "send_messages"
# Convert the string to an int value of the permissions object, based on the required permission
# If we hit an attribute error, that means the permission given was not correct
perm_obj = discord.Permissions.none()
try:
setattr(perm_obj, permissions, True)
except AttributeError:
await self.bot.say("{} does not appear to be a valid permission! Valid permissions are: ```\n{}```"
.format(permissions, "\n".join(valid_perms)))
return
perm_value = perm_obj.value
# Two cases I use should never have custom permissions setup on them, is_owner for obvious reasons
# The other case is if I'm using the default has_permissions case
# Which means I do not want to check custom permissions at all
# Currently the second case is only on adding and removing permissions, to avoid abuse on these
for check in cmd.checks:
if "is_owner" == check.__name__ or "has_permissions" in str(check):
await self.bot.say("This command cannot have custom permissions setup!")
return
key = ctx.message.server.id
entry = {'server_id': key,
'permissions': {cmd.qualified_name: perm_value}}
if not await utils.update_content('server_settings', entry, key):
await utils.add_content('server_settings', entry)
await self.bot.say("I have just added your custom permissions; "
"you now need to have `{}` permissions to use the command `{}`".format(permissions, command))
@perms.command(name="remove", aliases=["delete"], pass_context=True, no_pm=True)
@commands.has_permissions(manage_server=True)
async def remove_perms(self, ctx, *, command: str):
"""Removes the custom permissions setup on the command specified
EXAMPLE: !perms remove play
RESULT: Freedom!"""
cmd = utils.find_command(self.bot, command)
if cmd is None:
await self.bot.say(
"That command does not exist! You can't have custom permissions on a non-existant command....")
return
update = {'permissions': {cmd.qualified_name: None}}
await utils.update_content('server_settings', update, ctx.message.server.id)
await self.bot.say("I have just removed the custom permissions for {}!".format(cmd))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(manage_server=True)
async def prefix(self, ctx, *, prefix: str):
"""This command can be used to set a custom prefix per server
EXAMPLE: !prefix new_prefix
RESULT: You probably screwing it up and not realizing you now need to do new_prefixprefix"""
key = ctx.message.server.id
if prefix.lower().strip() == "none":
prefix = None
entry = {'server_id': key,
'prefix': prefix}
if not await utils.update_content('server_settings', entry, key):
await utils.add_content('server_settings', entry)
if prefix is None:
fmt = "I have just cleared your custom prefix, the default prefix will have to be used now"
else:
fmt = "I have just updated the prefix for this server; you now need to call commands with `{0}`. " \
"For example, you can call this command again with {0}prefix".format(prefix)
await self.bot.say(fmt)
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(manage_messages=True)
@commands.command()
@commands.guild_only()
@utils.can_run(manage_messages=True)
async def purge(self, ctx, limit: int = 100):
"""This command is used to a purge a number of messages from the channel
EXAMPLE: !purge 50
RESULT: -50 messages in this channel"""
if not ctx.message.channel.permissions_for(ctx.message.server.me).manage_messages:
await self.bot.say("I do not have permission to delete messages...")
if not ctx.message.channel.permissions_for(ctx.message.guild.me).manage_messages:
await ctx.send("I do not have permission to delete messages...")
return
try:
await self.bot.purge_from(ctx.message.channel, limit=limit)
await ctx.message.channel.purge(limit=limit, before=ctx.message)
await ctx.message.delete()
except discord.HTTPException:
await self.bot.send_message(ctx.message.channel, "Detected messages that are too far "
"back for me to delete; I can only bulk delete messages"
" that are under 14 days old.")
try:
await ctx.message.channel.send("Detected messages that are too far "
"back for me to delete; I can only bulk delete messages"
" that are under 14 days old.")
except:
pass
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(manage_messages=True)
async def prune(self, ctx, limit=None):
@commands.command()
@commands.guild_only()
@utils.can_run(manage_messages=True)
async def prune(self, ctx, *specifications):
"""This command can be used to prune messages from certain members
Mention any user you want to prune messages from; if no members are mentioned, the messages removed will be mine
If no limit is provided, then 100 will be used. This is also the max limit we can use
@ -406,124 +118,59 @@ class Mod:
EXAMPLE: !prune 50
RESULT: 50 of my messages are removed from this channel"""
# We can only get logs from 100 messages at a time, so make sure we are not above that threshold
try:
# We may not have been passed a limit, and only mentions
# If this happens, the limit will be set to that first mention
limit = int(limit)
except (TypeError, ValueError):
limit = 100
if limit > 100:
limit = 100
if limit < 0:
await self.bot.say("Limit cannot be less than 0!")
return
limit = 100
for x in specifications:
try:
limit = int(x)
if limit <= 100:
break
else:
limit = 100
except (TypeError, ValueError):
continue
# If no members are provided, assume we're trying to prune our own messages
members = ctx.message.mentions
roles = ctx.message.role_mentions
if len(members) == 0:
members = [ctx.message.server.me]
members = [ctx.message.guild.me]
# Our check for if a message should be deleted
def check(m):
if m.author in members:
return True
if any(x in m.author.roles for x in roles):
if any(r in m.author.roles for r in roles):
return True
return False
# If we're not setting the user to the bot, then we're deleting someone elses messages
# To do so, we need manage_messages permission, so check if we have that
if not ctx.message.channel.permissions_for(ctx.message.server.me).manage_messages:
await self.bot.say("I do not have permission to delete messages...")
if not ctx.message.channel.permissions_for(ctx.message.guild.me).manage_messages:
await ctx.send("I do not have permission to delete messages...")
return
# Since logs_from will give us any message, not just the user's we need
# We'll increment count, and stop deleting messages if we hit the limit.
count = 0
async for msg in self.bot.logs_from(ctx.message.channel, before=ctx.message):
async for msg in ctx.message.channel.history(before=ctx.message):
if check(msg):
try:
await self.bot.delete_message(msg)
await msg.delete()
count += 1
except:
pass
if count >= limit:
break
msg = await self.bot.say("{} messages succesfully deleted".format(count))
msg = await ctx.send("{} messages succesfully deleted".format(count))
await asyncio.sleep(5)
try:
await self.bot.delete_message(msg)
await self.bot.delete_message(ctx.message)
except discord.NotFound:
await msg.delete()
await ctx.message.delete()
except:
pass
@commands.group(aliases=['rule'], pass_context=True, no_pm=True, invoke_without_command=True)
@utils.custom_perms(send_messages=True)
async def rules(self, ctx, rule: int = None):
"""This command can be used to view the current rules on the server
EXAMPLE: !rules 5
RESULT: Rule 5 is printed"""
server_settings = await utils.get_content('server_settings', ctx.message.server.id)
rules = server_settings.get('rules')
if not rules or len(rules) == 0:
await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...")
return
if rule is None:
try:
pages = utils.Pages(self.bot, message=ctx.message, entries=rules, per_page=5)
pages.title = "Rules for {}".format(ctx.message.server.name)
await pages.paginate()
except utils.CannotPaginate as e:
await self.bot.say(str(e))
else:
try:
fmt = rules[rule - 1]
except IndexError:
await self.bot.say("That rules does not exist.")
return
await self.bot.say("Rule {}: \"{}\"".format(rule, fmt))
@rules.command(name='add', aliases=['create'], pass_context=True, no_pm=True)
@utils.custom_perms(manage_server=True)
async def rules_add(self, ctx, *, rule: str):
"""Adds a rule to this server's rules
EXAMPLE: !rules add No fun allowed in this server >:c
RESULT: No more fun...unless they break the rules!"""
key = ctx.message.server.id
entry = {'server_id': key,
'rules': [rule]}
update = {'rules': r.row['rules'].append(rule)}
server_settings = await utils.get_content('server_settings', key)
if server_settings and 'rules' in server_settings.keys():
await utils.update_content('server_settings', update, key)
elif server_settings:
await utils.update_content('server_settings', entry, key)
else:
await utils.add_content('server_settings', entry)
await self.bot.say("I have just saved your new rule, use the rules command to view this server's current rules")
@rules.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True)
@utils.custom_perms(manage_server=True)
async def rules_delete(self, ctx, rule: int):
"""Removes one of the rules from the list of this server's rules
Provide a number to delete that rule
EXAMPLE: !rules delete 5
RESULT: Freedom from opression!"""
update = {'rules': r.row['rules'].delete_at(rule - 1)}
if not await utils.update_content('server_settings', update, ctx.message.server.id):
await self.bot.say("That is not a valid rule number, try running the command again.")
else:
await self.bot.say("I have just removed that rule from your list of rules!")
def setup(bot):
bot.add_cog(Mod(bot))
bot.add_cog(Moderation(bot))

View file

@ -1,703 +0,0 @@
from .voice_utilities import *
import discord
from discord.ext import commands
from . import utils
import math
import time
import asyncio
import re
import os
import glob
import socket
import inspect
if not discord.opus.is_loaded():
discord.opus.load_opus('/usr/lib64/libopus.so.0')
class VoiceState:
def __init__(self, bot, download):
self.current = None
self.voice = None
self.bot = bot
self.play_next_song = asyncio.Event()
# This is the queue that holds all VoiceEntry's
self.songs = Playlist(bot)
self.required_skips = 0
# a set of user_ids that voted
self.skip_votes = set()
# Our actual task that handles the queue system
self.audio_player = self.bot.loop.create_task(self.audio_player_task())
self.opts = {
'default_search': 'auto',
'quiet': True
}
self.volume = 50
self.downloader = download
self.file_names = []
def is_playing(self):
# If our VoiceClient or current VoiceEntry do not exist, then we are not playing a song
if self.voice is None or self.current is None:
return False
# If they do exist, check if the current player has finished
player = self.current.player
try:
return not player.is_done()
except AttributeError:
return False
@property
def player(self):
return self.current.player
def skip(self):
# Make sure we clear the votes, before stopping the player
# When the player is stopped, our toggle_next method is called, so the next song can be played
self.skip_votes.clear()
if self.is_playing():
self.player.stop()
def toggle_next(self):
# Set the Event so that the next song in the queue can be played
self.bot.loop.call_soon_threadsafe(self.play_next_song.set)
async def audio_player_task(self):
while True:
# At the start of our task, clear the Event, so we can wait till it is next set
self.play_next_song.clear()
# Clear the votes skip that were for the last song
self.skip_votes.clear()
# Now wait for the next song in the queue
self.current = await self.songs.get_next_entry()
# Make sure we find a song
while self.current is None:
self.clear_audio_files()
await asyncio.sleep(1)
self.current = await self.songs.get_next_entry()
# At this point we're sure we have a song, however it needs to be downloaded
while not getattr(self.current, 'filename'):
print("Downloading...")
await asyncio.sleep(1)
# Now add this file to our list of filenames, so that it can be deleted later
if self.current.filename not in self.file_names:
self.file_names.append(self.current.filename)
# Create the player object
self.current.player = self.voice.create_ffmpeg_player(
self.current.filename,
before_options="-nostdin",
options="-vn -b:a 128k",
after=self.toggle_next
)
# Now we can start actually playing the song
self.current.player.start()
self.current.player.volume = self.volume / 100
# Save the variable for when our time for this song has started
self.current.start_time = time.time()
# Wait till the Event has been set, before doing our task again
await self.play_next_song.wait()
def clear_audio_files(self):
"""Deletes all the audio files this guild has created"""
for f in self.file_names:
os.remove(f)
self.file_names = []
class Music:
"""Voice related commands.
Works in multiple servers at once.
"""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
down = Downloader(download_folder='audio_tmp')
self.downloader = down
self.bot.downloader = down
self.clear_audio_tmp()
def clear_audio_tmp(self):
files = glob.glob('audio_tmp/*')
for f in files:
os.remove(f)
def get_voice_state(self, server):
state = self.voice_states.get(server.id)
# Internally handle creating a voice state if there isn't a current state
# This can be used for example, in case something is skipped when not being connected
# We create the voice state when checked
# This only creates the state, we are still not playing anything, which can then be handled separately
if state is None:
state = VoiceState(self.bot, self.downloader)
self.voice_states[server.id] = state
return state
async def create_voice_client(self, channel):
"""Creates a voice client and saves it"""
# First join the channel and get the VoiceClient that we'll use to save per server
await self.remove_voice_client(channel.server)
server = channel.server
state = self.get_voice_state(server)
voice = self.bot.voice_client_in(server)
# Attempt 3 times
for i in range(3):
try:
if voice is None:
state.voice = await self.bot.join_voice_channel(channel)
if state.voice:
return True
elif voice.channel == channel:
state.voice = voice
return True
else:
# This shouldn't theoretically ever happen yet it does. Thanks Discord
await voice.disconnect()
state.voice = await self.bot.join_voice_channel(channel)
if state.voice:
return True
except (discord.ClientException, socket.gaierror, ConnectionResetError):
continue
return False
async def remove_voice_client(self, server):
"""Removes any voice clients from a server
This is sometimes needed, due to the unreliability of Discord's voice connection
We do not want to end up with a voice client stuck somewhere, so this cancels any found for a server"""
state = self.get_voice_state(server)
voice = self.bot.voice_client_in(server)
if voice:
await voice.disconnect()
if state.voice:
await state.voice.disconnect()
def __unload(self):
# If this is unloaded, cancel all players and disconnect from all channels
for state in self.voice_states.values():
try:
state.audio_player.cancel()
if state.voice:
self.bot.loop.create_task(state.voice.disconnect())
except:
pass
async def on_voice_state_update(self, before, after):
state = self.get_voice_state(after.server)
if state.voice is None:
return
voice_channel = state.voice.channel
num_members = len(voice_channel.voice_members)
state.required_skips = math.ceil((num_members + 1) / 3)
async def queue_embed_task(self, state, channel, author):
index = 0
message = None
fmt = None
# Our check to ensure the only one who reacts is the bot
def check(reaction, user):
return user == author
possible_reactions = ['\u27A1', '\u2B05', '\u2b06', '\u2b07', '\u274c']
while True:
# Get the current queue (It might change while we're doing this)
# So do this in the while loop
queue = state.songs.entries
count = len(queue)
# This means the last song was removed
if count == 0:
await self.bot.send_message(channel, "Nothing currently in the queue")
break
# Get the current entry
entry = queue[index]
# Get the entry's embed
embed = entry.to_embed()
# Set the embed's title to indicate the amount of things in the queue
count = len(queue)
embed.title = "Current Queue [{}/{}]".format(index+1, count)
# Now we need to send the embed, so check if the message is already set
# If not, then we need to send a new one (i.e. this is the first time called)
if message:
message = await self.bot.edit_message(message, fmt, embed=embed)
# There's only one reaction we want to make sure we remove in the circumstances
# If the member doesn't have kick_members permissions, and isn't the requester
# Then they can't remove the song, otherwise they can
if not author.server_permissions.kick_members and author != entry.requester:
try:
await self.bot.remove_reaction(message, '\u274c', channel.server.me)
except:
pass
elif not author.server_permissions.kick_members and author == entry.requester:
try:
await self.bot.add_reaction(message, '\u274c')
except:
pass
else:
message = await self.bot.send_message(channel, embed=embed)
await self.bot.add_reaction(message, '\N{LEFTWARDS BLACK ARROW}')
await self.bot.add_reaction(message, '\N{BLACK RIGHTWARDS ARROW}')
# The moderation tools that can be used
if author.server_permissions.kick_members:
await self.bot.add_reaction(message, '\N{DOWNWARDS BLACK ARROW}')
await self.bot.add_reaction(message, '\N{UPWARDS BLACK ARROW}')
await self.bot.add_reaction(message, '\N{CROSS MARK}')
elif author == entry.requester:
await self.bot.add_reaction(message, '\N{CROSS MARK}')
# Reset the fmt message
fmt = None
# Now we wait for the next reaction
res = await self.bot.wait_for_reaction(possible_reactions, message=message, check=check, timeout=180)
if res is None:
break
else:
reaction, user = res
# Now we can prepare for the next embed to be sent
# If right is clicked
if '\u27A1' in reaction.emoji:
index += 1
if index >= count:
index = 0
# If left is clicked
elif '\u2B05' in reaction.emoji:
index -= 1
if index < 0:
index = count - 1
# If up is clicked
elif '\u2b06' in reaction.emoji:
# A second check just to make sure, as well as ensuring index is higher than 0
if author.server_permissions.kick_members and index > 0:
if entry != queue[index]:
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
else:
# Remove the current entry
del queue[index]
# Add it one position higher
queue.insert(index - 1, entry)
# Lets move the index to look at the new place of the entry
index -= 1
# If down is clicked
elif '\u2b07' in reaction.emoji:
# A second check just to make sure, as well as ensuring index is lower than last
if author.server_permissions.kick_members and index < (count - 1):
if entry != queue[index]:
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
else:
# Remove the current entry
del queue[index]
# Add it one position lower
queue.insert(index + 1, entry)
# Lets move the index to look at the new place of the entry
index += 1
# If x is clicked
elif '\u274c' in reaction.emoji:
# A second check just to make sure
if author.server_permissions.kick_members or author == entry.requester:
if entry != queue[index]:
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
else:
# Simply remove the entry in place
del queue[index]
# This is the only check we need to make, to ensure index is now not more than last
new_count = count - 1
if index >= new_count:
index = new_count - 1
try:
await self.bot.remove_reaction(message, reaction.emoji, user)
except discord.Forbidden:
pass
await self.bot.delete_message(message)
@commands.command(pass_context=True)
@commands.check(utils.is_owner)
async def vdebug(self, ctx, *, code : str):
"""Evaluates code."""
code = code.strip('` ')
python = '```py\n{}\n```'
result = None
env = {
'bot': self.bot,
'ctx': ctx,
'message': ctx.message,
'server': ctx.message.server,
'channel': ctx.message.channel,
'author': ctx.message.author
}
env.update(globals())
try:
result = eval(code, env)
if inspect.isawaitable(result):
result = await result
except Exception as e:
await self.bot.say(python.format(type(e).__name__ + ': ' + str(e)))
return
await self.bot.say(python.format(result))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def progress(self, ctx):
"""Provides the progress of the current song"""
# Make sure we're playing first
state = self.get_voice_state(ctx.message.server)
if not state.is_playing():
await self.bot.say('Not playing anything.')
else:
progress = state.current.progress
length = state.current.length
# Another check, just to make sure; this may happen for a very brief amount of time
# Between when the song was requested, and still downloading to play
if not progress or not length:
await self.bot.say('Not playing anything.')
return
# Otherwise just format this nicely
progress = divmod(round(progress, 0), 60)
length = divmod(round(length, 0), 60)
fmt = "Current song progress: {0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
await self.bot.say(fmt)
@commands.command(no_pm=True)
@utils.custom_perms(send_messages=True)
async def join(self, *, channel: discord.Channel):
"""Joins a voice channel."""
try:
await self.create_voice_client(channel)
# Check if the channel given was an actual voice channel
except discord.InvalidArgument:
await self.bot.say('This is not a voice channel...')
except (asyncio.TimeoutError, discord.ConnectionClosed):
await self.bot.say("I failed to connect! This usually happens if I don't have permission to join the"
" channel, but can sometimes be caused by your server region being far away."
" Otherwise this is an issue on Discord's end, causing the connect to timeout!")
await self.remove_voice_client(channel.server)
else:
await self.bot.say('Ready to play audio in ' + channel.name)
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def summon(self, ctx):
"""Summons the bot to join your voice channel."""
# This method will be invoked by other commands, so we should return True or False instead of just returning
# First check if the author is even in a voice_channel
summoned_channel = ctx.message.author.voice_channel
if summoned_channel is None:
await self.bot.say('You are not in a voice channel.')
return False
# Then simply create a voice client
try:
success = await self.create_voice_client(summoned_channel)
except (asyncio.TimeoutError, discord.ConnectionClosed):
await self.bot.say("I failed to connect! This usually happens if I don't have permission to join the"
" channel, but can sometimes be caused by your server region being far away."
" Otherwise this is an issue on Discord's end, causing the connect to timeout!")
await self.remove_voice_client(summoned_channel.server)
return False
if success:
try:
await self.bot.say('Ready to play audio in ' + summoned_channel.name)
except discord.Forbidden:
pass
return success
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def play(self, ctx, *, song: str):
"""Plays a song.
If there is a song currently in the queue, then it is
queued until the next song is done playing.
This command automatically searches as well from YouTube.
The list of supported sites can be found here:
https://rg3.github.io/youtube-dl/supportedsites.html
"""
state = self.get_voice_state(ctx.message.server)
# First check if we are connected to a voice channel at all, if not summon to the channel the author is in
# Since summon utils if the author is in a channel, we don't need to handle that here, just return if it failed
if state.voice is None:
success = await ctx.invoke(self.summon)
if not success:
return
# If the queue is full, we ain't adding anything to it
if state.songs.full:
await self.bot.say("The queue is currently full! You'll need to wait to add a new song")
return
author_channel = ctx.message.author.voice.voice_channel
my_channel = ctx.message.server.me.voice.voice_channel
if my_channel is None:
# If we're here this means that after 3 attempts...4 different "failsafes"...
# Discord has returned saying the connection was successful, and returned a None connection
await self.bot.say("I failed to connect to the channel! Please try again soon")
return
# To try to avoid some abuse, ensure the requester is actually in our channel
if my_channel != author_channel:
await self.bot.say("You are not currently in the channel; please join before trying to request a song.")
return
# Set the number of required skips to start
num_members = len(my_channel.voice_members)
state.required_skips = math.ceil((num_members + 1) / 3)
# Create the player, and check if this was successful
# Here all we want is to get the information of the player
song = re.sub('[<>\[\]]', '', song)
try:
_entry, position = await state.songs.add_entry(song, ctx.message.author)
except WrongEntryTypeError:
# This means that a song was attempted to be searched, instead of a link provided
try:
info = await self.downloader.extract_info(self.bot.loop, song, download=False, process=True)
song = info.get('entries', [])[0]['webpage_url']
except IndexError:
await self.bot.send_message(ctx.message.channel, "No results found for {}!".format(song))
return
except ExtractionError as e:
# This gets the youtube_dl error, instead of our error raised
error = str(e).split("\n\n")[1]
# Youtube has a "fancy" colour error message it prints to the console
# Obviously this doesn't work in Discord, so just remove this
error = " ".join(error.split()[1:])
await self.bot.send_message(ctx.message.channel, error)
return
try:
_entry, position = await state.songs.add_entry(song, ctx.message.author)
except WrongEntryTypeError:
# This is either a playlist, or something not supported
fmt = "Sorry but I couldn't download that! Either you provided a playlist, a streamed link, or " \
"a page that is not supported to download."
await self.bot.send_message(ctx.message.channel, fmt)
return
except ExtractionError as e:
# This gets the youtube_dl error, instead of our error raised
error = str(e).split("\n\n")[1]
# Youtube has a "fancy" colour error message it prints to the console
# Obviously this doesn't work in Discord, so just remove this
error = " ".join(error.split()[1:])
# Make sure we are not over our 2000 message limit length (there are some youtube errors that are)
if len(error) >= 2000:
error = "{}...".format(error[:1996])
await self.bot.send_message(ctx.message.channel, error)
return
await self.bot.say('Enqueued ' + str(_entry))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(kick_members=True)
async def volume(self, ctx, value: int = None):
"""Sets the volume of the currently playing song."""
state = self.get_voice_state(ctx.message.server)
if value is None:
volume = state.volume
await self.bot.say("Current volume is {}".format(volume))
return
if value > 200:
await self.bot.say("Sorry but the max volume is 200")
return
state.volume = value
if state.is_playing():
player = state.player
player.volume = value / 100
await self.bot.say('Set the volume to {:.0%}'.format(player.volume))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(kick_members=True)
async def pause(self, ctx):
"""Pauses the currently played song."""
state = self.get_voice_state(ctx.message.server)
if state.is_playing():
state.player.pause()
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(kick_members=True)
async def resume(self, ctx):
"""Resumes the currently played song."""
state = self.get_voice_state(ctx.message.server)
if state.is_playing():
state.player.resume()
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(kick_members=True)
async def stop(self, ctx):
"""Stops playing audio and leaves the voice channel.
This also clears the queue.
"""
server = ctx.message.server
state = self.get_voice_state(server)
# Stop playing whatever song is playing.
if state.is_playing():
state.player.stop()
state.songs.clear()
# This will stop cancel the audio event we're using to loop through the queue
# Then erase the voice_state entirely, and disconnect from the channel
try:
state.audio_player.cancel()
state.clear_audio_files()
await self.remove_voice_client(ctx.message.server)
del self.voice_states[server.id]
except:
pass
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def eta(self, ctx):
"""Provides an ETA on when your next song will play"""
# Note: There is no way to tell how long a song has been playing, or how long there is left on a song
# That is why this is called an "ETA"
state = self.get_voice_state(ctx.message.server)
author = ctx.message.author
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
return
queue = state.songs.entries
if len(queue) == 0:
await self.bot.say("Nothing currently in the queue")
return
# Start off by adding the remaining length of the current song
count = state.current.remaining
found = False
# Loop through the songs in the queue, until the author is found as the requester
# The found bool is used to see if we actually found the author, or we just looped through the whole queue
for song in queue:
if song.requester == author:
found = True
break
count += song.duration
# This is checking if nothing from the queue has been added to the total
# If it has not, then we have not looped through the queue at all
# Since the queue was already checked to have more than one song in it, this means the author is next
if count == state.current.duration:
await self.bot.say("You are next in the queue!")
return
if not found:
await self.bot.say("You are not in the queue!")
return
await self.bot.say("ETA till your next play is: {0[0]}m {0[1]}s".format(divmod(round(count, 0), 60)))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def queue(self, ctx):
"""Provides a printout of the songs that are in the queue"""
state = self.get_voice_state(ctx.message.server)
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
return
# Asyncio provides no non-private way to access the queue, so we have to use _queue
queue = state.songs.entries
if len(queue) == 0:
await self.bot.say("Nothing currently in the queue")
else:
self.bot.loop.create_task(self.queue_embed_task(state, ctx.message.channel, ctx.message.author))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def queuelength(self, ctx):
"""Prints the length of the queue"""
await self.bot.say("There are a total of {} songs in the queue"
.format(len(self.get_voice_state(ctx.message.server).songs.entries)))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def skip(self, ctx):
"""Vote to skip a song. The song requester can automatically skip.
approximately 1/3 of the members in the voice channel
are required to vote to skip for the song to be skipped.
"""
state = self.get_voice_state(ctx.message.server)
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
return
# Check if the person requesting a skip is the requester of the song, if so automatically skip
voter = ctx.message.author
if voter == state.current.requester:
await self.bot.say('Requester requested skipping song...')
state.skip()
# Otherwise check if the voter has already voted
elif voter.id not in state.skip_votes:
state.skip_votes.add(voter.id)
total_votes = len(state.skip_votes)
# Now check how many votes have been made, if 3 then go ahead and skip, otherwise add to the list of votes
if total_votes >= state.required_skips:
await self.bot.say('Skip vote passed, skipping song...')
state.skip()
else:
await self.bot.say('Skip vote added, currently at [{}/{}]'.format(total_votes, state.required_skips))
else:
await self.bot.say('You have already voted to skip this song.')
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(kick_members=True)
async def modskip(self, ctx):
"""Forces a song skip, can only be used by a moderator"""
state = self.get_voice_state(ctx.message.server)
if not state.is_playing():
await self.bot.say('Not playing any music right now...')
return
state.skip()
await self.bot.say('Song has just been skipped.')
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def playing(self, ctx):
"""Shows info about the currently played song."""
state = self.get_voice_state(ctx.message.server)
if not state.is_playing():
await self.bot.say('Not playing anything.')
else:
# Create the embed object we'll use
embed = discord.Embed()
# Fill in the simple things
embed.add_field(name='Title', value=state.current.title, inline=False)
embed.add_field(name='Requester', value=state.current.requester.display_name, inline=False)
# Get the amount of current skips, and display how many have been skipped/how many required
skip_count = len(state.skip_votes)
embed.add_field(name='Skip Count', value='{}/{}'.format(skip_count, state.required_skips), inline=False)
# Get the current progress and display this
progress = state.current.progress
length = state.current.length
progress = divmod(round(progress, 0), 60)
length = divmod(round(length, 0), 60)
fmt = "{0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
embed.add_field(name='Progress', value=fmt,inline=False)
# And send the embed
await self.bot.say(embed=embed)
def setup(bot):
bot.add_cog(Music(bot))

View file

@ -1,191 +1,198 @@
from .utils import config
from .utils import checks
from .utils import images
import utils
from discord.ext import commands
import discord
import aiohttp
from osuapi import OsuApi, AHConnector
from asyncpg import UniqueViolationError
# https://github.com/ppy/osu-api/wiki
base_url = 'https://osu.ppy.sh/api/'
BASE_URL = 'https://osu.ppy.sh/api/'
MAX_RETRIES = 5
class Osu:
class Osu(commands.Cog):
"""View OSU stats"""
def __init__(self, bot):
self.bot = bot
self.headers = {'User-Agent': config.user_agent}
self.key = config.osu_key
self.api = OsuApi(utils.osu_key, connector=AHConnector())
self.bot.loop.create_task(self.get_users())
self.osu_users = {}
async def _request(self, payload, endpoint):
"""Handles requesting to the API"""
async def get_user(self, member, username):
"""A function used to get and save user data in cache"""
user = self.osu_users.get(member.id)
if user is None:
user = await self.get_user_from_api(username)
if user is not None:
self.osu_users[member.id] = user
return user
# Just return False for the "osu API does not work correctly" issue
elif user is False:
return False
else:
if user.username.lower() == username.lower():
return user
else:
user = await self.get_user_from_api(username)
if user is not None:
self.osu_users[member.id] = user
return user
# Format the URL we'll need based on the base_url, and the endpoint we want to hit
url = "{}{}".format(base_url, endpoint)
# Check if our key was added, if it wasn't, add it
key = payload.get('k', self.key)
payload['k'] = key
# Attempt to connect up to our max retries
for x in range(MAX_RETRIES):
try:
async with aiohttp.get(url, headers=self.headers, params=payload) as r:
# If we failed to connect, attempt again
if r.status != 200:
continue
data = await r.json()
return data
# If any error happened when making the request, attempt again
except:
continue
async def find_beatmap(self, query):
"""Finds a beatmap ID based on the first match of searching a beatmap"""
pass
async def get_beatmap(self, b_id):
"""Gets beatmap info based on the ID provided"""
params = {'b': b_id}
endpoint = 'get_beatmaps'
data = await self._request(params, endpoint)
async def get_user_from_api(self, username):
"""A simple helper function to parse the list given and handle failures"""
try:
return data[0]
user = await self.api.get_user(username)
return user[0]
except IndexError:
return None
# So we can send a specific message for this error
except TypeError:
return False
@commands.group(pass_context=True, invoke_without_command=True)
@checks.custom_perms(send_messages=True)
async def osu(self, ctx):
pass
async def get_users(self):
"""A task used to 'cache' all member's and their Osu profile's"""
await self.bot.wait_until_ready()
@osu.command(name='scores', aliases=['score'], pass_context=True)
@checks.custom_perms(send_messages=True)
async def osu_user_scores(self, ctx, user, song=1):
"""Used to get the top scores for a user
You can provide either your Osu ID or your username
However, if your username is only numbers, this will confuse the API
If you have only numbers in your username, you will need to provide your ID
This will by default return the top song, provide song [up to 100] to get that song, in order from best to worst
query = "SELECT id, osu FROM users WHERE osu IS NOT NULL;"
rows = await self.bot.db.fetch(query)
EXAMPLE: !osu MyUsername 5
RESULT: Info about your 5th best song"""
for row in rows:
user = await self.get_user_from_api(row['osu'])
if user is not None:
self.osu_users[row['id']] = user
await self.bot.say("Looking up your Osu information...")
# To make this easy for the user, it's indexed starting at 1, so lets subtract by 1
song -= 1
# Make sure the song is not negative however, if so set to 0
if song < 0:
song = 0
@commands.group(invoke_without_command=True)
@utils.can_run(send_messages=True)
async def osu(self, ctx, member: discord.Member = None):
"""Provides basic information about a specific user
# A list of the possible values we'll receive, that we want to display
wanted_info = ['username', 'maxcombo', 'count300', 'count100', 'count50', 'countmiss',
'perfect', 'enabled_mods', 'date', 'rank' 'pp', 'beatmap_title', 'beatmap_version',
'max_combo', 'artist', 'difficulty']
EXAMPLE: !osu @Person
RESULT: Informationa bout that person's osu account"""
# A couple of these aren't the best names to display, so setup a map to change these just a little bit
key_map = {'maxcombo': 'combo',
'count300': '300 hits',
'count100': '100 hits',
'count50': '50 hits',
'countmiss': 'misses',
'perfect': 'got_max_combo'}
if member is None:
member = ctx.message.author
params = {'u': user,
'limit': 100}
# The endpoint that we're accessing to get this informatin
endpoint = 'get_user_best'
data = await self._request(params, endpoint)
try:
data = data[song]
except IndexError:
if len(data) == 0:
await self.bot.say("I could not find any top songs for the user {}".format(user))
return
else:
data = data[len(data) - 1]
# There's a little bit more info that we need, some info specific to the beatmap.
# Due to this we'll need to make a second request
beatmap_data = await self.get_beatmap(data.get('beatmap_id', None))
# Lets add the extra data we want
data['beatmap_title'] = beatmap_data.get('title')
data['beatmap_version'] = beatmap_data.get('version')
data['max_combo'] = beatmap_data.get('max_combo')
data['artist'] = beatmap_data.get('artist')
# Lets round this, no need for such a long number
data['difficulty'] = round(float(beatmap_data.get('difficultyrating')), 2)
# Now lets create our dictionary needed to create the image
# The dict comprehension we're using is simpler than it looks, it's simply they key: value
# If the key is in our wanted_info list
# We also get the wanted value from the key_map if it exists, using the key itself if it doesn't
# We then title it and replace _ with a space to ensure nice formatting
fmt = [(key_map.get(k, k).title().replace('_', ' '), v) for k, v in data.items() if k in wanted_info]
# Attempt to create our banner and upload that
# If we can't find the images needed, or don't have permissions, just send a message instead
try:
banner = await images.create_banner(ctx.message.author, "Osu User Stats", fmt)
await self.bot.upload(banner)
except (FileNotFoundError, discord.Forbidden):
_fmt = "\n".join("{}: {}".format(k, r) for k, r in fmt)
await self.bot.say("```\n{}```".format(_fmt))
@osu.command(name='user', pass_context=True)
@checks.custom_perms(send_messages=True)
async def osu_user_info(self, ctx, *, user):
"""Used to get information about a specific user
You can provide either your Osu ID or your username
However, if your username is only numbers, this will confuse the API
If you have only numbers in your username, you will need to provide your ID
EXAMPLE: !osu user MyUserName
RESULT: Info about your user"""
await self.bot.say("Looking up your Osu information...")
# A list of the possible values we'll receive, that we want to display
wanted_info = ['username', 'playcount', 'ranked_score', 'pp_rank', 'level', 'pp_country_rank',
'accuracy', 'country', 'pp_country_rank', 'count_rank_s', 'count_rank_a']
# A couple of these aren't the best names to display, so setup a map to change these just a little bit
key_map = {'playcount': 'play_count',
'count_rank_ss': 'total_SS_ranks',
'count_rank_s': 'total_s_ranks',
'count_rank_a': 'total_a_ranks'}
# The paramaters that we'll send to osu to get the information needed
params = {'u': user}
# The endpoint that we're accessing to get this informatin
endpoint = 'get_user'
data = await self._request(params, endpoint)
# Make sure we found a result, we should only find one with the way we're searching
try:
data = data[0]
except IndexError:
await self.bot.say("I could not find anyone with the user name/id of {}".format(user))
user = self.osu_users.get(member.id)
if user is None:
await ctx.send("I do not have {}'s Osu user saved!".format(member.display_name))
return
# Now lets create our dictionary needed to create the image
# The dict comprehension we're using is simpler than it looks, it's simply they key: value
# If the key is in our wanted_info list
# We also get the wanted value from the key_map if it exists, using the key itself if it doesn't
# We then title it and replace _ with a space to ensure nice formatting
fmt = {key_map.get(k, k).title().replace('_', ' '): v for k, v in data.items() if k in wanted_info}
e = discord.Embed(title='Osu profile for {}'.format(user.username))
e.add_field(name='Rank', value="{:,}".format(user.pp_rank))
e.add_field(name='Level', value=user.level)
e.add_field(name='Performance Points', value="{:,}".format(user.pp_raw))
e.add_field(name='Accuracy', value="{:.2%}".format(user.accuracy / 100))
e.add_field(name='SS Ranks', value="{:,}".format(user.count_rank_ss))
e.add_field(name='S Ranks', value="{:,}".format(user.count_rank_s))
e.add_field(name='A Ranks', value="{:,}".format(user.count_rank_a))
e.add_field(name='Country', value=user.country)
e.add_field(name='Country Rank', value="{:,}".format(user.pp_country_rank))
e.add_field(name='Playcount', value="{:,}".format(user.playcount))
e.add_field(name='Ranked Score', value="{:,}".format(user.ranked_score))
e.add_field(name='Total Score', value="{:,}".format(user.total_score))
await ctx.send(embed=e)
@osu.command(name='add', aliases=['create', 'connect'])
@utils.can_run(send_messages=True)
async def osu_add(self, ctx, *, username):
"""Links an osu account to your discord account
EXAMPLE: !osu add username
RESULT: Links your username to your account, and allows stats to be pulled from it"""
author = ctx.message.author
user = await self.get_user(author, username)
if user is None:
await ctx.send("I couldn't find an osu user that matches {}".format(username))
return
elif user is False:
await ctx.send("Unfortunately OSU's API is a 'beta', and for some users they do not return **any** data."
"In this case, that's you! Congrats?")
# Attempt to create our banner and upload that
# If we can't find the images needed, or don't have permissions, just send a message instead
try:
banner = await images.create_banner(ctx.message.author, "Osu User Stats", fmt)
await self.bot.upload(banner)
except (FileNotFoundError, discord.Forbidden):
_fmt = "\n".join("{}: {}".format(k, r) for k, r in fmt.items())
await self.bot.say("```\n{}```".format(_fmt))
await ctx.bot.db.execute("INSERT INTO users (id, osu) VALUES ($1, $2)", ctx.author.id, user.username)
except UniqueViolationError:
await ctx.bot.db.execute("UPDATE users SET osu = $1 WHERE id = $2", user.username, ctx.author.id)
await ctx.send("I have just saved your Osu user {}".format(author.display_name))
@osu.command(name='score', aliases=['scores'])
@utils.can_run(send_messages=True)
async def osu_scores(self, ctx, *data):
"""Find the top x osu scores for a provided member
Note: You can only get the top 50 songs for a user
EXAMPLE: !osu scores @Person 5
RESULT: The top 5 maps for the user @Person"""
# Set the defaults before we go through our passed data to figure out what we want
limit = 5
member = ctx.message.author
# Only loop through the first 2....we care about nothing after that
for piece in data[:2]:
# First lets try to convert to an int, for limit
try:
limit = int(piece)
# Since Osu's API returns no information about the beatmap on scores
# We also need to call it to get the beatmap...in order to not get rate limited
# Lets limit this to 50
if limit > 50:
limit = 50
elif limit < 1:
limit = 5
except Exception:
converter = commands.converter.MemberConverter()
try:
member = await converter.convert(ctx, piece)
except commands.converter.BadArgument:
pass
user = self.osu_users.get(member.id)
if user is None:
await ctx.send("I don't have that user's Osu account saved!")
return
scores = await self.api.get_user_best(user.username, limit=limit)
entries = []
if len(scores) == 0:
await ctx.send("Sorry, but I can't find any scores for you {}!".format(member.display_name))
return
for i in scores:
m = await self.api.get_beatmaps(beatmap_id=i.beatmap_id)
m = m[0]
entry = {
'title': 'Top {} Osu scores for {}'.format(limit, member.display_name),
'fields': [
{'name': 'Artist', 'value': m.artist},
{'name': 'Title', 'value': m.title},
{'name': 'Creator', 'value': m.creator},
{'name': 'CS (Circle Size)', 'value': m.diff_size},
{'name': 'AR (Approach Rate)', 'value': m.diff_approach},
{'name': 'HP (Health Drain)', 'value': m.diff_drain},
{'name': 'OD (Overall Difficulty)', 'value': m.diff_overall},
{'name': 'Length', 'value': m.total_length},
{'name': 'Score', 'value': i.score},
{'name': 'Max Combo', 'value': i.maxcombo},
{'name': 'Hits',
'value': "{}/{}/{}/{} (300/100/50/misses)".format(i.count300, i.count100, i.count50, i.countmiss),
"inline": False},
{'name': 'Perfect', 'value': "Yes" if i.perfect else "No"},
{'name': 'Rank', 'value': i.rank},
{'name': 'PP', 'value': i.pp},
{'name': 'Mods', 'value': str(i.enabled_mods)},
{'name': 'Date', 'value': str(i.date)}
]
}
entries.append(entry)
try:
pages = utils.DetailedPages(ctx.bot, message=ctx.message, entries=entries)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
def setup(bot):

View file

@ -1,7 +1,8 @@
from . import utils
import utils
from discord.ext import commands
import discord
import logging
BASE_URL = "https://api.owapi.net/api/v3/u/"
# This is a list of the possible things that we may want to retrieve from the stats
@ -12,45 +13,46 @@ check_g_stats = ["eliminations", "deaths", 'kpd', 'wins', 'losses', 'time_played
'cards', 'damage_done', 'healing_done', 'multikills']
check_o_stats = ['wins']
log = logging.getLogger()
class Overwatch:
class Overwatch(commands.Cog):
"""Class for viewing Overwatch stats"""
def __init__(self, bot):
self.bot = bot
@commands.group(no_pm=True)
async def ow(self):
@commands.group()
async def ow(self, ctx):
"""Command used to lookup information on your own user, or on another's
When adding your battletag, it is quite picky, use the exact format user#xxxx
Multiple names with the same username can be used, this is why the numbers are needed
Capitalization also matters"""
pass
@ow.command(name="stats", pass_context=True)
@utils.custom_perms(send_messages=True)
@ow.command(name="stats")
@utils.can_run(send_messages=True)
async def ow_stats(self, ctx, user: discord.Member = None, hero: str = ""):
"""Prints out a basic overview of a member's stats
Provide a hero after the member to get stats for that specific hero
EXAMPLE: !ow stats @OtherPerson Junkrat
RESULT: Whether or not you should unfriend this person because they're a dirty rat"""
user = user or ctx.message.author
ow_stats = await utils.get_content('overwatch', user.id)
bt = ctx.bot.db.load('overwatch', key=str(user.id), pluck='battletag')
if ow_stats is None:
await self.bot.say("I do not have this user's battletag saved!")
if bt is None:
await ctx.send("I do not have this user's battletag saved!")
return
# This API sometimes takes a while to look up information, so send a message saying we're processing
bt = ow_stats['battletag']
if hero == "":
# If no hero was provided, we just want the base stats for a player
url = BASE_URL + "{}/stats".format(bt)
data = await utils.request(url)
region = [x for x in data.keys() if data[x] is not None][0]
if data is None:
await ctx.send("I couldn't connect to overwatch at the moment!")
return
log.info(data)
region = [x for x in data.keys() if data[x] is not None and x in ['us', 'any', 'kr', 'eu']][0]
stats = data[region]['stats']['quickplay']
output_data = [(k.title().replace("_", " "), r) for k, r in stats['game_stats'].items() if
@ -60,14 +62,17 @@ class Overwatch:
hero = hero.lower().replace('-', '')
url = BASE_URL + "{}/heroes".format(bt)
data = await utils.request(url)
if data is None:
await ctx.send("I couldn't connect to overwatch at the moment!")
return
region = [x for x in data.keys() if data[x] is not None][0]
region = [x for x in data.keys() if data[x] is not None and x in ['us', 'any', 'kr', 'eu']][0]
stats = data[region]['heroes']['stats']['quickplay'].get(hero)
if stats is None:
fmt = "I couldn't find data with that hero, make sure that is a valid hero, " \
"otherwise {} has never used the hero {} before!".format(user.display_name, hero)
await self.bot.say(fmt)
await ctx.send(fmt)
return
# Same list comprehension as before
@ -77,53 +82,56 @@ class Overwatch:
output_data.append((k.title().replace("_", " "), r))
try:
banner = await utils.create_banner(user, "Overwatch", output_data)
await self.bot.upload(banner)
await ctx.send(file=discord.File(banner, filename='banner.png'))
except (FileNotFoundError, discord.Forbidden):
fmt = "\n".join("{}: {}".format(k, r) for k, r in output_data)
await self.bot.say("Overwatch stats for {}: ```py\n{}```".format(user.name, fmt))
await ctx.send("Overwatch stats for {}: ```py\n{}```".format(user.name, fmt))
@ow.command(pass_context=True, name="add")
@utils.custom_perms(send_messages=True)
@ow.command(name="add")
@utils.can_run(send_messages=True)
async def add(self, ctx, bt: str):
"""Saves your battletag for looking up information
EXAMPLE: !ow add Username#1234
RESULT: Your battletag is now saved"""
await ctx.message.channel.trigger_typing()
# Battletags are normally provided like name#id
# However the API needs this to be a -, so repliace # with - if it exists
bt = bt.replace("#", "-")
key = ctx.message.author.id
key = str(ctx.message.author.id)
# All we're doing here is ensuring that the status is 200 when looking up someone's general information
# If it's not, let them know exactly how to format their tag
url = BASE_URL + "{}/stats".format(bt)
data = await utils.request(url)
if data is None:
await self.bot.say("Profile does not exist! Battletags are picky, "
"format needs to be `user#xxxx`. Capitalization matters")
await ctx.send("Profile does not exist! Battletags are picky, "
"format needs to be `user#xxxx`. Capitalization matters")
return
# Now just save the battletag
entry = {'member_id': key, 'battletag': bt}
update = {'battletag': bt}
# Try adding this first, if that fails, update the saved entry
if not await utils.add_content('overwatch', entry):
await utils.update_content('overwatch', update, key)
await self.bot.say("I have just saved your battletag {}".format(ctx.message.author.mention))
entry = {
'member_id': key,
'battletag': bt
}
@ow.command(pass_context=True, name="delete", aliases=['remove'])
@utils.custom_perms(send_messages=True)
await ctx.bot.db.save('overwatch', entry)
await ctx.send("I have just saved your battletag {}".format(ctx.message.author.mention))
@ow.command(name="delete", aliases=['remove'])
@utils.can_run(send_messages=True)
async def delete(self, ctx):
"""Removes your battletag from the records
EXAMPLE: !ow delete
RESULT: Your battletag is no longer saved"""
if await utils.remove_content('overwatch', ctx.message.author.id):
await self.bot.say("I no longer have your battletag saved {}".format(ctx.message.author.mention))
else:
await self.bot.say("I don't even have your battletag saved {}".format(ctx.message.author.mention))
entry = {
'member_id': str(ctx.message.author.id),
'battletag': None
}
await ctx.bot.db.save('overwatch', entry)
await ctx.send("I no longer have your battletag saved {}".format(ctx.message.author.mention))
def setup(bot):

View file

@ -1,94 +1,226 @@
from discord.ext import commands
from . import utils
import re
import glob
import asyncio
import discord
import inspect
import aiohttp
import pendulum
import asyncio
import textwrap
import traceback
import subprocess
import io
from contextlib import redirect_stdout
class Owner:
"""Commands that can only be used by Phantom, bot management commands"""
def get_syntax_error(e):
if e.text is None:
return '```py\n{0.__class__.__name__}: {0}\n```'.format(e)
return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(e, '^', type(e).__name__)
def __init__(self, bot):
self.bot = bot
class Owner(commands.Cog):
"""Commands that can only be used by the owner of the bot, bot management commands"""
_last_result = None
sessions = set()
async def cog_check(self, ctx):
return await ctx.bot.is_owner(ctx.author)
@staticmethod
def cleanup_code(content):
"""Automatically removes code blocks from the code."""
# remove ```py\n```
if content.startswith('```') and content.endswith('```'):
return '\n'.join(content.split('\n')[1:-1])
# remove `foo`
return content.strip('` \n')
@commands.command(hidden=True)
async def repl(self, ctx):
msg = ctx.message
variables = {
'ctx': ctx,
'bot': ctx.bot,
'message': msg,
'guild': msg.guild,
'server': msg.guild,
'channel': msg.channel,
'author': msg.author,
'self': self,
'_': None,
}
if msg.channel.id in self.sessions:
await ctx.send('Already running a REPL session in this channel. Exit it with `quit`.')
return
self.sessions.add(msg.channel.id)
await ctx.send('Enter code to execute or evaluate. `exit()` or `quit` to exit.')
def check(m):
return m.author.id == msg.author.id and \
m.channel.id == msg.channel.id and \
m.content.startswith('`')
code = None
while True:
try:
response = await ctx.bot.wait_for('message', check=check, timeout=10.0 * 60.0)
except asyncio.TimeoutError:
await ctx.send('Exiting REPL session.')
self.sessions.remove(msg.channel.id)
break
cleaned = self.cleanup_code(response.content)
if cleaned in ('quit', 'exit', 'exit()'):
await ctx.send('Exiting.')
self.sessions.remove(msg.channel.id)
return
executor = exec
if cleaned.count('\n') == 0:
# single statement, potentially 'eval'
try:
code = compile(cleaned, '<repl session>', 'eval')
except SyntaxError:
pass
else:
executor = eval
if executor is exec:
try:
code = compile(cleaned, '<repl session>', 'exec')
except SyntaxError as e:
await ctx.send(get_syntax_error(e))
continue
variables['message'] = response
fmt = None
stdout = io.StringIO()
try:
with redirect_stdout(stdout):
result = executor(code, variables)
if inspect.isawaitable(result):
result = await result
except Exception:
value = stdout.getvalue()
fmt = '```py\n{}{}\n```'.format(value, traceback.format_exc())
else:
value = stdout.getvalue()
if result is not None:
fmt = '```py\n{}{}\n```'.format(value, result)
variables['_'] = result
elif value:
fmt = '```py\n{}\n```'.format(value)
try:
if fmt is not None:
if len(fmt) > 2000:
await ctx.send('Content too big to be printed.')
else:
await ctx.send(fmt)
except discord.Forbidden:
pass
except discord.HTTPException as e:
await ctx.send('Unexpected error: `{}`'.format(e))
@commands.command()
@commands.check(utils.is_owner)
async def motd_push(self, *, message):
"""Used to push a new message to the message of the day"""
date = pendulum.utcnow().to_date_string()
key = date
entry = {'motd': message, 'date': date}
# Try to add this, if there's an entry for that date, lets update it to make sure only one motd is sent a day
# I should be managing this myself, more than one should not be sent in a day
if await utils.add_content('motd', entry):
await utils.update_content('motd', entry, key)
await self.bot.say("New motd update for {}!".format(date))
@commands.command(pass_context=True)
@commands.check(utils.is_owner)
async def debug(self, ctx, *, code : str):
"""Evaluates code."""
code = code.strip('` ')
python = '```py\n{}\n```'
result = None
async def sendtochannel(self, ctx, cid: int, *, message):
"""Sends a message to a provided channel, by ID"""
channel = ctx.bot.get_channel(cid)
await channel.send(message)
try:
await ctx.message.delete()
except discord.Forbidden:
pass
@commands.command()
async def debug(self, ctx, *, body: str):
env = {
'bot': self.bot,
'bot': ctx.bot,
'ctx': ctx,
'message': ctx.message,
'server': ctx.message.server,
'channel': ctx.message.channel,
'author': ctx.message.author
'author': ctx.message.author,
'server': ctx.message.guild,
'guild': ctx.message.guild,
'message': ctx.message,
'self': self,
'_': self._last_result
}
env.update(globals())
try:
result = eval(code, env)
if inspect.isawaitable(result):
result = await result
except Exception as e:
await self.bot.say(python.format(type(e).__name__ + ': ' + str(e)))
return
try:
await self.bot.say(python.format(result))
except discord.HTTPException:
await self.bot.say("Result is too long for me to send")
except:
pass
body = self.cleanup_code(body)
stdout = io.StringIO()
@commands.command(pass_context=True)
@commands.check(utils.is_owner)
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ')
try:
exec(to_compile, env)
except SyntaxError as e:
return await ctx.send(get_syntax_error(e))
func = env['func']
try:
with redirect_stdout(stdout):
ret = await func()
except Exception:
value = stdout.getvalue()
await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```"[:2000])
else:
value = stdout.getvalue()
try:
await ctx.message.add_reaction('\u2705')
except Exception:
pass
if ret is None:
if value:
await ctx.send(f"```py\n{value}\n```"[:2000])
else:
self._last_result = ret
await ctx.send(f"```py\n{value}{ret}\n```"[:2000])
@commands.command()
async def bash(self, ctx, *, cmd: str):
"""Runs a bash command"""
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT
)
stdout = (await proc.communicate())[0]
if stdout:
await ctx.send(f'[stdout]\n{stdout.decode()}')
else:
await ctx.send("Process finished, no output")
@commands.command()
async def shutdown(self, ctx):
"""Shuts the bot down"""
fmt = 'Shutting down, I will miss you {0.author.name}'
await self.bot.say(fmt.format(ctx.message))
await self.bot.logout()
await self.bot.close()
await ctx.send(fmt.format(ctx.message))
await ctx.bot.logout()
await ctx.bot.close()
@commands.command()
@commands.check(utils.is_owner)
async def name(self, newNick: str):
async def name(self, ctx, new_nick: str):
"""Changes the bot's name"""
await self.bot.edit_profile(username=newNick)
await self.bot.say('Changed username to ' + newNick)
await ctx.bot.user.edit(username=new_nick)
await ctx.send('Changed username to ' + new_nick)
@commands.command()
@commands.check(utils.is_owner)
async def status(self, *, status: str):
async def status(self, ctx, *, status: str):
"""Changes the bot's 'playing' status"""
await self.bot.change_status(discord.Game(name=status, type=0))
await self.bot.say("Just changed my status to '{0}'!".format(status))
await ctx.bot.change_presence(activity=discord.Game(name=status, type=0))
await ctx.send("Just changed my status to '{}'!".format(status))
@commands.command()
@commands.check(utils.is_owner)
async def load(self, *, module: str):
async def load(self, ctx, *, module: str):
"""Loads a module"""
# Do this because I'm too lazy to type cogs.module
@ -98,15 +230,14 @@ class Owner:
# This try catch will catch errors such as syntax errors in the module we are loading
try:
self.bot.load_extension(module)
await self.bot.say("I have just loaded the {} module".format(module))
ctx.bot.load_extension(module)
await ctx.send("I have just loaded the {} module".format(module))
except Exception as error:
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```'
await self.bot.say(fmt.format(type(error).__name__, error))
await ctx.send(fmt.format(type(error).__name__, error))
@commands.command()
@commands.check(utils.is_owner)
async def unload(self, *, module: str):
async def unload(self, ctx, *, module: str):
"""Unloads a module"""
# Do this because I'm too lazy to type cogs.module
@ -114,27 +245,26 @@ class Owner:
if not module.startswith("cogs"):
module = "cogs.{}".format(module)
self.bot.unload_extension(module)
await self.bot.say("I have just unloaded the {} module".format(module))
ctx.bot.unload_extension(module)
await ctx.send("I have just unloaded the {} module".format(module))
@commands.command()
@commands.check(utils.is_owner)
async def reload(self, *, module: str):
async def reload(self, ctx, *, module: str):
"""Reloads a module"""
# Do this because I'm too lazy to type cogs.module
module = module.lower()
if not module.startswith("cogs"):
module = "cogs.{}".format(module)
self.bot.unload_extension(module)
ctx.bot.unload_extension(module)
# This try block will catch errors such as syntax errors in the module we are loading
try:
self.bot.load_extension(module)
await self.bot.say("I have just reloaded the {} module".format(module))
ctx.bot.load_extension(module)
await ctx.send("I have just reloaded the {} module".format(module))
except Exception as error:
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```'
await self.bot.say(fmt.format(type(error).__name__, error))
await ctx.send(fmt.format(type(error).__name__, error))
def setup(bot):

View file

@ -1,242 +1,111 @@
import aiohttp
import asyncio
import discord
import re
import rethinkdb as r
import traceback
import logging
import utils
from discord.ext import commands
from . import utils
log = logging.getLogger()
BASE_URL = 'https://ptvappapi.picarto.tv'
# This is a public key for use, I don't care if this is seen
api_key = '03e26294-b793-11e5-9a41-005056984bd4'
BASE_URL = 'https://api.picarto.tv/v1'
class Picarto:
def produce_embed(*channels):
description = ""
# Loop through each channel and produce the information that will go in the description
for channel in channels:
url = f"https://picarto.tv/{channel.get('name')}"
description = f"""{description}\n\n**Title:** [{channel.get("title")}]({url})
**Channel:** [{channel.get("name")}]({url})
**Adult:** {"Yes" if channel.get("adult") else "No"}
**Gaming:** {"Yes" if channel.get("gaming") else "No"}
**Commissions:** {"Yes" if channel.get("commissions") else "No"}"""
return discord.Embed(title="Channels that have gone online!", description=description.strip())
class Picarto(commands.Cog):
"""Pretty self-explanatory"""
def __init__(self, bot):
self.bot = bot
self.task = self.bot.loop.create_task(self.picarto_task())
self.channel_info = {}
# noinspection PyAttributeOutsideInit
async def get_online_users(self):
# This method is in place to just return all online users so we can compare against it
url = BASE_URL + '/online/all'
payload = {'key': api_key}
self.online_channels = await utils.request(url, payload=payload)
url = BASE_URL + '/online'
payload = {
'adult': 'true',
'gaming': 'true'
}
channel_info = {}
channels = await utils.request(url, payload=payload)
if channels and isinstance(channels, (list, set, tuple)) and len(channels) > 0:
for channel in channels:
name = channel["name"]
previous = self.channel_info.get(name)
# There are three statuses, on, remained, and off
# On means they were off previously, but are now online
# Remained means they were on previous, and are still on
# Off means they were on preivous, but are now offline
# If they weren't included in the online channels...well they're off
if previous is None:
channel_info[name] = channel
channel_info[name]["status"] = "on"
elif previous["status"] in ["on", "remaining"]:
channel_info[name] = channel
channel_info[name]["status"] = "remaining"
# After loop has finished successfully, we want to override the statuses of the channels
self.channel_info = channel_info
def channel_online(self, channel):
# Channel is the name we are checking against that
# This creates a list of all users that match this channel name (should only ever be 1)
# And returns True as long as it is more than 0
channel = re.search("(?<=picarto.tv/)(.*)", channel).group(1)
matches = [stream for stream in self.online_channels if stream['channel_name'].lower() == channel.lower()]
return len(matches) > 0
async def picarto_task(self):
# The first time we setup this task, if we leave an empty dict as channel_info....it will announce anything
# So what we want to do here, is get everyone who is online now before starting the actual check
# Also wait a little before starting
await self.bot.wait_until_ready()
await self.get_online_users()
await asyncio.sleep(30)
# Now that we've done the initial setup, start the actual loop we'll use
try:
while not self.bot.is_closed():
await self.check_channels()
await asyncio.sleep(30)
except Exception as error:
with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
await asyncio.sleep(30)
async def check_channels(self):
await self.bot.wait_until_ready()
# This is a loop that runs every 30 seconds, checking if anyone has gone online
try:
while not self.bot.is_closed:
await self.get_online_users()
picarto = await utils.filter_content('picarto', {'notifications_on': 1})
for data in picarto:
m_id = data['member_id']
url = data['picarto_url']
# Check if they are online
online = self.channel_online(url)
# If they're currently online, but saved as not then we'll send our notification
if online and data['live'] == 0:
for s_id in data['servers']:
server = self.bot.get_server(s_id)
if server is None:
continue
member = server.get_member(m_id)
if member is None:
continue
server_settings = await utils.get_content('server_settings', s_id)
if server_settings is not None:
channel_id = server_settings.get('notification_channel', s_id)
else:
channel_id = s_id
channel = server.get_channel(channel_id)
await self.bot.send_message(channel, "{} has just gone live! View their stream at <{}>".format(member.display_name, data['picarto_url']))
self.bot.loop.create_task(utils.update_content('picarto', {'live': 1}, m_id))
elif not online and data['live'] == 1:
for s_id in data['servers']:
server = self.bot.get_server(s_id)
if server is None:
continue
member = server.get_member(m_id)
if member is None:
continue
server_settings = await utils.get_content('server_settings', s_id)
if server_settings is not None:
channel_id = server_settings.get('notification_channel', s_id)
else:
channel_id = s_id
channel = server.get_channel(channel_id)
await self.bot.send_message(channel, "{} has just gone offline! View their stream next time at <{}>".format(member.display_name, data['picarto_url']))
self.bot.loop.create_task(utils.update_content('picarto', {'live': 0}, m_id))
await asyncio.sleep(30)
except Exception as e:
tb = traceback.format_exc()
fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e)
log.error(fmt)
@commands.group(invoke_without_command=True, no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def picarto(self, ctx, member: discord.Member = None):
"""This command can be used to view Picarto stats about a certain member
EXAMPLE: !picarto @otherPerson
RESULT: Info about their picarto stream"""
# If member is not given, base information on the author
member = member or ctx.message.author
picarto_entry = await utils.get_content('picarto', member.id)
if picarto_entry is None:
await self.bot.say("That user does not have a picarto url setup!")
return
member_url = picarto_entry['picarto_url']
# Use regex to get the actual username so that we can make a request to the API
stream = re.search("(?<=picarto.tv/)(.*)", member_url).group(1)
url = BASE_URL + '/channel/{}'.format(stream)
payload = {'key': api_key}
data = await utils.request(url, payload=payload)
if data is None:
await self.bot.say("I couldn't connect to Picarto!")
return
# Not everyone has all these settings, so use this as a way to print information if it does, otherwise ignore it
things_to_print = ['channel', 'commissions_enabled', 'is_nsfw', 'program', 'tablet', 'followers',
'content_type']
embed = discord.Embed(title='{}\'s Picarto'.format(data['channel']), url=url)
if data['avatar_url']:
embed.set_thumbnail(url=data['avatar_url'])
for i, result in data.items():
if i in things_to_print and str(result):
i = i.title().replace('_', ' ')
embed.add_field(name=i, value=str(result))
# Social URL's can be given if a user wants them to show
# Print them if they exist, otherwise don't try to include them
for i, result in data['social_urls'].items():
embed.add_field(name=i.title(), value=result)
await self.bot.say(embed=embed)
@picarto.command(name='add', no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def add_picarto_url(self, ctx, url: str):
"""Saves your user's picarto URL
EXAMPLE: !picarto add MyUsername
RESULT: Your picarto stream is saved, and notifications should go to this server"""
# This uses a lookbehind to check if picarto.tv exists in the url given
# If it does, it matches picarto.tv/user and sets the url as that
# Then (in the else) add https://www. to that
# Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0)
# This means that the url was just given as a user (or something complete invalid)
# So set URL as https://www.picarto.tv/[url]
# Even if this was invalid such as https://www.picarto.tv/picarto.tv/user
# For example, our next check handles that
try:
url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0)
except AttributeError:
url = "https://www.picarto.tv/{}".format(url)
else:
url = "https://www.{}".format(url)
channel = re.search("https://www.picarto.tv/(.*)", url).group(1)
api_url = BASE_URL + '/channel/{}'.format(channel)
payload = {'key': api_key}
data = await utils.request(api_url, payload=payload)
if not data:
await self.bot.say("That Picarto user does not exist! What would be the point of adding a nonexistant "
"Picarto user? Silly")
return
key = ctx.message.author.id
entry = {'picarto_url': url,
'servers': [ctx.message.server.id],
'notifications_on': 1,
'live': 0,
'member_id': key}
if await utils.add_content('picarto', entry):
await self.bot.say(
"I have just saved your Picarto URL {}, this server will now be notified when you go live".format(
ctx.message.author.mention))
else:
await utils.update_content('picarto', {'picarto_url': url}, key)
await self.bot.say("I have just updated your Picarto URL")
@picarto.command(name='remove', aliases=['delete'], no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def remove_picarto_url(self, ctx):
"""Removes your picarto URL"""
if await utils.remove_content('picarto', ctx.message.author.id):
await self.bot.say("I am no longer saving your picarto URL {}".format(ctx.message.author.mention))
else:
await self.bot.say(
"I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format(
ctx.message.author.mention, ctx.prefix))
@picarto.group(no_pm=True, invoke_without_command=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def notify(self, ctx):
"""This can be used to turn picarto notifications on or off
Call this command by itself, to add this server to the list of servers to be notified
EXAMPLE: !picarto notify
RESULT: This server will now be notified of you going live"""
key = ctx.message.author.id
result = await utils.get_content('picarto', key)
# Check if this user is saved at all
if result is None:
await self.bot.say(
"I do not have your Picarto URL added {}. You can save your Picarto url with !picarto add".format(
ctx.message.author.mention))
# Then check if this server is already added as one to notify in
elif ctx.message.server.id in result['servers']:
await self.bot.say("I am already set to notify in this server...")
else:
await utils.update_content('picarto', {'servers': r.row['servers'].append(ctx.message.server.id)}, key)
@notify.command(name='on', aliases=['start,yes'], no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def notify_on(self, ctx):
"""Turns picarto notifications on
EXAMPLE: !picarto notify on
RESULT: Notifications are sent when you go live"""
await utils.update_content('picarto', {'notifications_on': 1}, ctx.message.author.id)
await self.bot.say("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format(
ctx.message.author.mention))
@notify.command(name='off', aliases=['stop,no'], pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def notify_off(self, ctx):
"""Turns picarto notifications off
EXAMPLE: !picarto notify off
RESULT: No more notifications sent when you go live"""
await utils.update_content('picarto', {'notifications_on': 0}, ctx.message.author.id)
await self.bot.say(
"I will not notify if you go live anymore {}, "
"are you going to stream some lewd stuff you don't want people to see?~".format(
ctx.message.author.mention))
query = """
SELECT
id, followed_picarto_channels, COALESCE(picarto_alerts, default_alerts) AS channel
FROM
guilds
WHERE
COALESCE(picarto_alerts, default_alerts) IS NOT NULL
"""
# Recheck who is currently online
await self.get_online_users()
# Now get all guilds and their picarto channels they follow and loop through them
results = await self.bot.db.fetch(query) or []
for result in results:
# Get all the channels that have gone online
gone_online = [
self.channel_info.get(name)
for name in result["followed_picarto_channels"]
if self.channel_info.get(name, {}).get("status", "off") == "on"
]
# If they've gone online, produce the embed for them and send it
if gone_online:
embed = produce_embed(*gone_online)
channel = self.bot.get_channel(result["channel"])
if channel is not None:
try:
await channel.send(embed=embed)
except (discord.Forbidden, discord.HTTPException, AttributeError):
pass
def setup(bot):
p = Picarto(bot)
bot.loop.create_task(p.check_channels())
bot.add_cog(Picarto(bot))
bot.add_cog(Picarto(bot))

102
cogs/polls.py Normal file
View file

@ -0,0 +1,102 @@
import discord
from discord.ext import commands
import utils
def to_keycap(c):
return '\N{KEYCAP TEN}' if c == 10 else str(c) + '\u20e3'
class Poll:
def __init__(self, message):
self.message = message
@property
def guild(self):
return self.message.guild
@property
def creator(self):
return self.message.author
@property
def options(self):
return self.message.reactions
async def update_message(self):
self.message = await self.message.channel.fetch_message(self.message.id)
async def remove_other_reaction(self, reaction, member):
"""Ensures that this is the only reaction set for this user"""
for r in self.options:
if r.emoji == reaction.emoji:
continue
users = await r.users().flatten()
if member.id in [x.id for x in users]:
try:
await self.message.remove_reaction(r, member)
except discord.Forbidden:
return
class Polls(commands.Cog):
"""Create custom polls that can be tracked through reactions"""
def __init__(self, bot):
self.bot = bot
self.polls = []
async def create_poll(self, ctx, content):
question, *options = content.split("\n")
if len(options) == 0 or len(options) > 10:
await ctx.send("Please provide between 1-10 options")
else:
fmt = "{} asked: {}\n".format(ctx.message.author.display_name, question)
fmt += "\n".join(
"{}: {}".format(to_keycap(i + 1), opt)
for i, opt in enumerate(options)
)
msg = await ctx.send(fmt)
for i, _ in enumerate(options):
await msg.add_reaction(to_keycap(i + 1))
p = Poll(msg)
await p.update_message()
self.polls.append(p)
def get_poll(self, message):
for p in self.polls:
if p.message.id == message.id:
return p
@commands.Cog.listener()
async def on_reaction_add(self, reaction, user):
if user.id == self.bot.user.id:
return
poll = self.get_poll(reaction.message)
if poll:
try:
await poll.remove_other_reaction(reaction, user)
except discord.Forbidden:
pass
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def poll(self, ctx, *, question):
"""Sets up a poll based on the question that you have provided.
Provide the question on the first line and the options on the following lines
EXAMPLE: !poll This is my poll
Option 1
Option 2
Option 3
RESULT: A new poll people can vote on"""
await self.create_poll(ctx, question)
await ctx.message.delete()
def setup(bot):
bot.add_cog(Polls(bot))

View file

@ -1,239 +1,191 @@
from discord.ext import commands
from . import utils
from collections import defaultdict
import utils
import discord
import random
import pendulum
import re
import asyncio
import traceback
import random
class Raffle:
def __init__(self, bot):
self.bot = bot
self.bot.loop.create_task(self.raffle_task())
class Raffle(commands.Cog):
"""Used to hold custom raffles"""
raffles = defaultdict(list)
async def raffle_task(self):
while True:
try:
await self.check_raffles()
except Exception as error:
with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
await asyncio.sleep(900)
def create_raffle(self, ctx, title, num):
raffle = GuildRaffle(ctx, title, num)
self.raffles[ctx.guild.id].append(raffle)
raffle.start()
async def check_raffles(self):
# This is used to periodically check the current raffles, and see if they have ended yet
# If the raffle has ended, we'll pick a winner from the entrants
raffles = await utils.get_content('raffles')
if raffles is None:
return
for raffle in raffles:
server = self.bot.get_server(raffle['server_id'])
# Check to see if this cog can find the server in question
if server is None:
continue
now = pendulum.utcnow()
expires = pendulum.parse(raffle['expires'])
# Now lets compare and see if this raffle has ended, if not just continue
if expires > now:
continue
title = raffle['title']
entrants = raffle['entrants']
raffle_id = raffle['id']
# Make sure there are actually entrants
if len(entrants) == 0:
fmt = 'Sorry, but there were no entrants for the raffle `{}`!'.format(title)
else:
winner = None
count = 0
while winner is None:
winner = server.get_member(random.SystemRandom().choice(entrants))
# Lets make sure we don't get caught in an infinite loop
# Realistically having more than 25 random entrants found that aren't in the server anymore
# Isn't something that should be an issue
count += 1
if count >= 25:
break
if winner is None:
fmt = 'I couldn\'t find an entrant that is still in this server, for the raffle `{}`!'.format(title)
else:
fmt = 'The raffle `{}` has just ended! The winner is {}!'.format(title, winner.display_name)
# No matter which one of these matches were met, the raffle has ended and we want to remove it
# We don't have to wait for it however, so create a task for it
self.bot.loop.create_task(utils.remove_content('raffles', raffle_id ))
server_settings = await utils.get_content('server_settings', str(server.id))
channel_id = server_settings.get('notification_channel', server.id)
channel = self.bot.get_channel(channel_id)
try:
await channel.send(fmt)
except discord.Forbidden:
pass
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def raffles(self, ctx):
@commands.command(name="raffles")
@commands.guild_only()
@utils.can_run(send_messages=True)
async def _raffles(self, ctx):
"""Used to print the current running raffles on the server
EXAMPLE: !raffles
RESULT: A list of the raffles setup on this server"""
r_filter = {'server_id': ctx.message.server.id}
raffles = await utils.filter_content('raffles', r_filter)
if raffles is None:
await self.bot.say("There are currently no raffles setup on this server!")
raffles = self.raffles[ctx.guild.id]
if len(raffles) == 0:
await ctx.send("There are currently no raffles setup on this server!")
return
fmt = "\n\n".join("**Raffle:** {}\n**Title:** {}\n**Total Entrants:** {}\n**Ends:** {} UTC".format(
num + 1,
raffle['title'],
len(raffle['entrants']),
raffle['expires']) for num, raffle in enumerate(raffles))
await self.bot.say(fmt)
embed = discord.Embed(title=f"Raffles in {ctx.guild.name}")
@commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
@utils.custom_perms(send_messages=True)
async def raffle(self, ctx, raffle_id: int = 0):
for num, raffle in enumerate(raffles):
embed.add_field(
name=f"Raffle {num + 1}",
value=f"Title: {raffle.title}\n"
f"Total Entrants: {len(raffle.entrants)}\n"
f"Ends in {raffle.remaining}",
inline=False
)
await ctx.send(embed=embed)
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def raffle(self, ctx, raffle_id: int):
"""Used to enter a raffle running on this server
If there is more than one raffle running, provide an ID of the raffle you want to enter
EXAMPLE: !raffle 1
RESULT: You've entered the first raffle!"""
# Lets let people use 1 - (length of raffles) and handle 0 base ourselves
raffle_id -= 1
r_filter = {'server_id': ctx.message.server.id}
author = ctx.message.author
raffles = await utils.filter_content('raffles', r_filter)
if raffles is None:
await self.bot.say("There are currently no raffles setup on this server!")
return
raffle_count = len(raffles)
# There is only one raffle, so use the first's info
if raffle_count == 1:
entrants = raffles[0]['entrants']
# Lets make sure that the user hasn't already entered the raffle
if author.id in entrants:
await self.bot.say("You have already entered this raffle!")
return
entrants.append(author.id)
# Since we have no good thing to filter things off of, lets use the internal rethinkdb id
update = {'entrants': entrants}
await utils.update_content('raffles', update, raffles[0]['id'])
await self.bot.say("{} you have just entered the raffle!".format(author.mention))
# Otherwise, make sure the author gave a valid raffle_id
elif raffle_id in range(raffle_count - 1):
entrants = raffles[raffle_id]['entrants']
# Lets make sure that the user hasn't already entered the raffle
if author.id in entrants:
await self.bot.say("You have already entered this raffle!")
return
entrants.append(author.id)
update = {'entrants': entrants}
await utils.update_content('raffles', update, raffles[raffle_id]['id'])
await self.bot.say("{} you have just entered the raffle!".format(author.mention))
try:
raffle = self.raffles[ctx.guild.id][raffle_id - 1]
except IndexError:
await ctx.send(f"I could not find a raffle for ID {raffle_id}")
await self._raffles.invoke(ctx)
else:
fmt = "Please provide a valid raffle ID, as there are more than one setup on the server! " \
"There are currently `{}` raffles running, use {}raffles to view the current running raffles".format(
raffle_count, ctx.prefix)
await self.bot.say(fmt)
if raffle.enter(ctx.author):
await ctx.send(f"You have just joined the raffle {raffle['title']}")
else:
await ctx.send("You have already entered this raffle!")
@raffle.command(pass_context=True, no_pm=True, name='create', aliases=['start', 'begin', 'add'])
@utils.custom_perms(kick_members=True)
@raffle.command(name='create', aliases=['start', 'begin', 'add'])
@commands.guild_only()
@utils.can_run(kick_members=True)
async def raffle_create(self, ctx):
"""This is used in order to create a new server raffle
EXAMPLE: !raffle create
RESULT: A follow-along for setting up a new raffle"""
author = ctx.message.author
server = ctx.message.server
channel = ctx.message.channel
now = pendulum.utcnow()
author = ctx.author
channel = ctx.channel
await self.bot.say(
await ctx.send(
"Ready to start a new raffle! Please respond with the title you would like to use for this raffle!")
msg = await self.bot.wait_for_message(author=author, channel=channel, timeout=120)
if msg is None:
await self.bot.say("You took too long! >:c")
check = lambda m: m.author == author and m.channel == channel
try:
msg = await ctx.bot.wait_for('message', check=check, timeout=120)
except asyncio.TimeoutError:
await ctx.send("You took too long! >:c")
return
title = msg.content
fmt = "Alright, your new raffle will be titled:\n\n{}\n\nHow long would you like this raffle to run for? " \
"The format should be [number] [length] for example, `2 days` or `1 hour` or `30 minutes` etc. " \
"The minimum for this is 10 minutes, and the maximum is 3 months"
await self.bot.say(fmt.format(title))
"The minimum for this is 10 minutes, and the maximum is 3 days"
await ctx.send(fmt.format(title))
# Our check to ensure that a proper length of time was passed
check = lambda m: re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", m.content.lower()) is not None
msg = await self.bot.wait_for_message(author=author, channel=channel, timeout=120, check=check)
if msg is None:
await self.bot.say("You took too long! >:c")
def check(m):
if m.author == author and m.channel == channel:
return re.search("\d+ (minutes?|hours?|days?)", m.content.lower()) is not None
else:
return False
try:
msg = await ctx.bot.wait_for('message', timeout=120, check=check)
except asyncio.TimeoutError:
await ctx.send("You took too long! >:c")
return
# Lets get the length provided, based on the number and type passed
num, term = re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", msg.content.lower()).group(0).split(' ')
num, term = re.search("(\d+) (minutes?|hours?|days?)", msg.content.lower()).groups()
# This should be safe to convert, we already made sure with our check earlier this would match
num = int(num)
# Now lets ensure this meets our min/max
if "minute" in term and (num < 10 or num > 129600):
await self.bot.say(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "hour" in term and num > 2160:
await self.bot.say(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "day" in term and num > 90:
await self.bot.say(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "week" in term and num > 12:
await self.bot.say(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "month" in term and num > 3:
await self.bot.say(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
if "minute" in term:
num = num * 60
elif "hour" in term:
num = num * 60 * 60
elif "day" in term:
num = num * 24 * 60 * 60
if 60 < num < 259200:
await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 days")
return
# Pendulum only accepts the plural version of terms, lets make sure this is added
term = term if term.endswith('s') else '{}s'.format(term)
# If we're in the range, lets just pack this in a dictionary we can pass to set the time we want, then set that
payload = {term: num}
expires = now.add(**payload)
# Now we're ready to add this as a new raffle
entry = {'title': title,
'expires': expires.to_datetime_string(),
'entrants': [],
'author': author.id,
'server_id': server.id}
# We don't want to pass a filter to this, because we can have multiple raffles per server
await utils.add_content('raffles', entry)
await self.bot.say("I have just saved your new raffle!")
self.create_raffle(ctx, title, num)
await ctx.send("I have just saved your new raffle!")
def setup(bot):
bot.add_cog(Raffle(bot))
class GuildRaffle:
def __init__(self, ctx, title, expires):
self._ctx = ctx
self.title = title
self.expires = expires
self.entrants = set()
self.task = None
@property
def guild(self):
return self._ctx.guild
@property
def db(self):
return self._ctx.bot.db
def start(self):
self.task = self._ctx.bot.loop.call_later(self.expires, self.end_raffle())
@property
def remaining(self):
minutes, seconds = divmod(self.task.when(), 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
return f"{days} days, {hours} hours, {minutes} minutes, {seconds} seconds"
def enter(self, entrant):
self.entrants.add(entrant)
async def end_raffle(self):
entrants = {e for e in self.entrants if self.guild.get_member(e.id)}
query = """
SELECT
COALESCE(raffle_alerts, default_alerts) AS channel,
FROM
guilds
WHERE
id = $1
AND
COALESCE(raffle_alerts, default_alerts) IS NOT NULL
"""
channel = None
result = await self.db.fetch(query, self.guild.id)
if result:
channel = self.guild.get_channel(result['channel'])
if channel is None:
return
if entrants:
winner = random.SystemRandom().choice(self.entrants)
await channel.send(f"The winner of the raffle `{self.title}` is {winner.mention}! Congratulations!")
else:
await channel.send(
f"There were no entrants to the raffle `{self.title}`, who are in this server currently!"
)

View file

@ -1,65 +1,146 @@
from discord.ext import commands
import discord
from .utils import checks
import utils
import re
import asyncio
class Roles:
class Roles(commands.Cog):
"""Class to handle management of roles on the server"""
def __init__(self, bot):
self.bot = bot
@commands.command(aliases=['color'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def colour(self, ctx, role_colour: discord.Colour):
"""Used to give yourself a role matching the colour given.
If the role doesn't exist, it will be created. Names such as red, blue, yellow, etc. can be used.
Additionally, hex codes can be used as well
@commands.group(aliases=['roles'], invoke_without_command=True, no_pm=True, pass_context=True)
@checks.custom_perms(send_messages=True)
async def role(self, ctx):
EXAMPLE: !colour red
RESULT: A role that matches red (#e74c3c) will be given to you"""
result = await ctx.bot.db.fetchrow("SELECT colour_roles FROM guilds WHERE id = $1", ctx.guild.id)
if result and not result["colour_roles"]:
await ctx.send("Colour roles not allowed on this server! "
"The command `allowcolours` must be ran to enable them!")
return
if not ctx.me.guild_permissions.manage_roles:
await ctx.send("Error: I need manage_roles to be able to use this command")
return
# The convention we'll use for the name
name = "Bonfire {}".format(role_colour)
# Try to find a role that matches our convention, Name #000000 with the colour matching
role = discord.utils.get(ctx.guild.roles, name=name, colour=role_colour)
# The colour roles they currently have, we need to remove them if they want a new colour
old_roles = [r for r in ctx.author.roles if re.match(r'Bonfire #[0-9a-zA-Z]+', r.name)]
if old_roles:
await ctx.author.remove_roles(*old_roles)
# If the role doesn't exist, we need to create it
if not role:
opts = {
"name": name,
"colour": role_colour
}
try:
role = await ctx.guild.create_role(**opts)
except discord.HTTPException:
await ctx.send("{} is not a valid colour".format(role_colour))
return
# Now add the role
await ctx.author.add_roles(role)
await ctx.send("I have just given you your requested colour!")
@commands.group(aliases=['roles'], invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def role(self, ctx, *, role: discord.Role=None):
"""This command can be used to modify the roles on the server.
Pass no subcommands and this will print the roles currently available on this server
If you give a role as the argument then it will give some information about that role
EXAMPLE: !role
RESULT: A list of all your roles"""
# Simply get a list of all roles in this server and send them
server_roles = [role.name for role in ctx.message.server.roles if not role.is_everyone]
await self.bot.say("Your server's roles are: ```\n{}```".format("\n".join(server_roles)))
@role.command(name='remove', pass_context=True, no_pm=True)
@checks.custom_perms(manage_roles=True)
if role:
# Create the embed object
opts = {
"title": role.name,
"colour": role.colour,
}
if role.managed:
opts["description"] = "This role is managed by a third party service"
embed = discord.Embed(**opts)
# Add details to it
embed.add_field(name="Created", value=role.created_at.date())
embed.add_field(name="Mentionable", value="Yes" if role.mentionable else "No")
total_members = len(role.members)
embed.add_field(name="Total members", value=str(total_members))
# If there are only a few members in this role, display them
if 5 >= total_members > 0:
embed.add_field(name="Members", value="\n".join(m.display_name for m in role.members))
await ctx.send(embed=embed)
else:
# Don't include the colour roles
colour_role = re.compile("Bonfire #.+")
# Simply get a list of all roles in this server and send them
entries = [r.name for r in ctx.guild.roles[1:] if not colour_role.match(r.name)]
if len(entries) == 0:
await ctx.send("You do not have any roles setup on this server, other than the default role!")
return
try:
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
@role.command(name='remove')
@commands.guild_only()
@utils.can_run(manage_roles=True)
async def remove_role(self, ctx):
"""Use this to remove roles from a number of members
EXAMPLE: !role remove @Jim @Bot @Joe
RESULT: A follow-along to remove the role(s) you want to, from these 3 members"""
# No use in running through everything if the bot cannot manage roles
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't manage roles in this server, do you not trust me? :c")
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
await ctx.send("I can't manage roles in this server, do you not trust me? :c")
return
check = lambda m: m.author == ctx.message.author and m.channel == ctx.message.channel
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
# First get the list of all mentioned users
members = ctx.message.mentions
# If no users are mentioned, ask the author for a list of the members they want to remove the role from
if len(members) == 0:
await self.bot.say("Please provide the list of members you want to remove a role from")
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
await ctx.send("Please provide the list of members you want to remove a role from")
try:
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
if len(msg.mentions) == 0:
await self.bot.say("I cannot remove a role from someone if you don't provide someone...")
await ctx.send("I cannot remove a role from someone if you don't provide someone...")
return
# Override members if everything has gone alright, and then continue
members = msg.mentions
# This allows the user to remove multiple roles from the list of users, if they want.
await self.bot.say("Alright, please provide the roles you would like to remove from this member. "
"Make sure the roles, if more than one is provided, are separate by commas. "
"Here is a list of this server's roles:"
"```\n{}```".format("\n".join([r.name for r in server_roles])))
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
await ctx.send("Alright, please provide the roles you would like to remove from this member. "
"Make sure the roles, if more than one is provided, are separate by commas. "
"Here is a list of this server's roles:"
"```\n{}```".format("\n".join([r.name for r in server_roles])))
try:
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
# Split the content based on commas, using regex so we can split if a space was not provided or if it was
@ -73,17 +154,18 @@ class Roles:
# If no valid roles were given, let them know that and return
if len(roles) == 0:
await self.bot.say("Please provide a valid role next time!")
await ctx.send("Please provide a valid role next time!")
return
# Otherwise, remove the roles from each member given
for member in members:
await self.bot.remove_roles(member, *roles)
await self.bot.say("I have just removed the following roles:```\n{}``` from the following members:"
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
await member.remove_roles(*roles)
await ctx.send("I have just removed the following roles:```\n{}``` from the following members:"
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
@role.command(name='add', pass_context=True, no_pm=True)
@checks.custom_perms(manage_roles=True)
@role.command(name='add', aliases=['give', 'assign'])
@commands.guild_only()
@utils.can_run(manage_roles=True)
async def add_role(self, ctx):
"""Use this to add a role to multiple members.
Provide the list of members, and I'll ask for the role
@ -92,32 +174,34 @@ class Roles:
EXAMPLE: !role add @Bob @Joe @jim
RESULT: A follow along to add the roles you want to these 3"""
# No use in running through everything if the bot cannot manage roles
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't manage roles in this server, do you not trust me? :c")
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
await ctx.send("I can't manage roles in this server, do you not trust me? :c")
return
check = lambda m: m.author == ctx.message.author and m.channel == ctx.message.channel
# This is exactly the same as removing roles, except we call add_roles instead.
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
members = ctx.message.mentions
if len(members) == 0:
await self.bot.say("Please provide the list of members you want to add a role to")
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
await ctx.send("Please provide the list of members you want to add a role to")
try:
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
if len(msg.mentions) == 0:
await self.bot.say("I cannot add a role to someone if you don't provide someone...")
await ctx.send("I cannot add a role to someone if you don't provide someone...")
return
members = msg.mentions
await self.bot.say("Alright, please provide the roles you would like to add to this member. "
"Make sure the roles, if more than one is provided, are separate by commas. "
"Here is a list of this server's roles:"
"```\n{}```".format("\n".join([r.name for r in server_roles])))
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
await ctx.send("Alright, please provide the roles you would like to add to this member. "
"Make sure the roles, if more than one is provided, are separate by commas. "
"Here is a list of this server's roles:"
"```\n{}```".format("\n".join([r.name for r in server_roles])))
try:
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
role_names = re.split(', ?', msg.content)
roles = []
@ -127,51 +211,59 @@ class Roles:
roles.append(_role)
if len(roles) == 0:
await self.bot.say("Please provide a valid role next time!")
await ctx.send("Please provide a valid role next time!")
return
for member in members:
await self.bot.add_roles(member, *roles)
await self.bot.say("I have just added the following roles:```\n{}``` to the following members:"
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
await member.add_roles(*roles)
await ctx.send("I have just added the following roles:```\n{}``` to the following members:"
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
@role.command(name='delete', pass_context=True, no_pm=True)
@checks.custom_perms(manage_roles=True)
@role.command(name='delete')
@commands.guild_only()
@utils.can_run(manage_roles=True)
async def delete_role(self, ctx, *, role: discord.Role = None):
"""This command can be used to delete one of the roles from the server
EXAMPLE: !role delete StupidRole
RESULT: No more role called StupidRole"""
# No use in running through everything if the bot cannot manage roles
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't delete roles in this server, do you not trust me? :c")
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
await ctx.send("I can't delete roles in this server, do you not trust me? :c")
return
# If no role was given, get the current roles on the server and ask which ones they'd like to remove
if role is None:
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
await self.bot.say(
await ctx.send(
"Which role would you like to remove from the server? Here is a list of this server's roles:"
"```\n{}```".format("\n".join([r.name for r in server_roles])))
# For this method we're only going to delete one role at a time
# This check attempts to find a role based on the content provided, if it can't find one it returns None
# We can use that fact to simply use just that as our check
check = lambda m: discord.utils.get(server_roles, name=m.content)
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel, check=check)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
def check(m):
if m.author == ctx.message.author and m.channel == ctx.message.channel:
return discord.utils.get(server_roles, name=m.content) is not None
else:
return False
try:
msg = await ctx.bot.wait_for('message', timeout=60, check=check)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
# If we have gotten here, based on our previous check, we know that the content provided is a valid role.
# Due to that, no need for any error checking here
role = discord.utils.get(server_roles, name=msg.content)
await self.bot.delete_role(ctx.message.server, role)
await self.bot.say("I have just removed the role {} from this server".format(role.name))
await role.delete()
await ctx.send("I have just removed the role {} from this server".format(role.name))
@role.command(name='create', pass_context=True, no_pm=True)
@checks.custom_perms(manage_roles=True)
@role.command(name='create')
@commands.guild_only()
@utils.can_run(manage_roles=True)
async def create_role(self, ctx):
"""This command can be used to create a new role for this server
A prompt will follow asking what settings you would like for this new role
@ -180,58 +272,78 @@ class Roles:
EXAMPLE: !role create
RESULT: A follow along in order to create a new role"""
# No use in running through everything if the bot cannot create the role
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
await self.bot.say("I can't create roles in this server, do you not trust me? :c")
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
await ctx.send("I can't create roles in this server, do you not trust me? :c")
return
# Save a couple variables that will be used repeatedly
author = ctx.message.author
server = ctx.message.server
server = ctx.message.guild
channel = ctx.message.channel
# A couple checks that will be used in the wait_for_message's
num_separated_check = lambda m: re.search("(\d(, ?| )?|[nN]one)", m.content) is not None
yes_no_check = lambda m: re.search("(yes|no)", m.content.lower()) is not None
members_check = lambda m: len(m.mentions) > 0
def num_seperated_check(m):
if m.author == author and m.channel == channel:
return re.search("(\d(, ?| )?|[nN]one)", m.content) is not None
else:
return False
def yes_no_check(m):
if m.author == author and m.channel == channel:
return re.search("(yes|no)", m.content.lower()) is not None
else:
return False
def members_check(m):
if m.author == author and m.channel == channel:
return len(m.mentions) > 0
else:
return False
author_check = lambda m: m.author == author and m.channel == channel
# Start the checks for the role, get the name of the role first
await self.bot.say(
await ctx.send(
"Alright! I'm ready to create a new role, please respond with the name of the role you want to create")
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
try:
msg = await ctx.bot.wait_for('message', timeout=60.0, check=author_check)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
name = msg.content
# Print a list of all the permissions available, then ask for which ones need to be active on this new role
all_perms = [p for p in dir(discord.Permissions) if isinstance(getattr(discord.Permissions, p), property)]
fmt = "\n".join("{}) {}".format(i, perm) for i, perm in enumerate(all_perms))
await self.bot.say("Sounds fancy! Here is a list of all the permissions available. Please respond with just "
"the numbers, seperated by commas, of the permissions you want this role to have.\n"
"```\n{}```".format(fmt))
await ctx.send("Sounds fancy! Here is a list of all the permissions available. Please respond with just "
"the numbers, seperated by commas, of the permissions you want this role to have.\n"
"```\n{}```".format(fmt))
# For this we're going to give a couple extra minutes before we timeout
# as it might take a bit to figure out which permissions they want
msg = await self.bot.wait_for_message(timeout=180.0, author=author, channel=channel, check=num_separated_check)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
try:
msg = await ctx.bot.wait_for('message', timeout=180.0, check=num_seperated_check)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
# Check if any integer's were provided that are within the length of the list of permissions
num_permissions = [int(i) for i in re.split(' ?,?', msg.content) if i.isdigit() and int(i) < len(all_perms)]
# Check if this role should be in a separate section on the sidebard, i.e. hoisted
await self.bot.say("Do you want this role to be in a separate section on the sidebar? (yes or no)")
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=yes_no_check)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
await ctx.send("Do you want this role to be in a separate section on the sidebar? (yes or no)")
try:
msg = await ctx.bot.wait_for('message', timeout=60.0, check=yes_no_check)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
hoist = True if msg.content.lower() == "yes" else False
# Check if this role should be able to be mentioned
await self.bot.say("Do you want this role to be mentionable? (yes or no)")
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=yes_no_check)
if msg is None:
await self.bot.say("You took too long. I'm impatient, don't make me wait")
await ctx.send("Do you want this role to be mentionable? (yes or no)")
try:
msg = await ctx.bot.wait_for('message', timeout=60.0, check=yes_no_check)
except asyncio.TimeoutError:
await ctx.send("You took too long. I'm impatient, don't make me wait")
return
mentionable = True if msg.content.lower() == "yes" else False
@ -248,21 +360,96 @@ class Roles:
'mentionable': mentionable
}
# Create the role, and wait a second, sometimes it goes too quickly and we get a role with 'new role' to print
role = await self.bot.create_role(server, **payload)
role = await server.create_role(**payload)
await asyncio.sleep(1)
await self.bot.say("We did it! You just created the new role {}\nIf you want to add this role"
" to some people, mention them now".format(role.name))
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=members_check)
# There's no need to mention the users, so don't send a failure message if they didn't, just return
if msg is None:
await ctx.send("We did it! You just created the new role {}\nIf you want to add this role"
" to some people, mention them now".format(role.name))
try:
msg = await ctx.bot.wait_for('message', timeout=60.0, check=members_check)
except asyncio.TimeoutError:
# There's no need to mention the users, so don't send a failure message if they didn't, just return
return
# Otherwise members were mentioned, add the new role to them now
for member in msg.mentions:
await self.bot.add_roles(member, role)
await member.add_roles(role)
fmt = "\n".join(m.display_name for m in msg.mentions)
await self.bot.say("I have just added the role {} to: ```\n{}```".format(name, fmt))
await ctx.send("I have just added the role {} to: ```\n{}```".format(name, fmt))
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def assign(self, ctx, *role: discord.Role):
"""Assigns the provided role(s) to you, if they can be assigned
EXAMPLE: !assign me Member
RESULT: You now have the Member role"""
if not ctx.message.guild.me.guild_permissions.manage_roles:
await ctx.send("I need to have manage roles permissions to assign roles")
return
author = ctx.message.author
result = await ctx.bot.db.fetchrow("SELECT assignable_roles FROM guilds WHERE id = $1", ctx.guild.id)
if result is None:
await ctx.send("There are no self-assignable roles on this server")
return
self_assignable_roles = result["assignable_roles"]
if len(self_assignable_roles) == 0:
await ctx.send("There are no self-assignable roles on this server")
return
fmt = ""
roles = [r for r in role if r.id in self_assignable_roles]
fmt += "\n".join(["Successfully added {}".format(r.name)
if r.id in self_assignable_roles else
"{} is not available to be self-assigned".format(r.name)
for r in role])
try:
await author.add_roles(*roles)
await ctx.send(fmt)
except discord.HTTPException:
await ctx.send("I cannot assign roles to you {}".format(author.mention))
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def unassign(self, ctx, *role: discord.Role):
"""Unassigns the provided role(s) to you, if they can be assigned
EXAMPLE: !unassign Member
RESULT: You now no longer have the Member role"""
if not ctx.message.guild.me.guild_permissions.manage_roles:
await ctx.send("I need to have manage roles permissions to assign roles")
return
author = ctx.message.author
result = await ctx.bot.db.fetchrow("SELECT assignable_roles FROM guilds WHERE id = $1", ctx.guild.id)
if result is None:
await ctx.send("There are no self-assignable roles on this server")
return
self_assignable_roles = result["assignable_roles"]
if len(self_assignable_roles) == 0:
await ctx.send("There are no self-assignable roles on this server")
return
fmt = ""
roles = [r for r in role if str(r.id) in self_assignable_roles]
fmt += "\n".join(["Successfully removed {}".format(r.name)
if str(r.id) in self_assignable_roles else
"{} is not available to be self-assigned".format(r.name)
for r in role])
try:
await author.remove_roles(*roles)
await ctx.send(fmt)
except discord.HTTPException:
await ctx.send("I cannot remove roles from you {}".format(author.mention))
def setup(bot):

123
cogs/roulette.py Normal file
View file

@ -0,0 +1,123 @@
import discord
import random
import pendulum
import asyncio
from discord.ext import commands
import utils
class Roulette(commands.Cog):
"""A fun game that ends in someone getting kicked!"""
roulettes = []
def get_game(self, server):
for x in self.roulettes:
if x.server == server:
return x
def start_game(self, server, time):
game = self.get_game(server)
if game:
return False
else:
game = Game(server, time)
self.roulettes.append(game)
return game
def end_game(self, server):
game = self.get_game(server)
member = game.choose()
self.roulettes.remove(game)
return member
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def roulette(self, ctx):
"""Joins the current running roulette
EXAMPLE: !roulette
RESULT: You're probably going to get kicked..."""
r = self.get_game(ctx.message.guild)
if not r:
await ctx.send("There is no roulette game running on this server!")
else:
result = r.join(ctx.message.author)
time_left = r.time_left
if result:
await ctx.send("You have joined this roulette game! Good luck~ This roulette will end in " + time_left)
else:
await ctx.send("This roulette will end in " + time_left)
@roulette.command(name='start', aliases=['create'])
@commands.guild_only()
@utils.can_run(kick_members=True)
async def roulette_start(self, ctx, time: int=5):
"""Starts a roulette, that will end in one of the entrants being kicked from the server
By default, the roulette will end in 5 minutes; provide a number (up to 30)
to change how many minutes until it ends
EXAMPLE: !roulette start
RESULT: A new roulette game!"""
if time < 1 or time > 30:
await ctx.send("Invalid time! The roulette must be set to run between 1 and 30 minutes")
return
else:
game = self.start_game(ctx.message.guild, time)
if game:
await ctx.send("A new roulette game has just started! A random entrant will be kicked in {} minutes."
" Type {}roulette to join this roulette...good luck~".format(game.time_left, ctx.prefix))
else:
await ctx.send("There is already a roulette game running on this server!")
return
await asyncio.sleep(time * 60)
member = self.end_game(ctx.message.guild)
if member is None:
await ctx.send("Well no one joined the roulette. That was boring.")
return
try:
fmt = "The unlucky member to be kicked is {}; hopefully someone invites them back :)".format(
member.display_name
)
await member.kick()
except discord.Forbidden:
fmt = "Well, the unlucky member chosen was {} but I can't kick you...so kick yourself please?".format(
member.display_name
)
await ctx.send(fmt)
class Game:
def __init__(self, guild, time):
self.entrants = []
self.server = guild
self.end_time = pendulum.now(tz="UTC").add(minutes=time)
@property
def time_left(self):
return (self.end_time - pendulum.now(tz="UTC")).in_words()
def join(self, member):
"""Adds a member to the list of entrants"""
if member in self.entrants:
return False
else:
self.entrants.append(member)
return True
def choose(self):
try:
return random.SystemRandom().choice(self.entrants)
except IndexError:
return None
def setup(bot):
bot.add_cog(Roulette(bot))

487
cogs/spades.py Normal file
View file

@ -0,0 +1,487 @@
import asyncio
import discord
import utils
from utils import Deck, Suit, Face
from discord.ext import commands
card_map = {
"2": "two",
"3": "three",
"4": "four",
"5": "five",
"6": "six",
"7": "seven",
"8": "eight",
"9": "nine",
"10": "ten"
}
class Player:
def __init__(self, member, game):
self.discord_member = member
self.game = game
self.channel = member.dm_channel
if self.channel is None:
loop = asyncio.get_event_loop()
loop.create_task(self.set_channel())
self.hand_message = None
self.table_message = None
self.hand = Deck(prefill=False, spades_high=True)
self.bid = 0
self.points = 0
self.tricks = 0
self.played_card = None
self._messages_to_clean = []
@property
def bid_num(self):
if self.bid == "moon":
return 13
elif self.bid == "nil":
return 0
return self.bid
async def send_message(self, content=None, embed=None, delete=True):
"""A convenience method to send the message to the player, then add it to the list of messages to delete"""
_msg = await self.discord_member.send(content, embed=embed)
if delete:
self._messages_to_clean.append(_msg)
return _msg
async def set_channel(self):
self.channel = await self.discord_member.create_dm()
async def show_hand(self):
embed = discord.Embed(title="Hand")
diamonds = []
hearts = []
clubs = []
spades = []
for card in sorted(self.hand):
if card.suit == Suit.diamonds:
diamonds.append(str(card.face_short))
if card.suit == Suit.hearts:
hearts.append(str(card.face_short))
if card.suit == Suit.clubs:
clubs.append(str(card.face_short))
if card.suit == Suit.spades:
spades.append(str(card.face_short))
if diamonds:
embed.add_field(name="Diamonds", value=", ".join(diamonds), inline=False)
if hearts:
embed.add_field(name="Hearts", value=", ".join(hearts), inline=False)
if clubs:
embed.add_field(name="Clubs", value=", ".join(clubs), inline=False)
if spades:
embed.add_field(name="Spades", value=", ".join(spades), inline=False)
if self.hand_message:
await self.hand_message.edit(embed=embed)
else:
self.hand_message = await self.discord_member.send(embed=embed)
async def show_table(self):
embed = discord.Embed(title="Table")
if self.game.round.suit:
embed.add_field(name="Round suit", value=self.game.round.suit.name, inline=False)
else:
embed.add_field(name="Round suit", value=self.game.round.suit, inline=False)
winning_card = self.game.round.winning_card
if winning_card:
embed.add_field(name="Winning card", value=str(winning_card), inline=False)
for num, p in enumerate(self.game.players):
fmt = "{} ({}/{} tricks): {}".format(
p.discord_member.display_name,
p.tricks,
p.bid_num,
p.played_card
)
embed.add_field(name="Player {}".format(num + 1), value=fmt, inline=False)
if self.table_message:
await self.table_message.edit(embed=embed)
else:
self.table_message = await self.discord_member.send(embed=embed)
async def get_bid(self):
await self.send_message(
content="It is your turn to bid. Please provide 1-12, nil, or moon depending on the bid you want. "
"Please note you have 3 minutes to bid, any longer and you will be removed from the game.")
self.bid = 0
def check(m):
possible = ['nil', 'moon', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13']
return m.channel == self.channel \
and m.author.id == self.discord_member.id \
and m.content.strip().lower() in possible
msg = await self.game.bot.wait_for('message', check=check)
content = msg.content.strip().lower()
if content == '0':
self.bid = 'nil'
elif content == '13':
self.bid = 'moon'
elif content.isdigit():
self.bid = int(content)
else:
self.bid = content
await self.send_message(content="Thank you for your bid! Please wait while I get the other players' bids...")
return self.bid
async def play(self):
fmt = "It is your turn to play, provide your response in the form '[value] of [face]' such as Ace of Spades. "
fmt += "Your hand can be found above when you bid earlier."
await self.send_message(content=fmt)
await self.game.bot.wait_for('message', check=self.play_check)
await self.send_message(content="You have played...please wait for the other players")
async def clean_messages(self):
for msg in self._messages_to_clean:
await msg.delete()
self._messages_to_clean = []
def play_check(self, message):
if message.channel != self.channel or message.author.id != message.author.id:
return False
if " of " not in message.content:
return False
try:
parts = message.content.partition('of')
face = parts[0].split()[-1].lower()
suit = parts[2].split()[0].lower()
face = card_map.get(face, face)
suit = getattr(Suit, suit)
face = getattr(Face, face)
card = self.hand.get_card(suit, face)
if card is not None and self.game.round.can_play(self, card):
self.played_card = card
self.hand.pluck(card=card)
return True
else:
return False
except (IndexError, AttributeError):
return False
def score(self):
if self.bid == 'nil':
if self.tricks == 0:
self.points += 100
else:
self.points -= 100
elif self.bid == 'moon':
if self.tricks == 13:
self.points += 200
else:
self.points -= 200
else:
if self.tricks >= self.bid:
self.points += self.bid * 10
self.points += self.tricks - self.bid
else:
self.points -= self.bid * 10
self.bid = 0
self.tricks = 0
def has_suit(self, face):
for card in self.hand:
if face == card.suit:
return True
return False
def has_only_spades(self):
for card in self.hand:
if card.suit != Suit.spades:
return False
return True
class Round:
def __init__(self):
self.spades_broken = False
self.cards = Deck(prefill=False, spades_high=True)
self.suit = None
def can_play(self, player, card):
if self.cards.count == 0:
if card.suit != Suit.spades:
return True
else:
return self.spades_broken or player.has_only_spades()
else:
if card.suit == self.suit:
return True
else:
return not player.has_suit(self.suit)
def play(self, card):
# Set the suit
if self.cards.count == 0:
self.suit = card.suit
# This will override the deck, and set it to the round's deck (this is what we want)
card.deck = self.cards
self.cards.insert(card)
if card.suit == Suit.spades:
self.spades_broken = True
@property
def winning_card(self):
cards = sorted([
card
for card in self.cards
if card.suit == self.suit or card.suit == Suit.spades], reverse=True
)
try:
return cards[0]
except IndexError:
return None
def reset(self):
list(self.cards.draw(count=self.cards.count))
self.suit = None
class Game:
def __init__(self, bot):
self.bot = bot
self.players = []
self.deck = Deck(spades_high=True)
self.deck.shuffle()
self.round = Round()
self.started = False
self.card_count = 13
self.score_limit = 250
async def start(self):
self.started = True
special_bids = ['nil', 'moon', 'misdeal']
fmt = "All 4 players have joined, and your game of Spades has started!\n"
fmt += "Rules for this game can be found here: https://www.pagat.com/boston/spades.html. "
fmt += "Special actions/bids allowed are: `{}`\n".format(", ".join(special_bids))
fmt += "Players are:\n" + "\n".join(p.discord_member.display_name for p in self.players)
fmt += "\n\nPlease wait while all players bid...then the first round will begin"
for p in self.players:
await p.discord_member.send(fmt)
await self.game_task()
async def game_task(self):
winner = None
# some while loop, while no one has won yet
while winner is None:
await self.play_round()
winner = self.get_winner()
await self.new_round()
async def play_round(self):
# For clarities sake, I want to send when it starts, immediately
# then follow through with the hand and betting when it's their turn
self.deal()
for p in self.players:
await p.show_hand()
await p.show_table()
await p.get_bid()
self.order_turns(self.get_highest_bidder())
# Bids are complete, time to start the game
await self.clean_messages()
fmt = "Alright, everyone has bid, the bids are:\n{}".format(
"\n".join("{}: {}".format(p.discord_member.display_name, p.bid) for p in self.players))
for p in self.players:
await p.send_message(content=fmt)
# Once bids are done, we can play the actual round
for i in range(self.card_count):
# Wait for each player to play
for p in self.players:
await p.play()
self.round.play(p.played_card)
# Update everyone's table once each person has finished
await self.update_table()
# Get the winner after the round, increase their tricks
winner = self.get_round_winner()
winning_card = winner.played_card
winner.tricks += 1
# Order players based off the winner
self.order_turns(winner)
# Reset the round
await self.reset_round()
fmt = "{} won with a {}".format(winner.discord_member.display_name, winning_card)
for p in self.players:
await p.send_message(content=fmt)
async def update_table(self):
for p in self.players:
await p.show_table()
async def clean_messages(self):
for p in self.players:
await p.clean_messages()
async def reset_round(self):
self.round.reset()
await self.clean_messages()
# First loop through to set everyone's card to None
for p in self.players:
p.played_card = None
# Now we can show the table correctly, since everyone's card is set correctly
for p in self.players:
await p.show_hand()
await p.show_table()
def get_highest_bidder(self):
highest_bid = -1
highest_player = None
for player in self.players:
print(player.bid_num, player.discord_member.display_name)
if player.bid_num > highest_bid:
highest_player = player
print(highest_player.discord_member.display_name)
return highest_player
def order_turns(self, player):
index = self.players.index(player)
self.players = self.players[index:] + self.players[:index]
def get_round_winner(self):
winning_card = self.round.winning_card
for p in self.players:
if winning_card == p.played_card:
return p
def get_winner(self):
for p in self.players:
if p.points >= self.score_limit:
return p
async def new_round(self):
score_msg = discord.Embed(title="Table scores")
for p in self.players:
p.score()
p.played_card = None
p.hand_message = None
p.table_message = None
score_msg.add_field(
name="Player {}".format(p.discord_member.display_name),
value="{}/{}".format(p.points, self.score_limit),
inline=False
)
# We should do this after scoring, so a separate loop is needed here for that
for p in self.players:
await p.send_message(embed=score_msg)
# Round the round's reset information (this is the one run after each round of cards...this won't do everything)
self.round.reset()
# This is the only extra thing needed to fully reset the round itself
self.round.spades_broken = False
# Set the deck back and shuffle it
self.deck.refresh()
self.deck.shuffle()
# This is all we want to do here, this is just preparing for the new round...not actually starting it
def deal(self):
for _ in range(self.card_count):
for p in self.players:
card = list(self.deck.draw())
p.hand.insert(card)
def join(self, member):
p = Player(member, self)
self.players.append(p)
class Spades(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.pending_game = None
self.games = []
def get_game(self, member):
# Simply loop through each game's players, find the one that matches and return it
for g, _ in self.games:
for p in g.players:
if member.id == p.discord_member.id:
return g
if self.pending_game:
for p in self.pending_game.players:
if member.id == p.discord_member.id:
return self.pending_game
def join_game(self, author):
# First check if there's a pending game
if self.pending_game:
# If so add the player to it
self.pending_game.join(author)
# If we've hit 4 players, we want to start the game, add it to our list of games, and wipe our pending game
if len(self.pending_game.players) == 2:
task = self.bot.loop.create_task(self.pending_game.start())
self.games.append((self.pending_game, task))
self.pending_game = None
# If there's no pending game, start a pending game
else:
g = Game(self.bot)
g.join(author)
self.pending_game = g
def cog_unload(self):
# Simply cancel every task
for _, task in self.games:
task.cancel()
@commands.command()
@utils.can_run(send_messages=True)
async def spades(self, ctx):
"""Used to join a spades games. This can be used in servers, or in PM, however it will be handled purely via PM.
There are no teams in this version of spades, and blind nil/moon bids are not allowed. The way this is handled
is for each person joining, there is a pending game ready to start...once 4 people have joined the "lobby" the
game will start.
EXAMPLE: !spades
RESULT: You've joined the spades lobby!"""
author = ctx.message.author
game = self.get_game(author)
if game:
if game.started:
await ctx.send("You are already in a game! Check your PM's if you are confused")
else:
await ctx.send("There are {} players in your lobby".format(len(game.players)))
return
# Before we add the player to the game, we need to ensure we can PM this user
# So lets do this backwards, confirm the user has joined the game, *then* join the game
try:
await author.send("You have joined a spades lobby! Please wait for more people to join, "
"before the game can start")
if ctx.guild:
await ctx.send("Check your PM's {}. I have sent you information about your spades lobby".format(
author.display_name))
self.join_game(author)
except discord.Forbidden:
await ctx.send("This game is ran through PM's only! "
"Please enable your PM's on this server if you want to play!")
def setup(bot):
bot.add_cog(Spades(bot))

88
cogs/spotify.py Normal file
View file

@ -0,0 +1,88 @@
import asyncio
import aiohttp
import traceback
from discord.ext import commands
from base64 import urlsafe_b64encode
import utils
class Spotify(commands.Cog):
"""Pretty self-explanatory"""
def __init__(self, bot):
self.bot = bot
self._token = None
self._client_id = utils.spotify_id or ""
self._client_secret = utils.spotify_secret or ""
self._authorization = "{}:{}".format(self._client_id, self._client_secret)
self.headers = {
"Authorization": "Basic {}".format(
urlsafe_b64encode(self._authorization.encode()).decode()
)
}
self.task = self.bot.loop.create_task(self.api_token_task())
async def api_token_task(self):
while True:
delay = 2400
try:
delay = await self.get_api_token()
except Exception as error:
with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
finally:
await asyncio.sleep(delay)
async def get_api_token(self):
url = "https://accounts.spotify.com/api/token"
opts = {"grant_type": "client_credentials"}
async with aiohttp.ClientSession(headers=self.headers) as session:
response = await session.post(url, data=opts)
data = await response.json()
self._token = data.get("access_token")
return data.get("expires_in")
@commands.group(invoke_without_command=True)
@utils.can_run(send_messages=True)
async def spotify(self, ctx, *, query):
"""Searches Spotify for a song, giving you the link you can use to listen in. Give the query to search for
and it will search by title/artist for the best match
EXAMPLE: !spotify Eminem
RESULT: Some Eminem song"""
# Setup the headers with the token that should be here
headers = {"Authorization": "Bearer {}".format(self._token)}
opts = {"q": query, "type": "track"}
url = "https://api.spotify.com/v1/search"
response = await utils.request(url, headers=headers, payload=opts)
try:
await ctx.send(response.get("tracks").get("items")[0].get("external_urls").get("spotify"))
except (KeyError, AttributeError, IndexError):
await ctx.send("Couldn't find a song for:\n{}".format(query))
@spotify.command()
@utils.can_run(send_messages=True)
async def playlist(self, ctx, *, query):
"""Searches Spotify for a playlist, giving you the link you can use to listen in. Give the query to search for
and it will search for the best match
EXAMPLE: !spotify Eminem
RESULT: Some Eminem song"""
# Setup the headers with the token that should be here
headers = {"Authorization": "Bearer {}".format(self._token)}
opts = {"q": query, "type": "playlist"}
url = "https://api.spotify.com/v1/search"
response = await utils.request(url, headers=headers, payload=opts)
try:
await ctx.send(response.get("playlists").get("items")[0].get("external_urls").get("spotify"))
except (KeyError, AttributeError, IndexError):
await ctx.send("Couldn't find a song for:\n{}".format(query))
def setup(bot):
bot.add_cog(Spotify(bot))

View file

@ -1,45 +1,88 @@
import re
import utils
import discord
import datetime
from discord.ext import commands
from . import utils
import re
class Stats:
class Stats(commands.Cog):
"""Leaderboard/stats related commands"""
def __init__(self, bot):
self.bot = bot
def find_command(self, command):
cmd = None
async def _get_guild_usage(self, guild):
embed = discord.Embed(title="Server Command Usage")
count = await self.bot.db.fetchrow("SELECT COUNT(*), MIN(executed) FROM command_usage WHERE guild=$1", guild.id)
for part in command.split():
try:
if cmd is None:
cmd = self.bot.commands.get(part)
else:
cmd = cmd.commands.get(part)
except AttributeError:
cmd = None
break
embed.description = f"{count[0]} total commands used"
embed.set_footer(text='Tracking command usage since').timestamp = count[1] or datetime.datetime.utcnow()
return cmd
query = """
SELECT
command, COUNT(*) as uses
FROM
command_usage
WHERE
guild = $1
GROUP BY
command
ORDER BY
"uses" DESC
LIMIT 5
"""
@commands.command(no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
results = await self.bot.db.fetch(query, guild.id)
value = "\n".join(f"{command} ({uses} uses)" for command, uses in results or "No Commands")
embed.add_field(name='Top Commands', value=value)
return embed
async def _get_member_usage(self, member):
embed = discord.Embed(title=f"{member.display_name}'s command usage")
count = await self.bot.db.fetchrow(
"SELECT COUNT(*), MIN(executed) FROM command_usage WHERE author=$1",
member.id
)
embed.description = f"{count[0]} total commands used"
embed.set_footer(text='Tracking command usage since').timestamp = count[1] or datetime.datetime.utcnow()
query = """
SELECT
command, COUNT(*) as uses
FROM
command_usage
WHERE
author = $1
GROUP BY
command
ORDER BY
"uses" DESC
LIMIT 5
"""
results = await self.bot.db.fetch(query, member.id)
value = "\n".join(f"{command} ({uses} uses)" for command, uses in results or "No Commands")
embed.add_field(name='Top Commands', value=value)
return embed
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def serverinfo(self, ctx):
"""Provides information about the server
EXAMPLE: !serverinfo
RESULT: Information about your server!"""
server = ctx.message.server
server = ctx.message.guild
# Create our embed that we'll use for the information
embed = discord.Embed(title=server.name, description="Created on: {}".format(server.created_at.date()))
# Make sure we only set the icon url if it has been set
if server.icon_url != "":
if server.icon_url:
embed.set_thumbnail(url=server.icon_url)
# Add our fields, these are self-explanatory
@ -52,237 +95,216 @@ class Stats:
embed.add_field(name='Roles', value=len(server.roles))
# Split channels into voice and text channels
voice_channels = [c for c in server.channels if str(c.type) == 'voice']
text_channels = [c for c in server.channels if str(c.type) == 'text']
voice_channels = [c for c in server.channels if type(c) is discord.VoiceChannel]
text_channels = [c for c in server.channels if type(c) is discord.TextChannel]
embed.add_field(name='Channels', value='{} text, {} voice'.format(len(text_channels), len(voice_channels)))
embed.add_field(name='Owner', value=server.owner.display_name)
# Add the shard ID
embed.set_footer(text="Server is on shard: {}/{}".format(self.bot.shard_id+1, self.bot.shard_count))
await ctx.send(embed=embed)
await self.bot.say(embed=embed)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def userinfo(self, ctx, *, user: discord.Member = None):
"""Provides information about a provided member
@commands.group(no_pm=True)
@utils.custom_perms(send_messages=True)
async def command(self):
EXAMPLE: !userinfo
RESULT: Information about yourself!"""
if user is None:
user = ctx.message.author
embed = discord.Embed(colour=user.colour)
fmt = "{} ({})".format(str(user), user.id)
embed.set_author(name=fmt, icon_url=user.avatar_url)
embed.add_field(name='Joined this server', value=user.joined_at.date(), inline=False)
embed.add_field(name='Joined Discord', value=user.created_at.date(), inline=False)
# Sort them based on the hierarchy, but don't include @everyone
roles = sorted([x for x in user.roles if not x.is_default()], reverse=True)
# I only want the top 5 roles for this purpose
roles = ", ".join("{}".format(x.name) for x in roles[:5])
# If there are no roles, then just say this
roles = roles or "No roles added"
embed.add_field(name='Top 5 roles', value=roles, inline=False)
# Add the activity if there is one
act = user.activity
if isinstance(act, discord.activity.Spotify):
embed.add_field(name="Listening to", value=act.title, inline=False)
elif isinstance(act, discord.activity.Game):
embed.add_field(name='Playing', value=act.name, inline=False)
await ctx.send(embed=embed)
@commands.group()
@utils.can_run(send_messages=True)
async def command(self, ctx):
pass
@command.command(no_pm=True, pass_context=True, name="stats")
@utils.custom_perms(send_messages=True)
async def command_stats(self, ctx, *, command):
"""This command can be used to view some usage stats about a specific command
@command.command(name="stats")
@commands.guild_only()
@utils.can_run(send_messages=True)
async def command_stats(self, ctx, *, member: discord.Member = None):
"""This command can be used to view some usage stats about commands from either a user, or the server. Provide
a member if you want to view their usage, provide no one and the server's usage will be looked up"""
EXAMPLE: !command stats play
RESULT: The realization that this is the only reason people use me ;-;"""
cmd = self.bot.get_command(command)
if cmd is None:
await self.bot.say("`{}` is not a valid command".format(command))
return
command_stats = await utils.get_content('command_usage', cmd.qualified_name)
if command_stats is None:
await self.bot.say("That command has never been used! You know I worked hard on that! :c")
return
total_usage = command_stats['total_usage']
member_usage = command_stats['member_usage'].get(ctx.message.author.id, 0)
server_usage = command_stats['server_usage'].get(ctx.message.server.id, 0)
try:
data = [("Command Name", cmd.qualified_name),
("Total Usage", total_usage),
("Your Usage", member_usage),
("This Server's Usage", server_usage)]
banner = await utils.create_banner(ctx.message.author, "Command Stats", data)
await self.bot.upload(banner)
except (FileNotFoundError, discord.Forbidden):
fmt = "The command {} has been used a total of {} times\n" \
"{} times on this server\n" \
"It has been ran by you, {}, {} times".format(cmd.qualified_name, total_usage, server_usage,
ctx.message.author.display_name, member_usage)
await self.bot.say(fmt)
@command.command(no_pm=True, pass_context=True, name="leaderboard")
@utils.custom_perms(send_messages=True)
async def command_leaderboard(self, ctx, option="server"):
"""This command can be used to print a leaderboard of commands
Provide 'server' to print a leaderboard for this server
Provide 'me' to print a leaderboard for your own usage
EXAMPLE: !command leaderboard me
RESULT: The realization of how little of a life you have"""
if re.search('(author|me)', option):
author = ctx.message.author
# First lets get all the command usage
command_stats = await utils.get_content('command_usage')
# Now use a dictionary comprehension to get just the command name, and usage
# Based on the author's usage of the command
stats = {data['command']: data['member_usage'].get(author.id) for data in command_stats
if data['member_usage'].get(author.id, 0) > 0}
# Now sort it by the amount of times used
sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True)
# Create a string, each command on it's own line, based on the top 5 used commands
# I'm letting it use the length of the sorted_stats[:5]
# As this can include, for example, all 3 if there are only 3 entries
try:
top_5 = [(data[0], data[1]) for data in sorted_stats[:5]]
banner = await utils.create_banner(ctx.message.author, "Your command usage", top_5)
await self.bot.upload(banner)
except (FileNotFoundError, discord.Forbidden):
top_5 = "\n".join("{}: {}".format(data[0], data[1]) for data in sorted_stats[:5])
await self.bot.say(
"Your top {} most used commands are:\n```\n{}```".format(len(sorted_stats[:5]), top_5))
elif re.search('server', option):
# This is exactly the same as above, except server usage instead of member usage
server = ctx.message.server
command_stats = await utils.get_content('command_usage')
stats = {data['command']: data['server_usage'].get(server.id) for data in command_stats
if data['server_usage'].get(server.id, 0) > 0}
sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True)
top_5 = "\n".join("{}: {}".format(data[0], data[1]) for data in sorted_stats[:5])
await self.bot.say(
"This server's top {} most used commands are:\n```\n{}```".format(len(sorted_stats[:5]), top_5))
if member is None:
embed = await self._get_guild_usage(ctx.guild)
else:
await self.bot.say("That is not a valid option, valid options are: `server` or `me`")
embed = await self._get_member_usage(member)
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
await ctx.send(embed=embed)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def mostboops(self, ctx):
"""Shows the person you have 'booped' the most, as well as how many times
EXAMPLE: !mostboops
RESULT: You've booped @OtherPerson 351253897120935712093572193057310298 times!"""
boops = await utils.get_content('boops', ctx.message.author.id)
if boops is None:
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
return
query = """
SELECT
boopee, amount
FROM
boops
WHERE
booper=$1
AND
boopee = ANY($2)
ORDER BY
amount DESC
LIMIT 1
"""
members = [m.id for m in ctx.guild.members]
most = await ctx.bot.db.fetchrow(query, ctx.author.id, members)
# Just to make this easier, just pay attention to the boops data, now that we have the right entry
boops = boops['boops']
if most is None or len(most) == 0:
await ctx.send(f"You have not booped anyone in this server {ctx.author.mention}")
else:
member = ctx.guild.get_member(most['boopee'])
await ctx.send(
f"{ctx.author.mention} you have booped {member.display_name} the most amount of times, "
f"coming in at {most['amount']} times"
)
# First get a list of the ID's of all members in this server, for use in list comprehension
server_member_ids = [member.id for member in ctx.message.server.members]
# Then get a sorted list, based on the amount of times they've booped the member
# Reverse needs to be true, as we want it to go from highest to lowest
sorted_boops = sorted(boops.items(), key=lambda x: x[1], reverse=True)
# Then override the same list, checking if the member they've booped is in this server
sorted_boops = [x for x in sorted_boops if x[0] in server_member_ids]
# Since this is sorted, we just need to get the following information on the first user in the list
most_id, most_boops = sorted_boops[0]
member = discord.utils.find(lambda m: m.id == most_id, self.bot.get_all_members())
await self.bot.say("{0} you have booped {1} the most amount of times, coming in at {2} times".format(
ctx.message.author.mention, member.display_name, most_boops))
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def listboops(self, ctx):
"""Lists all the users you have booped and the amount of times
EXAMPLE: !listboops
RESULT: The list of your booped members!"""
boops = await utils.get_content('boops', ctx.message.author.id)
if boops is None:
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
return
# Just to make this easier, just pay attention to the boops data, now that we have the right entry
boops = boops['boops']
query = """
SELECT
boopee, amount
FROM
boops
WHERE
booper=$1
AND
boopee = ANY($2)
ORDER BY
amount DESC
LIMIT 10
"""
# Same concept as the mostboops method
server_member_ids = [member.id for member in ctx.message.server.members]
booped_members = {m_id: amt for m_id, amt in boops.items() if m_id in server_member_ids}
sorted_booped_members = sorted(booped_members.items(), key=lambda k: k[1], reverse=True)
# Now we only want the first 10 members, so splice this list
sorted_booped_members = sorted_booped_members[:10]
members = [m.id for m in ctx.guild.members]
most = await ctx.bot.db.fetch(query, ctx.author.id, members)
try:
output = [("{0.display_name}".format(ctx.message.server.get_member(m_id)), amt)
for m_id, amt in sorted_booped_members]
banner = await utils.create_banner(ctx.message.author, "Your booped victims", output)
await self.bot.upload(banner)
except (FileNotFoundError, discord.Forbidden):
output = "\n".join(
"{0.display_name}: {1} times".format(ctx.message.server.get_member(m_id), amt) for
m_id, amt in sorted_booped_members)
await self.bot.say("You have booped:```\n{}```".format(output))
if len(most) != 0:
embed = discord.Embed(title="Your booped victims", colour=ctx.author.colour)
embed.set_author(name=str(ctx.author), icon_url=ctx.author.avatar_url)
for row in most:
member = ctx.guild.get_member(row['boopee'])
embed.add_field(name=member.display_name, value=row['amount'])
await ctx.send(embed=embed)
else:
await ctx.send("You haven't booped anyone in this server!")
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def leaderboard(self, ctx):
"""Prints a leaderboard of everyone in the server's battling record
EXAMPLE: !leaderboard
RESULT: A leaderboard of this server's battle records"""
# Create a list of the ID's of all members in this server, for comparison to the records saved
server_member_ids = [member.id for member in ctx.message.server.members]
battles = await utils.get_content('battle_records')
battles = [battle for battle in battles if battle['member_id'] in server_member_ids]
# Sort the members based on their rating
sorted_members = sorted(battles, key=lambda k: k['rating'], reverse=True)
query = """
SELECT
id, battle_rating
FROM
users
WHERE
id = any($1::bigint[])
ORDER BY
battle_rating DESC
"""
output = []
for x in sorted_members:
member_id = x['member_id']
rating = x['rating']
member = ctx.message.server.get_member(member_id)
output.append("{} (Rating: {})".format(member.display_name, rating))
results = await ctx.bot.db.fetch(query, [m.id for m in ctx.guild.members])
try:
pages = utils.Pages(self.bot, message=ctx.message, entries=output)
await pages.paginate()
except utils.CannotPaginate as e:
await self.bot.say(str(e))
if results is None or len(results) == 0:
await ctx.send("No one has battled on this server!")
else:
@commands.command(pass_context=True, no_pm=True)
@utils.custom_perms(send_messages=True)
async def stats(self, ctx, member: discord.Member = None):
output = []
for row in results:
member = ctx.guild.get_member(row['id'])
output.append(f"{member.display_name} (Rating: {row['battle_rating']})")
try:
pages = utils.Pages(ctx, entries=output)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def battlestats(self, ctx, member: discord.Member = None):
"""Prints the battling stats for you, or the user provided
EXAMPLE: !stats @OtherPerson
RESULT: How good they are at winning a completely luck based game"""
member = member or ctx.message.author
# Get the different data that we'll display
query = """
SELECT id, rank, battle_rating, battle_wins, battle_losses
FROM
(SELECT
id,
ROW_NUMBER () OVER (ORDER BY battle_rating DESC) as "rank",
battle_rating,
battle_wins,
battle_losses
FROM
users
WHERE
id = any($1::bigint[]) AND
battle_rating IS NOT NULL
) AS sub
WHERE id = $2
"""
member_list = [m.id for m in ctx.guild.members]
result = await ctx.bot.db.fetchrow(query, member_list, member.id)
if result is None:
return await ctx.send("You have not battled!")
server_rank = result["rank"]
# overall_rank = "{}/{}".format(*ctx.bot.br.get_rank(member))
rating = result["battle_rating"]
record = f"{result['battle_wins']} - {result['battle_losses']}"
# For this one, we don't want to pass a filter, as we do need all battle records
# We need this because we want to make a comparison for overall rank
all_members = await utils.get_content('battle_records')
embed = discord.Embed(title="Battling stats for {}".format(ctx.author.display_name), colour=ctx.author.colour)
embed.set_author(name=str(member), icon_url=member.avatar_url)
embed.add_field(name="Record", value=record, inline=False)
embed.add_field(name="Server Rank", value=server_rank, inline=False)
# embed.add_field(name="Overall Rank", value=overall_rank, inline=False)
embed.add_field(name="Rating", value=rating, inline=False)
# Make a list comprehension to just check if the user has battled
if len([entry for entry in all_members if entry['member_id'] == member.id]) == 0:
await self.bot.say("That user has not battled yet!")
return
# Same concept as the leaderboard
server_member_ids = [member.id for member in ctx.message.server.members]
server_members = [stats for stats in all_members if stats['member_id'] in server_member_ids]
sorted_server_members = sorted(server_members, key=lambda x: x['rating'], reverse=True)
sorted_all_members = sorted(all_members, key=lambda x: x['rating'], reverse=True)
# Enumurate the list so that we can go through, find the user's place in the list
# and get just that for the rank
server_rank = [i for i, x in enumerate(sorted_server_members) if x['member_id'] == member.id][0] + 1
total_rank = [i for i, x in enumerate(sorted_all_members) if x['member_id'] == member.id][0] + 1
# The rest of this is straight forward, just formatting
entry = [m for m in server_members if m['member_id'] == member.id][0]
rating = entry['rating']
record = "{}-{}".format(entry['wins'], entry['losses'])
try:
title = 'Stats for {}'.format(member.display_name)
fmt = [('Record', record), ('Server Rank', '{}/{}'.format(server_rank, len(server_members))),
('Overall Rank', '{}/{}'.format(total_rank, len(all_members))), ('Rating', rating)]
banner = await utils.create_banner(member, title, fmt)
await self.bot.upload(banner)
except (FileNotFoundError, discord.Forbidden):
fmt = 'Stats for {}:\n\tRecord: {}\n\tServer Rank: {}/{}\n\tOverall Rank: {}/{}\n\tRating: {}'
fmt = fmt.format(member.display_name, record, server_rank, len(server_members), total_rank,
len(all_members), rating)
await self.bot.say('```\n{}```'.format(fmt))
await ctx.send(embed=embed)
def setup(bot):

View file

@ -1,158 +0,0 @@
from discord.ext import commands
import discord
from .utils import config
from .utils import checks
import aiohttp
import re
import json
import pendulum
import rethinkdb as r
def setup(bot):
bot.add_cog(Strawpoll(bot))
getter = re.compile(r'`(?!`)(.*?)`')
multi = re.compile(r'```(.*?)```', re.DOTALL)
class Strawpoll:
"""This class is used to create new strawpoll """
def __init__(self, bot):
self.bot = bot
self.url = 'https://strawpoll.me/api/v2/polls'
# In this class we'll only be sending POST requests when creating a poll
# Strawpoll requires the content-type, so just add that to the default headers
self.headers = {'User-Agent': 'Bonfire/1.0.0',
'Content-Type': 'application/json'}
self.session = aiohttp.ClientSession()
@commands.group(aliases=['strawpoll', 'poll', 'polls'], pass_context=True, invoke_without_command=True, no_pm=True)
@checks.custom_perms(send_messages=True)
async def strawpolls(self, ctx, poll_id: str = None):
"""This command can be used to show a strawpoll setup on this server
EXAMPLE: !strawpolls
RESULT: A list of all polls setup on this server"""
# Strawpolls cannot be 'deleted' so to handle whether a poll is running or not on a server
# Just save the poll, which can then be removed when it should not be "running" anymore
polls = await config.get_content('strawpolls', ctx.message.server.id)
# Check if there are any polls setup on this server
try:
polls = polls[0]['polls']
except TypeError:
await self.bot.say("There are currently no strawpolls running on this server!")
return
# Print all polls on this server if poll_id was not provided
if poll_id is None:
fmt = "\n".join(
"{}: https://strawpoll.me/{}".format(data['title'], data['poll_id']) for data in polls)
await self.bot.say("```\n{}```".format(fmt))
else:
# Since strawpoll should never allow us to have more than one poll with the same ID
# It's safe to assume there's only one result
try:
poll = [p for p in polls if p['poll_id'] == poll_id][0]
except IndexError:
await self.bot.say("That poll does not exist on this server!")
return
async with self.session.get("{}/{}".format(self.url, poll_id),
headers={'User-Agent': 'Bonfire/1.0.0'}) as response:
data = await response.json()
# The response for votes and options is provided as two separate lists
# We are enumarting the list of options, to print r (the option)
# And the votes to match it, based on the index of the option
# The rest is simple formatting
fmt_options = "\n\t".join(
"{}: {}".format(result, data['votes'][i]) for i, result in enumerate(data['options']))
author = discord.utils.get(ctx.message.server.members, id=poll['author'])
created_ago = (pendulum.utcnow() - pendulum.parse(poll['date'])).in_words()
link = "https://strawpoll.me/{}".format(poll_id)
fmt = "Link: {}\nTitle: {}\nAuthor: {}\nCreated: {} ago\nOptions:\n\t{}".format(link, data['title'],
author.display_name,
created_ago, fmt_options)
await self.bot.say("```\n{}```".format(fmt))
@strawpolls.command(name='create', aliases=['setup', 'add'], pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
async def create_strawpoll(self, ctx, title, *, options):
"""This command is used to setup a new strawpoll
The format needs to be: poll create "title here" all options here
Options need to be separated by using either one ` around each option
Or use a code block (3 ` around the options), each option on it's own line"""
# The following should use regex to search for the options inside of the two types of code blocks with `
# We're using this instead of other things, to allow most used puncation inside the options
match_single = getter.findall(options)
match_multi = multi.findall(options)
# Since match_single is already going to be a list, we just set
# The options to match_single and remove any blank entries
if match_single:
options = match_single
options = [option for option in options if option]
# Otherwise, options need to be set based on the list, split by lines.
# Then remove blank entries like the last one
elif match_multi:
options = match_multi[0].splitlines()
options = [option for option in options if option]
# If neither is found, then error out and let them know to use the help command, since this one is a bit finicky
else:
await self.bot.say(
"Please provide options for a new strawpoll! Use {}help {} if you do not know the format".format(
ctx.prefix, ctx.command.qualified_name))
return
# Make the post request to strawpoll, creating the poll, and returning the ID
# The ID is all we really need from the returned data, as the rest we already sent/are not going to use ever
payload = {'title': title,
'options': options}
try:
async with self.session.post(self.url, data=json.dumps(payload), headers=self.headers) as response:
data = await response.json()
except json.JSONDecodeError:
await self.bot.say("Sorry, I couldn't connect to strawpoll at the moment. Please try again later")
return
# Save this strawpoll in the list of running strawpolls for a server
poll_id = str(data['id'])
r_filter = {'server_id': ctx.message.server.id}
sub_entry = {'poll_id': poll_id,
'author': ctx.message.author.id,
'date': str(pendulum.utcnow()),
'title': title}
entry = {'server_id': ctx.message.server.id,
'polls': [sub_entry]}
update = {'polls': r.row['polls'].append(sub_entry)}
if not await config.update_content('strawpolls', update, r_filter):
await config.add_content('strawpolls', entry, {'poll_id': poll_id})
await self.bot.say("Link for your new strawpoll: https://strawpoll.me/{}".format(poll_id))
@strawpolls.command(name='delete', aliases=['remove', 'stop'], pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
async def remove_strawpoll(self, ctx, poll_id):
"""This command can be used to delete one of the existing strawpolls
EXAMPLE: !strawpoll remove 5
RESULT: No more strawpoll 5~"""
r_filter = {'server_id': ctx.message.server.id}
content = await config.get_content('strawpolls', r_filter)
try:
content = content[0]['polls']
except TypeError:
await self.bot.say("There are no strawpolls setup on this server!")
return
polls = [poll for poll in content if poll['poll_id'] != poll_id]
update = {'polls': polls}
# Try to remove the poll based on the ID, if it doesn't exist, this will return false
if await config.update_content('strawpolls', update, r_filter):
await self.bot.say("I have just removed the poll with the ID {}".format(poll_id))
else:
await self.bot.say("There is no poll setup with that ID!")

View file

@ -1,98 +1,229 @@
from discord.ext import commands
from .utils import config
from .utils import checks
import re
import discord
import utils
import asyncio
class Tags:
class Tags(commands.Cog):
"""This class contains all the commands for custom tags"""
def __init__(self, bot):
self.bot = bot
@commands.command(pass_context=True, no_pm=True)
@checks.custom_perms(send_messages=True)
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def tags(self, ctx):
"""Prints all the custom tags that this server currently has
EXAMPLE: !tags
RESULT: All tags setup on this server"""
tags = await config.get_content('tags', ctx.message.server.id)
# Simple generator that adds a tag to the list to print, if the tag is for this server
try:
fmt = "\n".join("{}".format(tag['tag']) for tag in tags)
await self.bot.say('```\n{}```'.format(fmt))
except TypeError:
await self.bot.say("There are not tags setup on this server!")
tags = await ctx.bot.db.fetch("SELECT trigger FROM tags WHERE guild=$1", ctx.guild.id)
@commands.group(pass_context=True, invoke_without_command=True, no_pm=True)
@checks.custom_perms(send_messages=True)
if len(tags) > 0:
entries = [t['trigger'] for t in tags]
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
else:
await ctx.send("There are no tags setup on this server!")
@commands.command()
@commands.guild_only()
@utils.can_run(send_messages=True)
async def mytags(self, ctx):
"""Prints all the custom tags that this server that you own
EXAMPLE: !mytags
RESULT: All your tags setup on this server"""
tags = await ctx.bot.db.fetch(
"SELECT trigger FROM tags WHERE guild=$1 AND creator=$2",
ctx.guild.id,
ctx.author.id
)
if len(tags) > 0:
entries = [t['trigger'] for t in tags]
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
else:
await ctx.send("You have no tags on this server!")
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def tag(self, ctx, *, tag: str):
"""This can be used to call custom tags
The format to call a custom tag is !tag <tag>
The format to call a custom tag is !tag <tag>
EXAMPLE: !tag butts
RESULT: Whatever you setup for the butts tag!!"""
r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag)
tags = await config.filter_content('tags', r_filter)
if tags is None:
await self.bot.say('That tag does not exist!')
return
# We shouldn't ever have two tags of the same name, so just get the first result
await self.bot.say("\u200B{}".format(tags[0]['result']))
EXAMPLE: !tag butts
RESULT: Whatever you setup for the butts tag!!"""
tag = await ctx.bot.db.fetchrow(
"SELECT id, result FROM tags WHERE guild=$1 AND trigger=$2",
ctx.guild.id,
tag.lower().strip()
)
@tag.command(name='add', aliases=['create', 'start'], pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
async def add_tag(self, ctx, *, result: str):
if tag:
await ctx.send("\u200B{}".format(tag['result']))
await ctx.bot.db.execute("UPDATE tags SET uses = uses + 1 WHERE id = $1", tag['id'])
else:
await ctx.send("There is no tag called {}".format(tag))
@tag.command(name='add', aliases=['create', 'setup'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def add_tag(self, ctx):
"""Use this to add a new tag that can be used in this server
Format to add a tag is !tag add <tag> - <result>
EXAMPLE: !tag add this is my new tag - This is what it will be
RESULT: A tag that can be called by '!tag this is my new tag' and will output 'This is what it will be'"""
EXAMPLE: !tag add
RESULT: A follow-along in order to create a new tag"""
def check(m):
return m.channel == ctx.message.channel and m.author == ctx.message.author and len(m.content) > 0
my_msg = await ctx.send("Ready to setup a new tag! What do you want the trigger for the tag to be?")
try:
# Use regex to get the matche for everything before and after a -
match = re.search("(.*) - (.*)", result)
tag = match.group(1).strip()
tag_result = match.group(2).strip()
# Next two checks are just to ensure there was a valid match found
except AttributeError:
await self.bot.say(
"Please provide the format for the tag in: {}tag add <tag> - <result>".format(ctx.prefix))
return
# If our regex failed to find the content (aka they provided the wrong format)
if len(tag) == 0 or len(tag_result) == 0:
await self.bot.say(
"Please provide the format for the tag in: {}tag add <tag> - <result>".format(ctx.prefix))
msg = await ctx.bot.wait_for("message", check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long!")
return
# Make sure the tag created does not mention everyone/here
if '@everyone' in tag_result or '@here' in tag_result:
await self.bot.say("You cannot create a tag that mentions everyone!")
trigger = msg.content.lower().strip()
forbidden_tags = ['add', 'create', 'setup', 'edit', 'info', 'delete', 'remove', 'stop']
if len(trigger) > 100:
await ctx.send("Please keep tag triggers under 100 characters")
return
elif trigger.lower() in forbidden_tags:
await ctx.send(
"Sorry, but your tag trigger was detected to be forbidden. "
"Current forbidden tag triggers are: \n{}".format("\n".join(forbidden_tags)))
return
entry = {'server_id': ctx.message.server.id, 'tag': tag, 'result': tag_result}
r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag)
# Try to create new entry first, if that fails (it already exists) then we update it
if not await config.update_content('tags', entry, r_filter):
await config.add_content('tags', entry)
await self.bot.say(
"I have just updated the tag `{0}`! You can call this tag by entering !tag {0}".format(tag))
@tag.command(name='delete', aliases=['remove', 'stop'], pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
async def del_tag(self, ctx, *, tag: str):
tag = await ctx.bot.db.fetchrow(
"SELECT result FROM tags WHERE guild=$1 AND trigger=$2",
ctx.guild.id,
trigger.lower().strip()
)
if tag:
await ctx.send("There is already a tag setup called {}!".format(trigger))
return
try:
await my_msg.delete()
await msg.delete()
except (discord.Forbidden, discord.HTTPException):
pass
my_msg = await ctx.send(
"Alright, your new tag can be called with {}!\n\nWhat do you want to be displayed with this tag?".format(
trigger))
try:
msg = await ctx.bot.wait_for("message", check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long!")
return
result = msg.content
try:
await my_msg.delete()
await msg.delete()
except (discord.Forbidden, discord.HTTPException):
pass
await ctx.send("I have just setup a new tag for this server! You can call your tag with {}".format(trigger))
await ctx.bot.db.execute(
"INSERT INTO tags(guild, creator, trigger, result) VALUES ($1, $2, $3, $4)",
ctx.guild.id,
ctx.author.id,
trigger,
result
)
@tag.command(name='edit')
@commands.guild_only()
@utils.can_run(send_messages=True)
async def edit_tag(self, ctx, *, trigger: str):
"""This will allow you to edit a tag that you have created
EXAMPLE: !tag edit this tag
RESULT: I'll ask what you want the new result to be"""
def check(m):
return m.channel == ctx.message.channel and m.author == ctx.message.author and len(m.content) > 0
tag = await ctx.bot.db.fetchrow(
"SELECT id, trigger FROM tags WHERE guild=$1 AND creator=$2 AND trigger=$3",
ctx.guild.id,
ctx.author.id,
trigger
)
if tag:
my_msg = await ctx.send(f"Alright, what do you want the new result for the tag {tag} to be")
try:
msg = await ctx.bot.wait_for("message", check=check, timeout=60)
except asyncio.TimeoutError:
await ctx.send("You took too long!")
return
new_result = msg.content
try:
await my_msg.delete()
await msg.delete()
except (discord.Forbidden, discord.HTTPException):
pass
await ctx.send(f"Alright, the tag {trigger} has been updated")
await ctx.bot.db.execute("UPDATE tags SET result=$1 WHERE id=$2", new_result, tag['id'])
else:
await ctx.send(f"You do not have a tag called {trigger} on this server!")
@tag.command(name='delete', aliases=['remove', 'stop'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def del_tag(self, ctx, *, trigger: str):
"""Use this to remove a tag from use for this server
Format to delete a tag is !tag delete <tag>
EXAMPLE: !tag delete stupid_tag
RESULT: Deletes that stupid tag"""
await self.bot.say("Temporarily disabled")
# TODO: Fix tags, this will inherently fix this method
"""r_filter = lambda row: (row['server_id'] == ctx.message.guild.id) & (row['tag'] == tag)
if await utils.remove_content('tags', r_filter):
await ctx.send('I have just removed the tag `{}`'.format(tag))
tag = await ctx.bot.db.fetchrow(
"SELECT id FROM tags WHERE guild=$1 AND creator=$2 AND trigger=$3",
ctx.guild.id,
ctx.author.id,
trigger
)
if tag:
await ctx.send(f"I have just deleted the tag {trigger}")
await ctx.bot.db.execute("DELETE FROM tags WHERE id=$1", tag['id'])
else:
await ctx.send(
"The tag {} does not exist! You can't remove something if it doesn't exist...".format(tag))"""
await ctx.send(f"You do not own a tag called {trigger} on this server!")
@tag.command(name="info")
@commands.guild_only()
@utils.can_run(send_messages=True)
async def info_tag(self, ctx, *, trigger: str):
"""Shows some information a bout the tag given"""
tag = await ctx.bot.db.fetchrow(
"SELECT creator, uses, trigger FROM tags WHERE guild=$1 AND trigger=$2",
ctx.guild.id,
trigger
)
if tag is not None:
embed = discord.Embed(title=tag['trigger'])
creator = ctx.guild.get_member(tag['creator'])
if creator:
embed.set_author(name=creator.display_name, url=creator.avatar_url)
embed.add_field(name="Uses", value=tag['uses'])
embed.add_field(name="Owner", value=creator.mention)
await ctx.send(embed=embed)
else:
await ctx.send(f"I cannot find a tag named '{trigger}'")
def setup(bot):

View file

@ -1,9 +1,7 @@
from discord.ext import commands
import discord
from .utils import config
from .utils import checks
from .utils import utilities
import utils
import re
import random
@ -99,10 +97,9 @@ class Board:
return "```\n{}```".format(_board)
class TicTacToe:
def __init__(self, bot):
self.bot = bot
self.boards = {}
class TicTacToe(commands.Cog):
"""Pretty self-explanatory"""
boards = {}
def create(self, server_id, player1, player2):
self.boards[server_id] = Board(player1, player2)
@ -110,8 +107,9 @@ class TicTacToe:
# Return whoever is x's so that we know who is going first
return self.boards[server_id].challengers['x']
@commands.group(pass_context=True, aliases=['tic', 'tac', 'toe'], no_pm=True, invoke_without_command=True)
@checks.custom_perms(send_messages=True)
@commands.group(aliases=['tic', 'tac', 'toe'], invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def tictactoe(self, ctx, *, option: str):
"""Updates the current server's tic-tac-toe board
You obviously need to be one of the players to use this
@ -121,15 +119,15 @@ class TicTacToe:
EXAMPLE: !tictactoe middle top
RESULT: Your piece is placed in the very top space, in the middle"""
player = ctx.message.author
board = self.boards.get(ctx.message.server.id)
board = self.boards.get(ctx.message.guild.id)
# Need to make sure the board exists before allowing someone to play
if not board:
await self.bot.say("There are currently no Tic-Tac-Toe games setup!")
await ctx.send("There are currently no Tic-Tac-Toe games setup!")
return
# Now just make sure the person can play, this will fail if o's are up and x tries to play
# Or if someone else entirely tries to play
if not board.can_play(player):
await self.bot.say("You cannot play right now!")
await ctx.send("You cannot play right now!")
return
# Search for the positions in the option given, the actual match doesn't matter, just need to check if it exists
@ -141,14 +139,14 @@ class TicTacToe:
# Just a bit of logic to ensure nothing that doesn't make sense is given
if top and bottom:
await self.bot.say("That is not a valid location! Use some logic, come on!")
await ctx.send("That is not a valid location! Use some logic, come on!")
return
if left and right:
await self.bot.say("That is not a valid location! Use some logic, come on!")
await ctx.send("That is not a valid location! Use some logic, come on!")
return
# Make sure at least something was given
if not top and not bottom and not left and not right and not middle:
await self.bot.say("Please provide a valid location to play!")
await ctx.send("Please provide a valid location to play!")
return
x = 0
@ -181,7 +179,7 @@ class TicTacToe:
# board.update will handle which letter is placed
# If it returns false however, then someone has already played in that spot and nothing was updated
if not board.update(x, y):
await self.bot.say("Someone has already played there!")
await ctx.send("Someone has already played there!")
return
# Next check if there's a winner
winner = board.check()
@ -189,25 +187,32 @@ class TicTacToe:
# Get the loser based on whether or not the winner is x's
# If the winner is x's, the loser is o's...obviously, and vice-versa
loser = board.challengers['x'] if board.challengers['x'] != winner else board.challengers['o']
await self.bot.say("{} has won this game of TicTacToe, better luck next time {}".format(winner.display_name,
loser.display_name))
await ctx.send("{} has won this game of TicTacToe, better luck next time {}".format(winner.display_name,
loser.display_name))
# Handle updating ratings based on the winner and loser
await utilities.update_records('tictactoe', winner, loser)
await utils.update_records('tictactoe', ctx.bot.db, winner, loser)
# This game has ended, delete it so another one can be made
del self.boards[ctx.message.server.id]
try:
del self.boards[ctx.message.guild.id]
except KeyError:
pass
else:
# If no one has won, make sure the game is not full. If it has, delete the board and say it was a tie
if board.full():
await self.bot.say("This game has ended in a tie!")
del self.boards[ctx.message.server.id]
await ctx.send("This game has ended in a tie!")
try:
del self.boards[ctx.message.guild.id]
except KeyError:
pass
# If no one has won, and the game has not ended in a tie, print the new updated board
else:
player_turn = board.challengers.get('x') if board.X_turn else board.challengers.get('o')
fmt = str(board) + "\n{} It is now your turn to play!".format(player_turn.display_name)
await self.bot.say(fmt)
await ctx.send(fmt)
@tictactoe.command(name='start', aliases=['challenge', 'create'], pass_context=True, no_pm=True)
@checks.custom_perms(send_messages=True)
@tictactoe.command(name='start', aliases=['challenge', 'create'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def start_game(self, ctx, player2: discord.Member):
"""Starts a game of tictactoe with another player
@ -216,32 +221,33 @@ class TicTacToe:
player1 = ctx.message.author
# For simplicities sake, only allow one game on a server at a time.
# Things can easily get confusing (on the server's end) if we allow more than one
if self.boards.get(ctx.message.server.id) is not None:
await self.bot.say("Sorry but only one Tic-Tac-Toe game can be running per server!")
if self.boards.get(ctx.message.guild.id) is not None:
await ctx.send("Sorry but only one Tic-Tac-Toe game can be running per server!")
return
# Make sure we're not being challenged, I always win anyway
if player2 == ctx.message.server.me:
await self.bot.say("You want to play? Alright lets play.\n\nI win, so quick you didn't even notice it.")
if player2 == ctx.message.guild.me:
await ctx.send("You want to play? Alright lets play.\n\nI win, so quick you didn't even notice it.")
return
if player2 == player1:
await self.bot.say("You can't play yourself, I won't allow it. Go find some friends")
await ctx.send("You can't play yourself, I won't allow it. Go find some friends")
return
# Create the board and return who has been decided to go first
x_player = self.create(ctx.message.server.id, player1, player2)
fmt = "A tictactoe game has just started between {} and {}".format(player1.display_name, player2.display_name)
x_player = self.create(ctx.message.guild.id, player1, player2)
fmt = "A tictactoe game has just started between {} and {}\n".format(player1.display_name, player2.display_name)
# Print the board too just because
fmt += str(self.boards[ctx.message.server.id])
fmt += str(self.boards[ctx.message.guild.id])
# We don't need to do anything weird with assigning x_player to something
# it is already a member object, just use it
fmt += "I have decided at random, and {} is going to be x's this game. It is your turn first! " \
"Use the {}tictactoe command, and a position, to choose where you want to play"\
"Use the {}tictactoe command, and a position, to choose where you want to play" \
.format(x_player.display_name, ctx.prefix)
await self.bot.say(fmt)
await ctx.send(fmt)
@tictactoe.command(name='delete', aliases=['stop', 'remove', 'end'], pass_context=True, no_pm=True)
@checks.custom_perms(kick_members=True)
@tictactoe.command(name='delete', aliases=['stop', 'remove', 'end'])
@commands.guild_only()
@utils.can_run(kick_members=True)
async def stop_game(self, ctx):
"""Force stops a game of tictactoe
This should realistically only be used in a situation like one player leaves
@ -249,12 +255,12 @@ class TicTacToe:
EXAMPLE: !tictactoe stop
RESULT: No more tictactoe!"""
if self.boards.get(ctx.message.server.id) is None:
await self.bot.say("There are no tictactoe games running on this server!")
if self.boards.get(ctx.message.guild.id) is None:
await ctx.send("There are no tictactoe games running on this server!")
return
del self.boards[ctx.message.server.id]
await self.bot.say("I have just stopped the game of TicTacToe, a new should be able to be started now!")
del self.boards[ctx.message.guild.id]
await ctx.send("I have just stopped the game of TicTacToe, a new should be able to be started now!")
def setup(bot):

124
cogs/tutorial.py Normal file
View file

@ -0,0 +1,124 @@
from discord.ext import commands
import utils
import discord
class Tutorial(commands.Cog):
@commands.command()
# @utils.can_run(send_messages=True)
async def tutorial(self, ctx, *, cmd_or_cog = None):
# The message we'll use to send
output = ""
# The list of commands we need to run through
commands = []
if cmd_or_cog:
cmd = ctx.bot.get_command(cmd_or_cog.lower())
# This should be a cog
if cmd is None:
cog = ctx.bot.get_cog(cmd_or_cog.title())
if cog is None:
await ctx.send("Could not find a command or a cog for {}".format(cmd_or_cog))
return
commands = set([
c
for c in ctx.bot.walk_commands()
if c.cog_name == cmd_or_cog.title()
])
# Specific command
else:
commands = [cmd]
# Use all commands
else:
commands = set(ctx.bot.walk_commands())
# Loop through all the commands that we want to use
for command in commands:
embed = self.generate_embed(command)
# await ctx.author.send(embed=embed)
await ctx.send(embed=embed)
return
def generate_embed(self, command):
# Create the embed object
opts = {
"title": "`{}` command tutorial:\n\n".format(command.qualified_name),
"colour": discord.Colour.green()
}
embed = discord.Embed(**opts)
if command.help is not None:
# Split into examples, results, and the description itself based on the string
description, _, rest = command.help.partition('EXAMPLE:')
example, _, rest = rest.partition('RESULT:')
result, _, gif = rest.partition("GIF:")
else:
example = None
result = None
gif = None
# Add a field for the aliases
if command.aliases:
embed.add_field(
name="Aliases",
value="\n".join(["\t{}".format(alias) for alias in command.aliases]),
inline=False
)
# Add any paramaters needed
if command.clean_params:
params = []
for key, value in command.clean_params.items():
# Get the parameter type, as well as the default value if it exists
param_type, has_default, default_value = str(value).partition("=")
try:
# We want everything after the :
param_type = param_type.split(":")[1]
# Now we want to split based on . (for possible deep level types IE discord.member.Member) then get the last value
param_type = param_type.split(".")
param_type = param_type[len(param_type) - 1]
# This could mean something like *param was provided as the parameter
except IndexError:
param_type = "str"
# Start the string that we'll use as the param's info
string = "{} (Type: {}".format(key, param_type)
if default_value:
string += ", Default: {}".format(default_value)
# This is the = from the partition, if it exists, then there's a default...hence the name
if has_default:
string += ", optional)"
else:
string += ", required)"
# Now push our string to the list of params
params.append(string)
name = "Paramaters"
embed.add_field(name=name, value="\n".join(params), inline=False)
# Set the description of the embed to the description
if description:
embed.description = description
# Add these two in one embed
if example and result:
embed.add_field(
name="Example",
value="{}\n{}".format(example.strip(), result.strip()),
inline=False
)
try:
can_run = [func for func in command.checks if "can_run" in func.__qualname__][0]
perms = ",".join(attribute for attribute, setting in can_run.perms.items() if setting)
embed.set_footer(text="Permissions required: {}".format(perms))
except IndexError:
pass
return embed
def setup(bot):
bot.add_cog(Tutorial(bot))

View file

@ -1,240 +0,0 @@
from discord.ext import commands
from . import utils
import aiohttp
import asyncio
import discord
import re
import rethinkdb as r
import traceback
import logging
log = logging.getLogger()
class Twitch:
"""Class for some twitch integration
You can add or remove your twitch stream for your user
I will then notify the server when you have gone live or offline"""
def __init__(self, bot):
self.bot = bot
self.key = utils.twitch_key
self.params = {'client_id': self.key}
async def channel_online(self, twitch_url: str):
# Check a specific channel's data, and get the response in text format
channel = re.search("(?<=twitch.tv/)(.*)", twitch_url).group(1)
url = "https://api.twitch.tv/kraken/streams/{}".format(channel)
response = await utils.request(url, payload=self.params)
# For some reason Twitch's API call is not reliable, sometimes it returns stream as None
# That is what we're checking specifically, sometimes it doesn't exist in the returned JSON at all
# Sometimes it returns something that cannot be decoded with JSON (which means we'll get None back)
# In either error case, just assume they're offline, the next check will most likely work
try:
return response['stream'] is not None
except (KeyError, TypeError):
return False
async def check_channels(self):
await self.bot.wait_until_ready()
# Loop through as long as the bot is connected
try:
while not self.bot.is_closed:
twitch = await utils.filter_content('twitch', {'notifications_on': 1})
for data in twitch:
m_id = data['member_id']
url = data['twitch_url']
# Check if they are online
online = await self.channel_online(url)
# If they're currently online, but saved as not then we'll send our notification
if online and data['live'] == 0:
for s_id in data['servers']:
server = self.bot.get_server(s_id)
if server is None:
continue
member = server.get_member(m_id)
if member is None:
continue
server_settings = await utils.get_content('server_settings', s_id)
if server_settings is not None:
channel_id = server_settings.get('notification_channel', s_id)
else:
channel_id = s_id
channel = server.get_channel(channel_id)
await self.bot.send_message(channel, "{} has just gone live! View their stream at <{}>".format(member.display_name, data['twitch_url']))
self.bot.loop.create_task(utils.update_content('twitch', {'live': 1}, m_id))
elif not online and data['live'] == 1:
for s_id in data['servers']:
server = self.bot.get_server(s_id)
if server is None:
continue
member = server.get_member(m_id)
if member is None:
continue
server_settings = await utils.get_content('server_settings', s_id)
if server_settings is not None:
channel_id = server_settings.get('notification_channel', s_id)
else:
channel_id = s_id
channel = server.get_channel(channel_id)
await self.bot.send_message(channel, "{} has just gone offline! View their stream next time at <{}>".format(member.display_name, data['twitch_url']))
self.bot.loop.create_task(utils.update_content('twitch', {'live': 0}, m_id))
await asyncio.sleep(30)
except Exception as e:
tb = traceback.format_exc()
fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e)
log.error(fmt)
@commands.group(no_pm=True, invoke_without_command=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def twitch(self, ctx, *, member: discord.Member = None):
"""Use this command to check the twitch info of a user
EXAMPLE: !twitch @OtherPerson
RESULT: Information about their twitch URL"""
await ctx.message.channel.trigger_typing()
if member is None:
member = ctx.message.author
result = await utils.get_content('twitch', member.id)
if result is None:
await self.bot.say("{} has not saved their twitch URL yet!".format(member.name))
return
url = result['twitch_url']
user = re.search("(?<=twitch.tv/)(.*)", url).group(1)
twitch_url = "https://api.twitch.tv/kraken/channels/{}".format(user)
payload = {'client_id': self.key}
data = await utils.request(twitch_url, payload=payload)
embed = discord.Embed(title=data['display_name'], url=url)
if data['logo']:
embed.set_thumbnail(url=data['logo'])
embed.add_field(name='Title', value=data['status'])
embed.add_field(name='Followers', value=data['followers'])
embed.add_field(name='Views', value=data['views'])
if data['game']:
embed.add_field(name='Game', value=data['game'])
embed.add_field(name='Language', value=data['broadcaster_language'])
await self.bot.say(embed=embed)
@twitch.command(name='add', no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def add_twitch_url(self, ctx, url: str):
"""Saves your user's twitch URL
EXAMPLE: !twitch add MyTwitchName
RESULT: Saves your twitch URL; notifications will be sent to this server when you go live"""
await ctx.message.channel.trigger_typing()
# This uses a lookbehind to check if twitch.tv exists in the url given
# If it does, it matches twitch.tv/user and sets the url as that
# Then (in the else) add https://www. to that
# Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0)
# This means that the url was just given as a user (or something complete invalid)
# So set URL as https://www.twitch.tv/[url]
# Even if this was invalid such as https://www.twitch.tv/google.com/
# For example, our next check handles that
try:
url = re.search("((?<=://)?twitch.tv/)+(.*)", url).group(0)
except AttributeError:
url = "https://www.twitch.tv/{}".format(url)
else:
url = "https://www.{}".format(url)
# Try to find the channel provided, we'll get a 404 response if it does not exist
status = await utils.request(url, attr='status')
if not status == 200:
await self.bot.say("That twitch user does not exist! "
"What would be the point of adding a nonexistant twitch user? Silly")
return
key = ctx.message.author.id
entry = {'twitch_url': url,
'servers': [ctx.message.server.id],
'notifications_on': 1,
'live': 0,
'member_id': key}
update = {'twitch_url': url}
# Check to see if this user has already saved a twitch URL
# If they have, update the URL, otherwise create a new entry
# Assuming they're not live, and notifications should be on
if not await utils.add_content('twitch', entry):
await utils.update_content('twitch', update, key)
await self.bot.say("I have just saved your twitch url {}".format(ctx.message.author.mention))
@twitch.command(name='remove', aliases=['delete'], no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def remove_twitch_url(self, ctx):
"""Removes your twitch URL
EXAMPLE: !twitch remove
RESULT: I stop saving your twitch URL"""
# Just try to remove it, if it doesn't exist, nothing is going to happen
await utils.remove_content('twitch', ctx.message.author.id)
await self.bot.say("I am no longer saving your twitch URL {}".format(ctx.message.author.mention))
@twitch.group(no_pm=True, invoke_without_command=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def notify(self, ctx):
"""This can be used to modify notification settings for your twitch user
Call this command by itself to add 'this' server as one that will be notified when you on/offline
EXAMPLE: !twitch notify
RESULT: This server will now be notified when you go live"""
key = ctx.message.author.id
result = await utils.get_content('twitch', key)
# Check if this user is saved at all
if result is None:
await self.bot.say(
"I do not have your twitch URL added {}. You can save your twitch url with !twitch add".format(
ctx.message.author.mention))
# Then check if this server is already added as one to notify in
elif ctx.message.server.id in result['servers']:
await self.bot.say("I am already set to notify in this server...")
else:
await utils.update_content('twitch', {'servers': r.row['servers'].append(ctx.message.server.id)}, key)
await self.bot.say("This server will now be notified if you go live")
@notify.command(name='on', aliases=['start,yes'], no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def notify_on(self, ctx):
"""Turns twitch notifications on
EXAMPLE: !twitch notify on
RESULT: Notifications will be sent when you go live"""
if await utils.update_content('twitch', {"notifications_on": 1}, ctx.message.author.id):
await self.bot.say("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format(
ctx.message.author.mention))
else:
await self.bot.say("I can't notify if you go live if I don't know your twitch URL yet!")
@notify.command(name='off', aliases=['stop,no'], no_pm=True, pass_context=True)
@utils.custom_perms(send_messages=True)
async def notify_off(self, ctx):
"""Turns twitch notifications off
EXAMPLE: !twitch notify off
RESULT: Notifications will not be sent when you go live"""
if await utils.update_content('twitch', {"notifications_on": 0}, ctx.message.author.id):
await self.bot.say(
"I will not notify if you go live anymore {}, "
"are you going to stream some lewd stuff you don't want people to see?~".format(
ctx.message.author.mention))
else:
await self.bot.say(
"I mean, I'm already not going to notify anyone, because I don't have your twitch URL saved...")
def setup(bot):
t = Twitch(bot)
bot.loop.create_task(t.check_channels())
bot.add_cog(Twitch(bot))

View file

@ -1,6 +0,0 @@
from .cards import Deck
from .checks import is_owner, custom_perms, db_check
from .config import *
from .utilities import *
from .images import create_banner
from .paginator import Pages, CannotPaginate

View file

@ -1,43 +0,0 @@
import itertools
import random
suits = ['S', 'C', 'H', 'D']
faces = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
class Deck:
def __init__(self, prefill=True):
# itertools.product creates us a tuple based on every output of our faces and suits
# This is EXACTLY what a deck of normal playing cards is, so it's perfect
if prefill:
self.deck = list(itertools.product(suits, faces))
else:
self.deck = []
def __iter__(self):
for card in self.deck:
yield card
@property
def count(self):
"""A property to provide how many cards are currently in the deck"""
return len(self.deck)
@property
def empty(self):
"""A property to determine whether or not the deck has cards in it"""
return len(self.deck) == 0
def draw(self, count=1):
"""Generator to draw from the deck"""
try:
for i in range(count):
yield self.deck.pop()
except IndexError:
yield None
def insert(self, cards):
"""Adds the provided cards to the end of the deck"""
self.deck.extend(cards)
def shuffle(self):
"""Shuffles the deck in place"""
random.SystemRandom().shuffle(self.deck)

View file

@ -1,94 +0,0 @@
import asyncio
import rethinkdb as r
from discord.ext import commands
import discord
from . import config
loop = asyncio.get_event_loop()
# The tables needed for the database, as well as their primary keys
required_tables = {
'battle_records': 'member_id',
'boops': 'member_id',
'command_usage': 'command',
'motd': 'date',
'overwatch': 'member_id',
'picarto': 'member_id',
'server_settings': 'server_id',
'raffles': 'id',
'strawpolls': 'server_id',
'osu': 'member_id',
'tags': 'server_id',
'tictactoe': 'member_id',
'twitch': 'member_id'
}
async def db_check():
"""Used to check if the required database/tables are setup"""
db_opts = config.db_opts
r.set_loop_type('asyncio')
# First try to connect, and see if the correct information was provided
try:
conn = await r.connect(**db_opts)
except r.errors.ReqlDriverError:
print("Cannot connect to the RethinkDB instance with the following information: {}".format(db_opts))
print("The RethinkDB instance you have setup may be down, otherwise please ensure you setup a"
" RethinkDB instance, and you have provided the correct database information in config.yml")
quit()
return
# Get the current databases and check if the one we need is there
dbs = await r.db_list().run(conn)
if db_opts['db'] not in dbs:
# If not, we want to create it
print('Couldn\'t find database {}...creating now'.format(db_opts['db']))
await r.db_create(db_opts['db']).run(conn)
# Then add all the tables
for table, key in required_tables.items():
print("Creating table {}...".format(table))
await r.table_create(table, primary_key=key).run(conn)
print("Done!")
else:
# Otherwise, if the database is setup, make sure all the required tables are there
tables = await r.table_list().run(conn)
for table, key in required_tables.items():
if table not in tables:
print("Creating table {}...".format(table))
await r.table_create(table, primary_key=key).run(conn)
print("Done checking tables!")
def is_owner(ctx):
return ctx.message.author.id in config.owner_ids
def custom_perms(**perms):
def predicate(ctx):
# Return true if this is a private channel, we'll handle that in the registering of the command
if ctx.message.channel.is_private:
return True
# Get the member permissions so that we can compare
member_perms = ctx.message.author.permissions_in(ctx.message.channel)
# Next, set the default permissions if one is not used, based on what was passed
# This will be overriden later, if we have custom permissions
required_perm = discord.Permissions.none()
for perm, setting in perms.items():
setattr(required_perm, perm, setting)
try:
server_settings = config.cache.get('server_settings').values
required_perm_value = [x for x in server_settings if x['server_id'] == ctx.message.server.id][0]['permissions'][ctx.command.qualified_name]
required_perm = discord.Permissions(required_perm_value)
except (TypeError, IndexError, KeyError):
pass
# Now just check if the person running the command has these permissions
return member_perms >= required_perm
predicate.perms = perms
return commands.check(predicate)

View file

@ -1,243 +0,0 @@
import ruamel.yaml as yaml
import asyncio
import rethinkdb as r
import pendulum
loop = asyncio.get_event_loop()
global_config = {}
# Ensure that the required config.yml file actually exists
try:
with open("config.yml", "r") as f:
global_config = yaml.safe_load(f)
except FileNotFoundError:
print("You have no config file setup! Please use config.yml.sample to setup a valid config file")
quit()
try:
bot_token = global_config["bot_token"]
except KeyError:
print("You have no bot_token saved, this is a requirement for running a bot.")
print("Please use config.yml.sample to setup a valid config file")
quit()
try:
owner_ids = global_config["owner_id"]
except KeyError:
print("You have no owner_id saved! You're not going to be able to run certain commands without this.")
print("Please use config.yml.sample to setup a valid config file")
quit()
# This is a simple class for the cache concept, all it holds is it's own key and the values
# With a method that gets content based on it's key
class Cache:
def __init__(self, key):
self.key = key
self.values = {}
self.refreshed = pendulum.utcnow()
loop.create_task(self.update())
async def update(self):
self.values = await get_content(self.key)
self.refreshed = pendulum.utcnow()
# Default bot's description
bot_description = global_config.get("description")
# Bot's default prefix for commands
default_prefix = global_config.get("command_prefix", "!")
# The sharding information
shard_count = global_config.get("shard_count", 1)
shard_id = global_config.get("shard_id", 1)
# The key for bots.discord.pw and carbonitex
discord_bots_key = global_config.get('discord_bots_key', "")
carbon_key = global_config.get('carbon_key', "")
# The client ID for twitch requsets
twitch_key = global_config.get('twitch_key', "")
# The steam API key
steam_key = global_config.get("steam_key", "")
# The key for youtube API calls
youtube_key = global_config.get("youtube_key", "")
# The key for Osu API calls
osu_key = global_config.get('osu_key', '')
# The key for League of Legends API calls
lol_key = global_config.get('lol_key', '')
# The keys needed for deviant art calls
da_id = global_config.get("da_id", "")
da_secret = global_config.get("da_secret", "")
# The invite link for the server made for the bot
dev_server = global_config.get("dev_server", "")
# The User-Agent that we'll use for most requests
user_agent = global_config.get('user_agent', "")
# The extensions to load
extensions = global_config.get('extensions', [])
# The default status the bot will use
default_status = global_config.get("default_status", None)
# The URL that will be used to link to for the help command
help_url = global_config.get("help_url", "")
# The rethinkdb hostname
db_host = global_config.get('db_host', 'localhost')
# The rethinkdb database name
db_name = global_config.get('db_name', 'Discord_Bot')
# The rethinkdb certification
db_cert = global_config.get('db_cert', '')
# The rethinkdb port
db_port = global_config.get('db_port', 28015)
# The user and password assigned
db_user = global_config.get('db_user', 'admin')
db_pass = global_config.get('db_pass', '')
# We've set all the options we need to be able to connect
# so create a dictionary that we can use to unload to connect
# db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'ssl':
# {'ca_certs': db_cert}, 'user': db_user, 'password': db_pass}
db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'user': db_user, 'password': db_pass}
# This will be a dictionary that holds the cache object, based on the key that is saved
cache = {}
# Populate cache with each object
# With the new saving method, we're not going to be able to cache the way that I was before
# This is on standby until I rethink how to do this, because I do still want to cache data
"""for k in possible_keys:
ca che[k] = Cache(k)"""
# We still need 'cache' for prefixes and custom permissions however, so for now, just include that
cache['server_settings'] = Cache('server_settings')
async def update_cache():
for value in cache.values():
await value.update()
def command_prefix(bot, message):
# We do not want to make a query for every message that is sent
# So assume it's in cache, or it doesn't exist
# If the prefix does exist in the database and isn't in our cache; too bad, something has messed up
# But it is not worth a query for every single message the bot detects, to fix
try:
prefixes = cache['server_settings'].values
prefix = [x for x in prefixes if x['server_id'] == message.server.id][0]['prefix']
return prefix or default_prefix
except (KeyError, TypeError, IndexError, AttributeError):
return default_prefix
async def add_content(table, content):
r.set_loop_type("asyncio")
conn = await r.connect(**db_opts)
# First we need to make sure that this entry doesn't exist
# For all rethinkDB cares, multiple entries can exist with the same content
# For our purposes however, we do not want this
try:
result = await r.table(table).insert(content).run(conn)
except r.ReqlOpFailedError:
# This means the table does not exist
await r.table_create(table).run(conn)
await r.table(table).insert(content).run(conn)
result = {}
await conn.close()
if table == 'server_settings':
loop.create_task(cache[table].update())
return result.get('inserted', 0) > 0
async def remove_content(table, key):
r.set_loop_type("asyncio")
conn = await r.connect(**db_opts)
try:
result = await r.table(table).get(key).delete().run(conn)
except r.ReqlOpFailedError:
result = {}
pass
await conn.close()
if table == 'server_settings':
loop.create_task(cache[table].update())
return result.get('deleted', 0) > 0
async def update_content(table, content, key):
r.set_loop_type("asyncio")
conn = await r.connect(**db_opts)
# This method is only for updating content, so if we find that it doesn't exist, just return false
try:
# Update based on the content and filter passed to us
# rethinkdb allows you to do many many things inside of update
# This is why we're accepting a variable and using it, whatever it may be, as the query
result = await r.table(table).get(key).update(content).run(conn)
except r.ReqlOpFailedError:
result = {}
await conn.close()
if table == 'server_settings':
loop.create_task(cache[table].update())
return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0
async def replace_content(table, content, key):
# This method is here because .replace and .update can have some different functionalities
r.set_loop_type("asyncio")
conn = await r.connect(**db_opts)
try:
result = await r.table(table).get(key).replace(content).run(conn)
except r.ReqlOpFailedError:
result = {}
await conn.close()
if table == 'server_settings':
loop.create_task(cache[table].update())
return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0
async def get_content(table, key=None):
r.set_loop_type("asyncio")
conn = await r.connect(**db_opts)
try:
if key:
cursor = await r.table(table).get(key).run(conn)
else:
cursor = await r.table(table).run(conn)
if cursor is None:
content = None
elif type(cursor) is not dict:
content = await _convert_to_list(cursor)
if len(content) == 0:
content = None
else:
content = cursor
except (IndexError, r.ReqlOpFailedError):
content = None
await conn.close()
return content
async def filter_content(table: str, r_filter):
r.set_loop_type("asyncio")
conn = await r.connect(**db_opts)
try:
cursor = await r.table(table).filter(r_filter).run(conn)
content = await _convert_to_list(cursor)
if len(content) == 0:
content = None
except (IndexError, r.ReqlOpFailedError):
content = None
await conn.close()
return content
async def _convert_to_list(cursor):
# This method is here because atm, AsyncioCursor is not iterable
# For our purposes, we want a list, so we need to do this manually
cursor_list = []
while True:
try:
val = await cursor.next()
cursor_list.append(val)
except r.ReqlCursorEmpty:
break
return cursor_list

View file

@ -1,195 +0,0 @@
import asyncio
import discord
class CannotPaginate(Exception):
pass
class Pages:
"""Implements a paginator that queries the user for the
pagination interface.
Pages are 1-index based, not 0-index based.
If the user does not reply within 2 minutes, the pagination
interface exits automatically.
"""
def __init__(self, bot, *, message, entries, per_page=10):
self.bot = bot
self.entries = entries
self.message = message
self.author = message.author
self.per_page = per_page
pages, left_over = divmod(len(self.entries), self.per_page)
if left_over:
pages += 1
self.maximum_pages = pages
self.embed = discord.Embed()
self.paginating = len(entries) > per_page
self.reaction_emojis = [
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ),
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
('\N{INFORMATION SOURCE}', self.show_help),
]
server = self.message.server
if server is not None:
self.permissions = self.message.channel.permissions_for(server.me)
else:
self.permissions = self.message.channel.permissions_for(self.bot.user)
if not self.permissions.embed_links:
raise CannotPaginate('Bot does not have embed links permission.')
def get_page(self, page):
base = (page - 1) * self.per_page
return self.entries[base:base + self.per_page]
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
p = []
for t in enumerate(entries, 1 + ((page - 1) * self.per_page)):
p.append('%s. %s' % t)
self.embed.set_footer(text='Page %s/%s (%s entries)' % (page, self.maximum_pages, len(self.entries)))
if not self.paginating:
self.embed.description = '\n'.join(p)
return await self.bot.send_message(self.message.channel, embed=self.embed)
if not first:
self.embed.description = '\n'.join(p)
await self.bot.edit_message(self.message, embed=self.embed)
return
# verify we can actually use the pagination session
if not self.permissions.add_reactions:
raise CannotPaginate('Bot does not have add reactions permission.')
if not self.permissions.read_message_history:
raise CannotPaginate('Bot does not have Read Message History permission.')
p.append('')
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
self.embed.description = '\n'.join(p)
self.message = await self.bot.send_message(self.message.channel, embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
try:
await self.bot.add_reaction(self.message, reaction)
except discord.NotFound:
# If the message isn't found, we don't care about clearing anything
return
async def checked_show_page(self, page):
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
async def first_page(self):
"""goes to the first page"""
await self.show_page(1)
async def last_page(self):
"""goes to the last page"""
await self.show_page(self.maximum_pages)
async def next_page(self):
"""goes to the next page"""
await self.checked_show_page(self.current_page + 1)
async def previous_page(self):
"""goes to the previous page"""
await self.checked_show_page(self.current_page - 1)
async def show_current_page(self):
if self.paginating:
await self.show_page(self.current_page)
async def numbered_page(self):
"""lets you type a page number to go to"""
to_delete = []
to_delete.append(await self.bot.send_message(self.message.channel, 'What page do you want to go to?'))
msg = await self.bot.wait_for_message(author=self.author, channel=self.message.channel,
check=lambda m: m.content.isdigit(), timeout=30.0)
if msg is not None:
page = int(msg.content)
to_delete.append(msg)
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
else:
to_delete.append(await self.bot.say('Invalid page given. (%s/%s)' % (page, self.maximum_pages)))
await asyncio.sleep(5)
else:
to_delete.append(await self.bot.send_message(self.message.channel, 'Took too long.'))
await asyncio.sleep(5)
try:
await self.bot.delete_messages(to_delete)
except Exception:
pass
async def show_help(self):
"""shows this message"""
e = discord.Embed()
messages = ['Welcome to the interactive paginator!\n']
messages.append('This interactively allows you to see pages of text by navigating with ' \
'reactions. They are as follows:\n')
for (emoji, func) in self.reaction_emojis:
messages.append('%s %s' % (emoji, func.__doc__))
e.description = '\n'.join(messages)
e.colour = 0x738bd7 # blurple
e.set_footer(text='We were on page %s before this message.' % self.current_page)
await self.bot.edit_message(self.message, embed=e)
async def go_back_to_current_page():
await asyncio.sleep(60.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def stop_pages(self):
"""stops the interactive pagination session"""
await self.bot.delete_message(self.message)
self.paginating = False
def react_check(self, reaction, user):
if user is None or user.id != self.author.id:
return False
for (emoji, func) in self.reaction_emojis:
if reaction.emoji == emoji:
self.match = func
return True
return False
async def paginate(self, start_page=1):
"""Actually paginate the entries and run the interactive loop if necessary."""
await self.show_page(start_page, first=True)
while self.paginating:
react = await self.bot.wait_for_reaction(message=self.message, check=self.react_check, timeout=120.0)
if react is None:
self.paginating = False
try:
await self.bot.clear_reactions(self.message)
except:
pass
finally:
break
try:
await self.bot.remove_reaction(self.message, react.reaction.emoji, react.user)
except:
pass # can't remove it so don't bother doing so
await self.match()

View file

@ -1,191 +0,0 @@
import aiohttp
from io import BytesIO
import inspect
from . import config
from PIL import Image
def convert_to_jpeg(pfile):
# Open the file given
img = Image.open(pfile)
# Create the BytesIO object we'll use as our new "file"
new_file = BytesIO()
# Save to this file as jpeg
img.save(new_file, format='JPEG')
# In order to use the file, we need to seek back to the 0th position
new_file.seek(0)
return new_file
def get_all_commands(bot):
"""Returns a list of all command names for the bot"""
# First lets create a set of all the parent names
parent_command_names = set(cmd.qualified_name for cmd in bot.commands.values())
all_commands = []
# Now lets loop through and get all the child commands for each command
# Only the command itself will be yielded if there are no children
for cmd_name in parent_command_names:
cmd = bot.commands.get(cmd_name)
for child_cmd in get_subcommands(cmd):
all_commands.append(child_cmd)
return all_commands
def get_subcommands(command):
yield command.qualified_name
try:
non_aliases = set(cmd.name for cmd in command.commands.values())
for cmd_name in non_aliases:
yield from get_subcommands(command.commands[cmd_name])
except AttributeError:
pass
async def channel_is_nsfw(channel):
server = channel.server.id
channel = channel.id
server_settings = await config.get_content('server_settings', server)
try:
return channel in server_settings['nsfw_channels']
except (TypeError, IndexError, KeyError):
return False
def find_command(bot, command):
"""Finds a command (be it parent or sub command) based on string given"""
# This method ensures the command given is valid. We need to loop through commands
# As bot.commands only includes parent commands
# So we are splitting the command in parts, looping through the commands
# And getting the subcommand based on the next part
# If we try to access commands of a command that isn't a group
# We'll hit an AttributeError, meaning an invalid command was given
# If we loop through and don't find anything, cmd will still be None
# And we'll report an invalid was given as well
cmd = None
for part in command.split():
try:
if cmd is None:
cmd = bot.commands.get(part)
else:
cmd = cmd.commands.get(part)
except AttributeError:
cmd = None
break
return cmd
async def download_image(url):
"""Returns a file-like object based on the URL provided"""
headers = {'User-Agent': config.user_agent}
# Simply read the image, to get the bytes
bts = await request(url, attr='read')
if bts is None:
return None
# Then wrap it in a BytesIO object, to be used like an actual file
image = BytesIO(bts)
return image
async def request(url, *, headers=None, payload=None, method='GET', attr='json'):
# Make sure our User Agent is what's set, and ensure it's sent even if no headers are passed
if headers == None:
headers = {}
headers['User-Agent'] = config.user_agent
# Try 5 times
for i in range(5):
try:
# Create the session with our headeres
with aiohttp.ClientSession(headers=headers) as session:
# Make the request, based on the method, url, and paramaters given
async with session.request(method, url, params=payload) as response:
# If the request wasn't successful, re-attempt
if response.status != 200:
continue
try:
# Get the attribute requested
return_value = getattr(response, attr)
# Next check if this can be called
if callable(return_value):
return_value = return_value()
# If this is awaitable, await it
if inspect.isawaitable(return_value):
return_value = await return_value
# Then return it
return return_value
except AttributeError:
# If an invalid attribute was requested, return None
return None
# If an error was hit other than the one we want to catch, try again
except:
continue
async def update_records(key, winner, loser):
# We're using the Harkness scale to rate
# http://opnetchessclub.wikidot.com/harkness-rating-system
r_filter = lambda row: (row['member_id'] == str(winner.id)) | (row['member_id'] == str(loser.id))
matches = await config.filter_content(key, r_filter)
winner_stats = {}
loser_stats = {}
try:
for stat in matches:
if stat.get('member_id') == winner.id:
winner_stats = stat
elif stat.get('member_id') == loser.id:
loser_stats = stat
except TypeError:
pass
winner_rating = winner_stats.get('rating') or 1000
loser_rating = loser_stats.get('rating') or 1000
# The scale is based off of increments of 25, increasing the change by 1 for each increment
# That is all this loop does, increment the "change" for every increment of 25
# The change caps off at 300 however, so break once we are over that limit
difference = abs(winner_rating - loser_rating)
rating_change = 0
count = 25
while count <= difference:
if count > 300:
break
rating_change += 1
count += 25
# 16 is the base change, increased or decreased based on whoever has the higher current rating
if winner_rating > loser_rating:
winner_rating += 16 - rating_change
loser_rating -= 16 - rating_change
else:
winner_rating += 16 + rating_change
loser_rating -= 16 + rating_change
# Just increase wins/losses for each person, making sure it's at least 0
winner_wins = winner_stats.get('wins', 0)
winner_losses = winner_stats.get('losses', 0)
loser_wins = loser_stats.get('wins', 0)
loser_losses = loser_stats.get('losses', 0)
winner_wins += 1
loser_losses += 1
# Now save the new wins, losses, and ratings
winner_stats = {'wins': winner_wins, 'losses': winner_losses, 'rating': winner_rating}
loser_stats = {'wins': loser_wins, 'losses': loser_losses, 'rating': loser_rating}
if not await config.update_content(key, winner_stats, winner.id):
winner_stats['member_id'] = winner.id
await config.add_content(key, winner_stats)
if not await config.update_content(key, loser_stats, loser.id):
loser_stats['member_id'] = loser.id
await config.add_content(key, loser_stats)

View file

@ -1,3 +0,0 @@
from .downloader import Downloader
from .playlist import Playlist
from .exceptions import *

View file

@ -1,85 +0,0 @@
import os
import asyncio
import functools
import youtube_dl
from concurrent.futures import ThreadPoolExecutor
ytdl_format_options = {
'format': 'bestaudio/best',
'extractaudio': True,
'audioformat': 'mp3',
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
'restrictfilenames': True,
'noplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': False,
'logtostderr': False,
'quiet': True,
'no_warnings': True,
'default_search': 'auto',
'source_address': '0.0.0.0'
}
# Fuck your useless bugreports message that gets two link embeds and confuses users
youtube_dl.utils.bug_reports_message = lambda: ''
'''
Alright, here's the problem. To catch youtube-dl errors for their useful information, I have to
catch the exceptions with `ignoreerrors` off. To not break when ytdl hits a dumb video
(rental videos, etc), I have to have `ignoreerrors` on. I can change these whenever, but with async
that's bad. So I need multiple ytdl objects.
'''
class Downloader:
def __init__(self, download_folder=None):
self.thread_pool = ThreadPoolExecutor(max_workers=2)
self.unsafe_ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
self.safe_ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
self.safe_ytdl.params['ignoreerrors'] = True
self.download_folder = download_folder
if download_folder:
otmpl = self.unsafe_ytdl.params['outtmpl']
self.unsafe_ytdl.params['outtmpl'] = os.path.join(download_folder, otmpl)
# print("setting template to " + os.path.join(download_folder, otmpl))
otmpl = self.safe_ytdl.params['outtmpl']
self.safe_ytdl.params['outtmpl'] = os.path.join(download_folder, otmpl)
@property
def ytdl(self):
return self.safe_ytdl
async def extract_info(self, loop, *args, on_error=None, retry_on_error=False, **kwargs):
"""
Runs ytdl.extract_info within the threadpool. Returns a future that will fire when it's done.
If `on_error` is passed and an exception is raised, the exception will be caught and passed to
on_error as an argument.
"""
if callable(on_error):
try:
return await loop.run_in_executor(self.thread_pool, functools.partial(self.unsafe_ytdl.extract_info, *args, **kwargs))
except Exception as e:
# (youtube_dl.utils.ExtractorError, youtube_dl.utils.DownloadError)
# I hope I don't have to deal with ContentTooShortError's
if asyncio.iscoroutinefunction(on_error):
asyncio.ensure_future(on_error(e), loop=loop)
elif asyncio.iscoroutine(on_error):
asyncio.ensure_future(on_error, loop=loop)
else:
loop.call_soon_threadsafe(on_error, e)
if retry_on_error:
return await self.safe_extract_info(loop, *args, **kwargs)
else:
return await loop.run_in_executor(self.thread_pool, functools.partial(self.unsafe_ytdl.extract_info, *args, **kwargs))
async def safe_extract_info(self, loop, *args, **kwargs):
return await loop.run_in_executor(self.thread_pool, functools.partial(self.safe_ytdl.extract_info, *args, **kwargs))

View file

@ -1,286 +0,0 @@
import asyncio
import json
import os
import traceback
import time
import discord
from hashlib import md5
from .exceptions import ExtractionError
async def get_header(session, url, headerfield=None, *, timeout=5):
with aiohttp.Timeout(timeout):
async with session.head(url) as response:
if headerfield:
return response.headers.get(headerfield)
else:
return response.headers
def md5sum(filename, limit=0):
fhash = md5()
with open(filename, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
fhash.update(chunk)
return fhash.hexdigest()[-limit:]
class BasePlaylistEntry:
def __init__(self):
self.filename = None
self._is_downloading = False
self._waiting_futures = []
@property
def is_downloaded(self):
if self._is_downloading:
return False
return bool(self.filename)
@classmethod
def from_json(cls, playlist, jsonstring):
raise NotImplementedError
def to_json(self):
raise NotImplementedError
async def _download(self):
raise NotImplementedError
def get_ready_future(self):
"""
Returns a future that will fire when the song is ready to be played. The future will either fire with the result (being the entry) or an exception
as to why the song download failed.
"""
future = asyncio.Future()
if self.is_downloaded:
# In the event that we're downloaded, we're already ready for playback.
future.set_result(self)
else:
# If we request a ready future, let's ensure that it'll actually resolve at one point.
asyncio.ensure_future(self._download())
self._waiting_futures.append(future)
return future
def _for_each_future(self, cb):
"""
Calls `cb` for each future that is not cancelled. Absorbs and logs any errors that may have occurred.
"""
futures = self._waiting_futures
self._waiting_futures = []
for future in futures:
if future.cancelled():
continue
try:
cb(future)
except:
traceback.print_exc()
def __eq__(self, other):
return self is other
def __hash__(self):
return id(self)
class URLPlaylistEntry(BasePlaylistEntry):
def __init__(self, playlist, url, title, requester, duration=0, expected_filename=None, **meta):
super().__init__()
self.playlist = playlist
self.url = url
self.title = title
self.duration = duration
self.expected_filename = expected_filename
self.meta = meta
self.requester = requester
self.download_folder = self.playlist.downloader.download_folder
def __str__(self):
fmt = '*{0}* requested by **{1.display_name}**'
if self.duration:
fmt += ' [length: {0[0]}m {0[1]}s]'.format(divmod(round(self.duration, 0), 60))
return fmt.format(self.title, self.requester)
@property
def length(self):
if self.duration:
return self.duration
@property
def progress(self):
if self.start_time:
return round(time.time() - self.start_time)
@property
def remaining(self):
length = self.length
progress = self.progress
if length and progress:
return length - progress
@classmethod
def from_json(cls, playlist, jsonstring):
data = json.loads(jsonstring)
print(data)
# TODO: version check
url = data['url']
title = data['title']
duration = data['duration']
downloaded = data['downloaded']
filename = data['filename'] if downloaded else None
meta = {}
# TODO: Better [name] fallbacks
if 'channel' in data['meta']:
ch = playlist.bot.get_channel(data['meta']['channel']['id'])
meta['channel'] = ch or data['meta']['channel']['name']
if 'author' in data['meta']:
meta['author'] = meta['channel'].server.get_member(data['meta']['author']['id'])
return cls(playlist, url, title, duration, filename, **meta)
def to_json(self):
data = {
'version': 1,
'type': self.__class__.__name__,
'url': self.url,
'title': self.title,
'duration': self.duration,
'downloaded': self.is_downloaded,
'filename': self.filename,
'meta': {
i: {
'type': self.meta[i].__class__.__name__,
'id': self.meta[i].id,
'name': self.meta[i].name
} for i in self.meta
}
# Actually I think I can just getattr instead, getattr(discord, type)
}
return json.dumps(data, indent=2)
# noinspection PyTypeChecker
async def _download(self):
if self._is_downloading:
return
self._is_downloading = True
try:
# Ensure the folder that we're going to move into exists.
if not os.path.exists(self.download_folder):
os.makedirs(self.download_folder)
# self.expected_filename: audio_cache\youtube-9R8aSKwTEMg-NOMA_-_Brain_Power.m4a
extractor = os.path.basename(self.expected_filename).split('-')[0]
# the generic extractor requires special handling
if extractor == 'generic':
# print("Handling generic")
flistdir = [f.rsplit('-', 1)[0] for f in os.listdir(self.download_folder)]
expected_fname_noex, fname_ex = os.path.basename(self.expected_filename).rsplit('.', 1)
if expected_fname_noex in flistdir:
try:
rsize = int(await get_header(self.playlist.bot.aiosession, self.url, 'CONTENT-LENGTH'))
except:
rsize = 0
lfile = os.path.join(
self.download_folder,
os.listdir(self.download_folder)[flistdir.index(expected_fname_noex)]
)
# print("Resolved %s to %s" % (self.expected_filename, lfile))
lsize = os.path.getsize(lfile)
# print("Remote size: %s Local size: %s" % (rsize, lsize))
if lsize != rsize:
await self._really_download(hash=True)
else:
# print("[Download] Cached:", self.url)
self.filename = lfile
else:
# print("File not found in cache (%s)" % expected_fname_noex)
await self._really_download(hash=True)
else:
ldir = os.listdir(self.download_folder)
flistdir = [f.rsplit('.', 1)[0] for f in ldir]
expected_fname_base = os.path.basename(self.expected_filename)
expected_fname_noex = expected_fname_base.rsplit('.', 1)[0]
# idk wtf this is but its probably legacy code
# or i have youtube to blame for changing shit again
if expected_fname_base in ldir:
self.filename = os.path.join(self.download_folder, expected_fname_base)
print("[Download] Cached:", self.url)
elif expected_fname_noex in flistdir:
print("[Download] Cached (different extension):", self.url)
self.filename = os.path.join(self.download_folder, ldir[flistdir.index(expected_fname_noex)])
print("Expected %s, got %s" % (
self.expected_filename.rsplit('.', 1)[-1],
self.filename.rsplit('.', 1)[-1]
))
else:
await self._really_download()
# Trigger ready callbacks.
self._for_each_future(lambda future: future.set_result(self))
except Exception as e:
traceback.print_exc()
self._for_each_future(lambda future: future.set_exception(e))
finally:
self._is_downloading = False
# noinspection PyShadowingBuiltins
async def _really_download(self, *, hash=False):
print("[Download] Started:", self.url)
try:
result = await self.playlist.downloader.extract_info(self.playlist.loop, self.url, download=True)
except Exception as e:
raise ExtractionError(e)
print("[Download] Complete:", self.url)
if result is None:
raise ExtractionError("ytdl broke and hell if I know why")
# What the fuck do I do now?
self.filename = unhashed_fname = self.playlist.downloader.ytdl.prepare_filename(result)
if hash:
# insert the 8 last characters of the file hash to the file name to ensure uniqueness
self.filename = md5sum(unhashed_fname, 8).join('-.').join(unhashed_fname.rsplit('.', 1))
if os.path.isfile(self.filename):
# Oh bother it was actually there.
os.unlink(unhashed_fname)
else:
# Move the temporary file to it's final location.
os.rename(unhashed_fname, self.filename)
def to_embed(self):
"""Returns an embed that can be used to display information about this particular song"""
# Create the embed object we'll use
embed = discord.Embed()
# Fill in the simple things
embed.add_field(name='Title', value=self.title, inline=False)
embed.add_field(name='Requester', value=self.requester.display_name, inline=False)
# Get the current length of the song and display this
length = divmod(round(self.length, 0), 60)
fmt = "{0[0]}m {0[1]}s".format(length)
embed.add_field(name='Duration', value=fmt,inline=False)
# And return the embed we created
return embed

View file

@ -1,38 +0,0 @@
import asyncio
import traceback
import collections
class EventEmitter:
def __init__(self):
self._events = collections.defaultdict(list)
self.loop = asyncio.get_event_loop()
def emit(self, event, *args, **kwargs):
if event not in self._events:
return
for cb in self._events[event]:
# noinspection PyBroadException
try:
if asyncio.iscoroutinefunction(cb):
asyncio.ensure_future(cb(*args, **kwargs), loop=self.loop)
else:
cb(*args, **kwargs)
except:
traceback.print_exc()
def on(self, event, cb):
self._events[event].append(cb)
return self
def off(self, event, cb):
self._events[event].remove(cb)
if not self._events[event]:
del self._events[event]
return self
# TODO: add .once

View file

@ -1,88 +0,0 @@
import shutil
import textwrap
# Base class for exceptions
class MusicbotException(Exception):
def __init__(self, message, *, expire_in=0):
self._message = message
self.expire_in = expire_in
@property
def message(self):
return self._message
@property
def message_no_format(self):
return self._message
# Something went wrong during the processing of a command
class CommandError(MusicbotException):
pass
# Something went wrong during the processing of a song/ytdl stuff
class ExtractionError(MusicbotException):
pass
# The no processing entry type failed and an entry was a playlist/vice versa
class WrongEntryTypeError(ExtractionError):
def __init__(self, message, is_playlist, use_url):
super().__init__(message)
self.is_playlist = is_playlist
self.use_url = use_url
# The user doesn't have permission to use a command
class PermissionsError(CommandError):
@property
def message(self):
return "You don't have permission to use that command.\nReason: " + self._message
# Error with pretty formatting for hand-holding users through various errors
class HelpfulError(MusicbotException):
def __init__(self, issue, solution, *, preface="An error has occured:\n", expire_in=0):
self.issue = issue
self.solution = solution
self.preface = preface
self.expire_in = expire_in
@property
def message(self):
return ("\n{}\n{}\n{}\n").format(
self.preface,
self._pretty_wrap(self.issue, " Problem: "),
self._pretty_wrap(self.solution, " Solution: "))
@property
def message_no_format(self):
return "\n{}\n{}\n{}\n".format(
self.preface,
self._pretty_wrap(self.issue, " Problem: ", width=None),
self._pretty_wrap(self.solution, " Solution: ", width=None))
@staticmethod
def _pretty_wrap(text, pretext, *, width=-1):
if width is None:
return pretext + text
elif width == -1:
width = shutil.get_terminal_size().columns
l1, *lx = textwrap.wrap(text, width=width - 1 - len(pretext))
lx = [((' ' * len(pretext)) + l).rstrip().ljust(width) for l in lx]
l1 = (pretext + l1).ljust(width)
return ''.join([l1, *lx])
class HelpfulWarning(HelpfulError):
pass
# Base class for control signals
class Signal(Exception):
pass
# signal to restart the bot
class RestartSignal(Signal):
pass
# signal to end the bot "gracefully"
class TerminateSignal(Signal):
pass

View file

@ -1,280 +0,0 @@
import datetime
import traceback
from collections import deque
from itertools import islice
from random import shuffle
from .entry import URLPlaylistEntry
from .exceptions import ExtractionError, WrongEntryTypeError
from .event_emitter import EventEmitter
class Playlist(EventEmitter):
"""
A playlist is manages the list of songs that will be played.
"""
def __init__(self, bot):
super().__init__()
self.bot = bot
self.loop = bot.loop
self.downloader = bot.downloader
self.entries = deque()
self.max_songs = 10
def __iter__(self):
return iter(self.entries)
def shuffle(self):
shuffle(self.entries)
def clear(self):
self.entries.clear()
@property
def full(self):
return self.count >= self.max_songs
@property
def count(self):
if self.entries:
return len(self.entries)
else:
return 0
async def add_entry(self, song_url, requester, **meta):
"""
Validates and adds a song_url to be played. This does not start the download of the song.
Returns the entry & the position it is in the queue.
:param song_url: The song url to add to the playlist.
:param meta: Any additional metadata to add to the playlist entry.
"""
try:
info = await self.downloader.extract_info(self.loop, song_url, download=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(song_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % song_url)
# TODO: Sort out what happens next when this happens
if info.get('_type', None) == 'playlist':
raise WrongEntryTypeError("This is a playlist.", True, info.get('webpage_url', None) or info.get('url', None))
if info['extractor'] in ['generic', 'Dropbox']:
try:
# unfortunately this is literally broken
# https://github.com/KeepSafe/aiohttp/issues/758
# https://github.com/KeepSafe/aiohttp/issues/852
content_type = await get_header(self.bot.aiosession, info['url'], 'CONTENT-TYPE')
print("Got content type", content_type)
except Exception as e:
print("[Warning] Failed to get content type for url %s (%s)" % (song_url, e))
content_type = None
if content_type:
if content_type.startswith(('application/', 'image/')):
if '/ogg' not in content_type: # How does a server say `application/ogg` what the actual fuck
raise ExtractionError("Invalid content type \"%s\" for url %s" % (content_type, song_url))
elif not content_type.startswith(('audio/', 'video/')):
print("[Warning] Questionable content type \"%s\" for url %s" % (content_type, song_url))
entry = URLPlaylistEntry(
self,
song_url,
info.get('title', 'Untitled'),
requester,
info.get('duration', 0) or 0,
self.downloader.ytdl.prepare_filename(info),
**meta
)
self._add_entry(entry)
return entry, len(self.entries)
async def import_from(self, playlist_url, **meta):
"""
Imports the songs from `playlist_url` and queues them to be played.
Returns a list of `entries` that have been enqueued.
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
:param meta: Any additional metadata to add to the playlist entry
"""
position = len(self.entries) + 1
entry_list = []
try:
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % playlist_url)
# Once again, the generic extractor fucks things up.
if info.get('extractor', None) == 'generic':
url_field = 'url'
else:
url_field = 'webpage_url'
baditems = 0
for items in info['entries']:
if items:
try:
entry = URLPlaylistEntry(
self,
items[url_field],
items.get('title', 'Untitled'),
items.get('duration', 0) or 0,
self.downloader.ytdl.prepare_filename(items),
**meta
)
self._add_entry(entry)
entry_list.append(entry)
except:
baditems += 1
# Once I know more about what's happening here I can add a proper message
traceback.print_exc()
print(items)
print("Could not add item")
else:
baditems += 1
if baditems:
print("Skipped %s bad entries" % baditems)
return entry_list, position
async def async_process_youtube_playlist(self, playlist_url, **meta):
"""
Processes youtube playlists links from `playlist_url` in a questionable, async fashion.
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
:param meta: Any additional metadata to add to the playlist entry
"""
try:
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % playlist_url)
gooditems = []
baditems = 0
for entry_data in info['entries']:
if entry_data:
baseurl = info['webpage_url'].split('playlist?list=')[0]
song_url = baseurl + 'watch?v=%s' % entry_data['id']
try:
entry, elen = await self.add_entry(song_url, **meta)
gooditems.append(entry)
except ExtractionError:
baditems += 1
except Exception as e:
baditems += 1
print("There was an error adding the song {}: {}: {}\n".format(
entry_data['id'], e.__class__.__name__, e))
else:
baditems += 1
if baditems:
print("Skipped %s bad entries" % baditems)
return gooditems
async def async_process_sc_bc_playlist(self, playlist_url, **meta):
"""
Processes soundcloud set and bancdamp album links from `playlist_url` in a questionable, async fashion.
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
:param meta: Any additional metadata to add to the playlist entry
"""
try:
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
except Exception as e:
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
if not info:
raise ExtractionError('Could not extract information from %s' % playlist_url)
gooditems = []
baditems = 0
for entry_data in info['entries']:
if entry_data:
song_url = entry_data['url']
try:
entry, elen = await self.add_entry(song_url, **meta)
gooditems.append(entry)
except ExtractionError:
baditems += 1
except Exception as e:
baditems += 1
print("There was an error adding the song {}: {}: {}\n".format(
entry_data['id'], e.__class__.__name__, e))
else:
baditems += 1
if baditems:
print("Skipped %s bad entries" % baditems)
return gooditems
def _add_entry(self, entry):
self.entries.append(entry)
self.emit('entry-added', playlist=self, entry=entry)
if self.peek() is entry:
entry.get_ready_future()
async def get_next_entry(self, predownload_next=True):
"""
A coroutine which will return the next song or None if no songs left to play.
Additionally, if predownload_next is set to True, it will attempt to download the next
song to be played - so that it's ready by the time we get to it.
"""
if not self.entries:
return None
entry = self.entries.popleft()
if predownload_next:
next_entry = self.peek()
if next_entry:
next_entry.get_ready_future()
return await entry.get_ready_future()
def peek(self):
"""
Returns the next entry that should be scheduled to be played.
"""
if self.entries:
return self.entries[0]
async def estimate_time_until(self, position, player):
"""
(very) Roughly estimates the time till the queue will 'position'
"""
estimated_time = sum([e.duration for e in islice(self.entries, position - 1)])
# When the player plays a song, it eats the first playlist item, so we just have to add the time back
if not player.is_stopped and player.current_entry:
estimated_time += player.current_entry.duration - player.progress
return datetime.timedelta(seconds=estimated_time)
def count_for_user(self, user):
return sum(1 for e in self.entries if e.meta.get('author', None) == user)

View file

@ -1,24 +1,17 @@
bot_token: 'token'
owner_id: ['12345']
description: 'Bot Description Here'
command_prefix: '!'
default_status: '!help for a list of commands'
discord_bots_key: 'key'
carbon_key: 'key'
twitch_key: 'key'
description: Description
command_prefix: ['?', '!']
default_status: 'Status'
dev_server: 'https://discord.gg/f6uzJEj'
user_agent: 'Bonfire/4.0.3 (https://github.com/Phxntxm/Bonfire)'
youtube_key: 'key'
osu_key: 'key'
dev_server: 'https://discord.gg/123456'
patreon_key: 'key'
patreon_id: 'id'
patreon_link: 'link'
spotify_id: 'id'
spotify_secret: 'secret'
user_agent: 'User-Agent/1.0.0 (Comment like link to site)'
extensions: [cogs.cog1, cogs.cog2]
shard_count: 1
shard_id: 0
db_host: 'localhost'
db_name: 'Bot_Name'
db_cert: 'cert.pem'
db_port: 28015
db_user: 'admin'
db_pass: 'password'
db_name: 'bonfire'
db_user: 'bonfire'

View file

@ -343,21 +343,21 @@ Moderator Utilities
Sets custom permissions for a provided command. Format must be 'perms add <command> <permission>'
If you want to open the command to everyone, provide 'none' as the permission
- Default permissions required: manage_server
- Default permissions required: manage_guild
- Aliases `setup, create`
.. data:: perms remove
Removes the custom permissions setup on a command
- Default permissions required: manage_server
- Default permissions required: manage_guild
- Aliases `delete`
.. data:: prefix
Used to setup a custom prefix for this server
- Default permissions required: manage_server
- Default permissions required: manage_guild
.. data:: purge
@ -384,14 +384,14 @@ Moderator Utilities
Adds the specified rule to the list of server's rules.
- Default permissions required: manage_server
- Default permissions required: manage_guild
- Aliases `rules create, rule create, rule add`
.. data:: rules remove
Deletes a specified rule from the server; the rule deleted needs to be specified by the number.
- Default permissions required: manage_server
- Default permissions required: manage_guild
- Aliases `rules delete, rule delete, rules remove`
Stats
@ -468,7 +468,7 @@ Blackjack
That is why this is restricted to someone who can manage the server, as it should only be used in case
people have gone afk and the game is still running, which can get annoying.
- Default permissions required: manage_server
- Default permissions required: manage_guild
DeviantArt
----------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

7
requirements.txt Normal file
View file

@ -0,0 +1,7 @@
pyyaml
psutil
pendulum
beautifulsoup4
osuapi
asyncpg
discord.py

View file

@ -1 +0,0 @@
fe04e897-b34e-4bf5-b79e-ccd5664bbd54

6
utils/__init__.py Normal file
View file

@ -0,0 +1,6 @@
from .cards import Deck, Face, Suit
from .checks import can_run
from .config import *
from .utilities import *
from .paginator import Pages, CannotPaginate, HelpPaginator
from .database import DB, Cache

188
utils/cards.py Normal file
View file

@ -0,0 +1,188 @@
import random
from enum import IntEnum, auto
class Suit(IntEnum):
clubs = auto()
hearts = auto()
diamonds = auto()
spades = auto()
class Face(IntEnum):
ace = auto()
two = auto()
three = auto()
four = auto()
five = auto()
six = auto()
seven = auto()
eight = auto()
nine = auto()
ten = auto()
jack = auto()
queen = auto()
king = auto()
class Deck:
def __init__(self, prefill=True, ace_high=True, spades_high=False):
self.deck = []
if prefill:
self.refresh()
self.ace_high = ace_high
self.spades_high = spades_high
def __iter__(self):
for card in self.deck:
yield card
def __getitem__(self, key):
return self.deck[key]
def refresh(self):
"""A method that 'restarts' the deck, filling it back with 52 cards"""
self.deck = []
for _suit in Suit:
for _face in Face:
self.insert(Card(_suit, _face, self))
@property
def count(self):
"""A property to provide how many cards are currently in the deck"""
return len(self.deck)
@property
def empty(self):
"""A property to determine whether or not the deck has cards in it"""
return len(self.deck) == 0
def draw(self, count=1):
"""Generator to draw from the deck"""
try:
for i in range(count):
yield self.deck.pop()
except IndexError:
yield None
def insert(self, cards):
"""Adds the provided cards to the end of the deck"""
try:
self.deck.extend(cards)
for card in cards:
card._deck = self
except TypeError:
self.deck.append(cards)
cards._deck = self
def index(self, card):
"""Returns the index of the card provided (-1 if card is not in the deck)"""
return self.deck.index(card)
def pluck(self, index=None, card=None):
"""Pulls the provided card from the deck"""
if index:
return self.deck.pop(index)
elif card:
return self.deck.pop(self.index(card))
def shuffle(self):
"""Shuffles the deck in place"""
random.SystemRandom().shuffle(self.deck)
def get_card(self, suit, face):
"""Returns the provided card in the deck"""
for card in self.deck:
if card.suit == suit and card.value == face:
return card
class Card:
"""The class that holds all the details for a card in the deck"""
def __init__(self, suit, value, deck):
self._suit = suit
self._value = value
self._deck = deck
@property
def suit(self):
"""The suit (club, diamond, heart, spade)"""
return self._suit
@property
def value(self):
"""The face value (2-10, J, Q, K, A)"""
return self._value
@property
def face_short(self):
"""The first 'letter' of the face, (2-10 will be the numbers)"""
if self.value == Face.ace or \
self.value == Face.jack or \
self.value == Face.queen or \
self.value == Face.king:
return self.value.name[0]
else:
return self.value.value
def __hash__(self):
return self.value.value + len(Face) * (self.suit.value - 1)
def __eq__(self, other):
if not isinstance(other, Card):
return False
return self.suit == other.suit and self.value == other.value
def __ne__(self, other):
if not isinstance(other, Card):
return True
return not self.__eq__(other)
def __str__(self):
return "%s of %s" % (self.value.name, self.suit.name)
def __lt__(self, other):
if self._deck.spades_high:
if other.suit == Suit.spades and self.suit != Suit.spades:
return True
if self.suit == Suit.spades and other.suit != Suit.spades:
return False
self_value = self.value.value
other_value = other.value.value
if self._deck.ace_high:
if self.value == Face.ace:
self_value = 14
if other.value == Face.ace:
other_value = 14
return self_value < other_value
def __gt__(self, other):
if self._deck.spades_high:
if other.suit == Suit.spades and self.suit != Suit.spades:
return False
if self.suit == Suit.spades and other.suit != Suit.spades:
return True
self_value = self.value.value
other_value = other.value.value
if self._deck.ace_high:
if self.value == Face.ace:
self_value = 14
if other.value == Face.ace:
other_value = 14
return self_value > other_value
def __le__(self, other):
return self.__eq__(other) or self.__lt__(other)
def __ge__(self, other):
return self.__eq__(other) or self.__gt__(other)

138
utils/checks.py Normal file
View file

@ -0,0 +1,138 @@
import asyncio
from discord.ext import commands
import discord
from . import utilities
loop = asyncio.get_event_loop()
def should_ignore(ctx):
if ctx.message.guild is None:
return False
ignored = ctx.bot.cache.ignored[ctx.guild.id]
if not ignored:
return False
return ctx.message.author.id in ignored['members'] or ctx.message.channel.id in ignored['channels']
async def check_not_restricted(ctx):
# Return true if this is a private channel, we'll handle that in the registering of the command
if type(ctx.message.channel) is discord.DMChannel:
return True
# First get all the restrictions
restrictions = ctx.bot.cache.restrictions[ctx.guild.id]
# Now lets check the "from" restrictions
for from_restriction in restrictions.get('from', []):
# Get the source and destination
# Source should ALWAYS be a command in this case
source = from_restriction.get('source')
destination = from_restriction.get('destination')
# Special check for what the "disable" command produces
if destination == "everyone" and ctx.command.qualified_name == source:
return False
# Convert destination to the object we want
destination = await utilities.convert(ctx, destination)
# If we couldn't find the destination, just continue with other restrictions
# Also if this restriction we're checking isn't for this command
if destination is None or source != ctx.command.qualified_name:
continue
# This means that the type of restriction we have is `command from channel`
# Which means we do not want commands to be ran in this channel
if destination == ctx.message.channel:
return False
# This type is `command from Role` meaning anyone with this role can't run this command
elif destination in ctx.message.author.roles:
return False
# This is `command from Member` meaning this user specifically cannot run this command
elif destination == ctx.message.author:
return False
# If we are here, then there are no blacklists stopping this from running
# Now for the to restrictions this is a little different, we need to make a whitelist and
# see if our current channel is in this whitelist, as well as any whitelisted roles are in the author's roles
# Only if there is no whitelist, do we want to blanket return True
to_restrictions = restrictions.get('to', [])
if len(to_restrictions) == 0:
return True
# Otherwise there is a whitelist, and we need to start it
whitelisted_channels = []
whitelisted_roles = []
for to_restriction in to_restrictions:
# Get the source and destination
# Source should ALWAYS be a command in this case
source = to_restriction.get('source')
destination = to_restriction.get('destination')
# Convert destination to the object we want
destination = await utilities.convert(ctx, destination)
# If we couldn't find the destination, just continue with other restrictions
# Also if this restriction we're checking isn't for this command
if destination is None or source != ctx.command.qualified_name:
continue
# Append to our two whitelists depending on what type this is
if isinstance(destination, discord.TextChannel):
whitelisted_channels.append(destination)
elif isinstance(destination, discord.Role):
whitelisted_roles.append(destination)
if whitelisted_channels:
if ctx.channel not in whitelisted_channels:
return False
if whitelisted_roles:
if not any(x in ctx.message.author.roles for x in whitelisted_roles):
return False
# If we have passed all of these, then we are allowed to run this command
# This looks like a whole lot, but all of these lists will be very tiny in almost all cases
# And only delving deep into the specific lists that may be large, will we finally see "large" lists
# Which means this still will not be slow in other cases
return True
def has_perms(ctx, **perms):
# Return true if this is a private channel, we'll handle that in the registering of the command
if type(ctx.message.channel) is discord.DMChannel:
return True
# Get the member permissions so that we can compare
guild_perms = ctx.message.author.guild_permissions
channel_perms = ctx.message.author.permissions_in(ctx.message.channel)
# Currently the library doesn't handle administrator overrides..so lets do this manually
if guild_perms.administrator:
return True
# Next, set the default permissions if one is not used, based on what was passed
# This will be overriden later, if we have custom permissions
required_perm = discord.Permissions.none()
for perm, setting in perms.items():
setattr(required_perm, perm, setting)
required_perm_value = ctx.bot.cache.custom_permissions[ctx.guild.id].get(ctx.command.qualified_name)
if required_perm_value:
required_perm = discord.Permissions(required_perm_value)
# Now just check if the person running the command has these permissions
return guild_perms >= required_perm or channel_perms >= required_perm
def can_run(**kwargs):
async def predicate(ctx):
# Next check if it requires any certain permissions
if kwargs and not has_perms(ctx, **kwargs):
return False
# Next...check custom restrictions
if not await check_not_restricted(ctx):
return False
# Then if the user/channel should be ignored
if should_ignore(ctx):
return False
# Otherwise....we're good
return True
predicate.perms = kwargs
return commands.check(predicate)

106
utils/config.py Normal file
View file

@ -0,0 +1,106 @@
import yaml
import asyncio
loop = asyncio.get_event_loop()
global_config = {}
# Ensure that the required config.yml file actually exists
try:
with open("config.yml", "r") as f:
global_config = yaml.safe_load(f)
global_config = {k: v for k, v in global_config.items() if v}
except FileNotFoundError:
print("You have no config file setup! Please use config.yml.sample to setup a valid config file")
quit()
try:
bot_token = global_config["bot_token"]
except KeyError:
print("You have no bot_token saved, this is a requirement for running a bot.")
print("Please use config.yml.sample to setup a valid config file")
quit()
# Default bot's description
bot_description = global_config.get("description")
# Bot's default prefix for commands
default_prefix = global_config.get("command_prefix", "!")
# The key for bot sites (discord bots and discordbots are TWO DIFFERENT THINGS)
discord_bots_key = global_config.get('discord_bots_key', "")
discordbots_key = global_config.get('discordbots_key', "")
carbon_key = global_config.get('carbon_key', "")
# The client ID for twitch requests
twitch_key = global_config.get('twitch_key', "")
# The key for youtube API calls
youtube_key = global_config.get("youtube_key", "")
# The key for Osu API calls
osu_key = global_config.get('osu_key', '')
# The key for e621 calls
e621_key = global_config.get('e621_key', '')
# The username the API key is under
e621_user = global_config.get('e621_user', '')
# The key for League of Legends API calls
lol_key = global_config.get('lol_key', '')
# The keys needed for deviant art calls
# The invite link for the server made for the bot
dev_server = global_config.get("dev_server", "")
# The User-Agent that we'll use for most requests
user_agent = global_config.get('user_agent', None)
# The URL to proxy youtube_dl's requests through
ytdl_proxy = global_config.get('youtube_dl_proxy', None)
# The patreon key, as well as the patreon ID to use
patreon_key = global_config.get('patreon_key', None)
patreon_id = global_config.get('patreon_id', None)
patreon_link = global_config.get('patreon_link', None)
# The client ID/secret for spotify
spotify_id = global_config.get("spotify_id", None)
spotify_secret = global_config.get("spotify_secret", None)
# Error channel to send uncaught exceptions to
error_channel = global_config.get("error_channel", None)
# The extensions to load
extensions = [
'cogs.interaction',
'cogs.misc',
'cogs.mod',
'cogs.admin',
'cogs.config',
'cogs.images',
'cogs.birthday',
'cogs.owner',
'cogs.stats',
'cogs.picarto',
'cogs.links',
'cogs.roles',
'cogs.tictactoe',
'cogs.hangman',
'cogs.events',
'cogs.raffle',
'cogs.blackjack',
'cogs.osu',
'cogs.tags',
'cogs.roulette',
'cogs.spotify',
'cogs.polls'
]
# The default status the bot will use
default_status = global_config.get("default_status", None)
# The database hostname
db_host = global_config.get('db_host', None)
# The database name
db_name = global_config.get('db_name', 'bonfire')
# The database port
db_port = global_config.get('db_port', None)
# The user and password assigned
db_user = global_config.get('db_user', None)
db_pass = global_config.get('db_pass', None)
# We've set all the options we need to be able to connect
# so create a dictionary that we can use to unload to connect
db_opts = {'host': db_host, 'database': db_name, 'port': db_port, 'user': db_user, 'password': db_pass}
def command_prefix(bot, message):
if not message.guild:
return default_prefix
return bot.cache.prefixes.get(message.guild.id) or default_prefix

141
utils/database.py Normal file
View file

@ -0,0 +1,141 @@
import asyncio
import asyncpg
from collections import defaultdict
from . import config
class Cache:
"""A class to hold the entires that are called on every message/command"""
def __init__(self, db):
self.db = db
self.prefixes = {}
self.ignored = defaultdict(dict)
self.custom_permissions = defaultdict(dict)
self.restrictions = defaultdict(dict)
async def setup(self):
# Make sure db is setup first
await self.db.setup()
await self.load_prefixes()
await self.load_custom_permissions()
await self.load_restrictions()
await self.load_ignored()
async def load_ignored(self):
query = """
SELECT
id, ignored_channels, ignored_members
FROM
guilds
WHERE
array_length(ignored_channels, 1) > 0 OR
array_length(ignored_members, 1) > 0
"""
rows = await self.db.fetch(query)
for row in rows:
self.ignored[row['id']]['members'] = row['ignored_members']
self.ignored[row['id']]['channels'] = row['ignored_channels']
async def load_prefixes(self):
query = """
SELECT
id, prefix
FROM
guilds
WHERE
prefix IS NOT NULL
"""
rows = await self.db.fetch(query)
for row in rows:
self.prefixes[row['id']] = row['prefix']
def update_prefix(self, guild, prefix):
self.prefixes[guild.id] = prefix
async def load_custom_permissions(self):
query = """
SELECT
guild, command, permission
FROM
custom_permissions
WHERE
permission IS NOT NULL
"""
rows = await self.db.fetch(query)
for row in rows:
self.custom_permissions[row['guild']][row['command']] = row['permission']
def update_custom_permission(self, guild, command, permission):
self.custom_permissions[guild.id][command.qualified_name] = permission
async def load_restrictions(self):
query = """
SELECT
guild, source, from_to, destination
FROM
restrictions
"""
rows = await self.db.fetch(query)
for row in rows:
opt = {"source": row['source'], "destination": row['destination']}
from_restrictions = self.restrictions[row['guild']].get(row['from_to'], [])
from_restrictions.append(opt)
self.restrictions[row['guild']][row['from_to']] = from_restrictions
def add_restriction(self, guild, from_to, restriction):
restrictions = self.restrictions[guild.id].get(from_to, [])
restrictions.append(restriction)
self.restrictions[guild.id][from_to] = restrictions
def remove_restriction(self, guild, from_to, restriction):
restrictions = self.restrictions[guild.id].get(from_to, [])
if restriction in restrictions:
restrictions.remove(restriction)
self.restrictions[guild.id][from_to] = restrictions
class DB:
def __init__(self):
self.loop = asyncio.get_event_loop()
self.opts = config.db_opts
self.cache = {}
self._pool = None
async def connect(self):
self._pool = await asyncpg.create_pool(**self.opts)
async def setup(self):
await self.connect()
async def _query(self, call, query, *args, **kwargs):
"""this will acquire a connection and make the call, then return the result"""
async with self._pool.acquire() as connection:
async with connection.transaction():
return await getattr(connection, call)(query, *args, **kwargs)
async def execute(self, *args, **kwargs):
return await self._query("execute", *args, **kwargs)
async def fetch(self, *args, **kwargs):
return await self._query("fetch", *args, **kwargs)
async def fetchrow(self, *args, **kwargs):
return await self._query("fetchrow", *args, **kwargs)
async def fetchval(self, *args, **kwargs):
return await self._query("fetchval", *args, **kwargs)
async def upsert(self, table, data):
keys = values = ""
for num, k in enumerate(data.keys()):
if num > 0:
keys += ", "
values += ", "
keys += k
values += f"${num}"
query = f"INSERT INTO {table} ({keys}) VALUES ({values}) ON CONFLICT DO UPDATE"
return await self.execute(query, *data.values())

View file

@ -1,20 +1,28 @@
import aiohttp
import datetime
import os
from shutil import copyfile
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, ImageOps
from . import utilities
base_path = "images/banner/base"
tmp_path = "images/banner/tmp"
whitneyMedium = "/usr/share/fonts/whitney-medium.ttf"
whitneyBold = "/usr/share/fonts/whitney-bold.ttf"
whitneyMedium = "fonts/whitney-medium.ttf"
whitneyBold = "fonts/whitney-bold.ttf"
header_height = 125
canvas_height = 145
banner_background = "{}/bannerTop2.png".format(base_path)
banner_bot = "{}/bannerBot.png".format(base_path)
def convert_to_file(img):
"""Converts a Pillow image to a file-like object"""
new_file = BytesIO()
# Save to this file as jpeg
img.save(new_file, format='PNG')
# In order to use the file, we need to seek back to the 0th position
new_file.seek(0)
return new_file
async def create_banner(member, image_title, data):
"""Creates a banner based on the options passed
Paramaters:
@ -23,23 +31,10 @@ async def create_banner(member, image_title, data):
data -> A dictionary that will be displayed, in the format 'Key: Value' like normal dictionaries"""
# First ensure the paths we need are created
os.makedirs(base_path, exist_ok=True)
os.makedirs(tmp_path, exist_ok=True)
offset = 125
# Open up the avatar, save it as a temporary file
avatar_url = member.avatar_url
avatar_path = "{}/avatar_{}_{}.jpg".format(tmp_path, member.id, int(datetime.datetime.utcnow().timestamp()))
# Ensure the user has an avatar
if avatar_url != "":
with aiohttp.ClientSession() as s:
async with s.get(avatar_url) as r:
val = await r.read()
with open(avatar_path, "wb") as f:
f.write(val)
# Otherwise use the default avatar
else:
avatar_src_path = "{}/default_avatar.png".format(base_path)
copyfile(avatar_src_path, avatar_path)
# Download the avatar
avatar = await utilities.download_image(str(member.avatar_url))
# Parse the data we need to create our image
username = (member.display_name[:23] + '...') if len(member.display_name) > 23 else member.display_name
@ -47,12 +42,11 @@ async def create_banner(member, image_title, data):
result_keys = [k for k, v in data]
result_values = [v for k, v in data]
lines_of_text = len(result_keys)
output_file = "{}/banner_{}_{}.jpg".format(tmp_path, member.id, int(datetime.datetime.utcnow().timestamp()))
base_height = canvas_height + (lines_of_text * 20)
# This is the background to the avatar
mask = Image.open('{}/mask.png'.format(base_path)).convert('L')
user_avatar = Image.open(avatar_path)
user_avatar = Image.open(avatar)
output = ImageOps.fit(user_avatar, mask.size, centering=(0.5, 0.5))
output.putalpha(mask)
@ -98,14 +92,13 @@ async def create_banner(member, image_title, data):
stat_offset = draw.textsize(text, font=font, spacing=0)
font = ImageFont.truetype(whitneyMedium, 96)
draw.text((360, -4), text, (255, 255, 255), font=font, align="center")
# draw.text((360, -4), text, (255, 255, 255), font=font, align="center")
draw.text((360, -4), text, (255, 255, 255), font=font)
draw.text((360 + stat_offset[0], -4), stat_text, (0, 402, 504), font=font)
save_me = text_bar.resize((350, 20), Image.ANTIALIAS)
offset += 20
base_image.paste(save_me, (0, offset), save_me)
base_image.paste(header, (0, 0), header)
base_image.save(output_file)
output = convert_to_file(base_image)
os.remove(avatar_path)
return output_file
return output

509
utils/paginator.py Normal file
View file

@ -0,0 +1,509 @@
import asyncio
import discord
import itertools
import inspect
import re
class CannotPaginate(Exception):
pass
class Pages:
"""Implements a paginator that queries the user for the
pagination interface.
Pages are 1-index based, not 0-index based.
If the user does not reply within 2 minutes then the pagination
interface exits automatically.
Parameters
------------
ctx: Context
The context of the command.
entries: List[str]
A list of entries to paginate.
per_page: int
How many entries show up per page.
show_entry_count: bool
Whether to show an entry count in the footer.
Attributes
-----------
embed: discord.Embed
The embed object that is being used to send pagination info.
Feel free to modify this externally. Only the description,
footer fields, and colour are internally modified.
permissions: discord.Permissions
Our permissions for the channel.
"""
def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True):
self.bot = ctx.bot
self.entries = entries
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.per_page = per_page
pages, left_over = divmod(len(self.entries), self.per_page)
if left_over:
pages += 1
self.maximum_pages = pages
self.embed = discord.Embed(colour=discord.Colour.blurple())
self.paginating = len(entries) > per_page
self.show_entry_count = show_entry_count
self.reaction_emojis = [
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page),
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
('\N{INFORMATION SOURCE}', self.show_help),
]
if ctx.guild is not None:
self.permissions = self.channel.permissions_for(ctx.guild.me)
else:
self.permissions = self.channel.permissions_for(ctx.bot.user)
if not self.permissions.embed_links:
raise CannotPaginate('Bot does not have embed links permission.')
if not self.permissions.send_messages:
raise CannotPaginate('Bot cannot send messages.')
if self.paginating:
# verify we can actually use the pagination session
if not self.permissions.add_reactions:
raise CannotPaginate('Bot does not have add reactions permission.')
if not self.permissions.read_message_history:
raise CannotPaginate('Bot does not have Read Message History permission.')
def get_page(self, page):
base = (page - 1) * self.per_page
return self.entries[base:base + self.per_page]
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
p = []
for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
p.append(f'{index}. {entry}')
if self.maximum_pages > 1:
if self.show_entry_count:
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
else:
text = f'Page {page}/{self.maximum_pages}'
self.embed.set_footer(text=text)
if not self.paginating:
self.embed.description = '\n'.join(p)
return await self.channel.send(embed=self.embed)
if not first:
self.embed.description = '\n'.join(p)
await self.message.edit(embed=self.embed)
return
p.append('')
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
self.embed.description = '\n'.join(p)
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
async def checked_show_page(self, page):
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
async def first_page(self):
"""goes to the first page"""
await self.show_page(1)
async def last_page(self):
"""goes to the last page"""
await self.show_page(self.maximum_pages)
async def next_page(self):
"""goes to the next page"""
await self.checked_show_page(self.current_page + 1)
async def previous_page(self):
"""goes to the previous page"""
await self.checked_show_page(self.current_page - 1)
async def show_current_page(self):
if self.paginating:
await self.show_page(self.current_page)
async def numbered_page(self):
"""lets you type a page number to go to"""
to_delete = []
to_delete.append(await self.channel.send('What page do you want to go to?'))
def message_check(m):
return m.author == self.author and self.channel == m.channel and m.content.isdigit()
try:
msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
except asyncio.TimeoutError:
to_delete.append(await self.channel.send('Took too long.'))
await asyncio.sleep(5)
else:
page = int(msg.content)
to_delete.append(msg)
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
else:
to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
await asyncio.sleep(5)
try:
await self.channel.delete_messages(to_delete)
except Exception:
pass
async def show_help(self):
"""shows this message"""
messages = ['Welcome to the interactive paginator!\n']
messages.append('This interactively allows you to see pages of text by navigating with '
'reactions. They are as follows:\n')
for (emoji, func) in self.reaction_emojis:
messages.append(f'{emoji} {func.__doc__}')
self.embed.description = '\n'.join(messages)
self.embed.clear_fields()
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(60.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def stop_pages(self):
"""stops the interactive pagination session"""
await self.message.delete()
self.paginating = False
def react_check(self, reaction, user):
if user is None or user.id != self.author.id:
return False
if reaction.message.id != self.message.id:
return False
for (emoji, func) in self.reaction_emojis:
if reaction.emoji == emoji:
self.match = func
return True
return False
async def paginate(self):
"""Actually paginate the entries and run the interactive loop if necessary."""
first_page = self.show_page(1, first=True)
if not self.paginating:
await first_page
else:
# allow us to react to reactions right away if we're paginating
self.bot.loop.create_task(first_page)
while self.paginating:
try:
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
except asyncio.TimeoutError:
self.paginating = False
try:
await self.message.clear_reactions()
except:
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except:
pass # can't remove it so don't bother doing so
await self.match()
class FieldPages(Pages):
"""Similar to Pages except entries should be a list of
tuples having (key, value) to show as embed fields instead.
"""
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
self.embed.clear_fields()
self.embed.description = discord.Embed.Empty
for key, value in entries:
self.embed.add_field(name=key, value=value, inline=False)
if self.maximum_pages > 1:
if self.show_entry_count:
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
else:
text = f'Page {page}/{self.maximum_pages}'
self.embed.set_footer(text=text)
if not self.paginating:
return await self.channel.send(embed=self.embed)
if not first:
await self.message.edit(embed=self.embed)
return
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
# ?help
# ?help Cog
# ?help command
# -> could be a subcommand
_mention = re.compile(r'<@\!?([0-9]{1,19})>')
def cleanup_prefix(bot, prefix):
m = _mention.match(prefix)
if m:
user = bot.get_user(int(m.group(1)))
if user:
return f'@{user.name} '
return prefix
async def _can_run(cmd, ctx):
try:
return await cmd.can_run(ctx)
except:
return False
def _command_signature(cmd):
# this is modified from discord.py source
result = [cmd.qualified_name]
if cmd.usage:
result.append(cmd.usage)
return ' '.join(result)
params = cmd.clean_params
if not params:
return ' '.join(result)
for name, param in params.items():
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append(f'[{name}={param.default!r}]')
else:
result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL:
result.append(f'[{name}...]')
else:
result.append(f'<{name}>')
return ' '.join(result)
class HelpPaginator(Pages):
def __init__(self, ctx, entries, *, per_page=4):
super().__init__(ctx, entries=entries, per_page=per_page)
self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
self.total = len(entries)
@classmethod
async def from_cog(cls, ctx, cog):
cog_name = cog.__class__.__name__
# get the commands
entries = sorted(ctx.bot.get_cog_commands(cog_name), key=lambda c: c.name)
# remove the ones we can't run
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
self = cls(ctx, entries)
self.title = f'{cog_name} Commands'
self.description = inspect.getdoc(cog)
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
return self
@classmethod
async def from_command(cls, ctx, command):
try:
entries = sorted(command.commands, key=lambda c: c.name)
except AttributeError:
entries = []
else:
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
self = cls(ctx, entries)
self.title = command.signature
if command.description:
self.description = f'{command.description}\n\n{command.help}'
else:
self.description = command.help or 'No help given.'
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
return self
@classmethod
async def from_bot(cls, ctx):
def key(c):
return c.cog_name or '\u200bMisc'
entries = sorted(ctx.bot.commands, key=key)
nested_pages = []
per_page = 9
# 0: (cog, desc, commands) (max len == 9)
# 1: (cog, desc, commands) (max len == 9)
# ...
for cog, commands in itertools.groupby(entries, key=key):
plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden]
if len(plausible) == 0:
continue
description = ctx.bot.get_cog(cog)
if description is None:
description = discord.Embed.Empty
else:
description = inspect.getdoc(description) or discord.Embed.Empty
nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
# swap the get_page implementation with one that supports our style of pagination
self.get_page = self.get_bot_page
self._is_bot = True
# replace the actual total
self.total = sum(len(o) for _, _, o in nested_pages)
return self
def get_bot_page(self, page):
cog, description, commands = self.entries[page - 1]
self.title = f'{cog} Commands'
self.description = description
return commands
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
self.embed.clear_fields()
self.embed.description = self.description
self.embed.title = self.title
if hasattr(self, '_is_bot'):
value = 'For more help, join the official bot support server: https://discord.gg/f6uzJEj'
self.embed.add_field(name='Support', value=value, inline=False)
self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
signature = _command_signature
for entry in entries:
self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
if self.maximum_pages:
self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
if not self.paginating:
return await self.channel.send(embed=self.embed)
if not first:
await self.message.edit(embed=self.embed)
return
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
async def show_help(self):
"""shows this message"""
self.embed.title = 'Paginator help'
self.embed.description = 'Hello! Welcome to the help page.'
messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
self.embed.clear_fields()
self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(30.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def show_bot_help(self):
"""shows how to use the bot"""
self.embed.title = 'Using the bot'
self.embed.description = 'Hello! Welcome to the help page.'
self.embed.clear_fields()
entries = (
('<argument>', 'This means the argument is __**required**__.'),
('[argument]', 'This means the argument is __**optional**__.'),
('[A|B]', 'This means the it can be __**either A or B**__.'),
('[argument...]', 'This means you can have multiple arguments.\n'
'Now that you know the basics, it should be noted that...\n'
'__**You do not type in the brackets!**__')
)
self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.')
for name, value in entries:
self.embed.add_field(name=name, value=value, inline=False)
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(30.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())

Some files were not shown because too many files have changed in this diff Show more