Compare commits

...

615 Commits

Author SHA1 Message Date
Stijn De Clercq d46a31a7be
Merge pull request #182 from stijndcl/fix/markdown-names
Fix formatting edge case for usernames starting with "XX."
2024-02-12 22:58:36 +01:00
stijndcl 165be35f8e Fix name formatting 2024-02-12 22:55:35 +01:00
Stijn De Clercq 35d1d871de
Merge pull request #181 from stijndcl/return-dinks
Re-enable some currency commands
2024-02-12 20:20:51 +01:00
stijndcl cc75dd8909 Fix typing 2024-02-12 20:16:52 +01:00
stijndcl 685e4d953e Re-enable currency, add withdraw and cf 2024-02-12 20:07:57 +01:00
Stijn De Clercq f181a7a833
Merge pull request #180 from stijndcl/fix/free-games
Fix free games
2023-12-25 23:04:30 +01:00
stijndcl dc4ee08b63 Fix free games 2023-12-25 23:00:48 +01:00
Stijn De Clercq 17b6ebac89 Add image processing 2023-09-24 22:05:34 +02:00
Stijn De Clercq 5bfd3a92a9
Merge pull request #177 from stijndcl/didier-3.7.0
Didier v3.7.0
2023-09-24 16:28:02 +02:00
Stijn De Clercq 1fc7fc94d8 Purge old aliases & add them back 2023-09-24 16:23:45 +02:00
Stijn De Clercq ae28f3a286 Add more ids of courses 2023-09-24 15:40:27 +02:00
Stijn De Clercq a54fb985e7 Change Ids of courses 2023-09-24 15:34:39 +02:00
Stijn De Clercq 3ca4520e56 Stash 2023-09-17 16:34:30 +02:00
Stijn De Clercq eee0a38afa
Merge pull request #176 from stijndcl/bump-dependencies
Bump dependencies & fix typing
2023-07-16 22:21:22 +02:00
Stijn De Clercq d874ec6bbd Fix file handler type 2023-07-16 22:16:32 +02:00
Stijn De Clercq cdfcd094a4 Fix typing everywhere, fetch error channel after bot is ready 2023-07-08 15:38:17 +02:00
Stijn De Clercq feccb88cfd Fix db typing 2023-07-08 01:23:47 +02:00
Stijn De Clercq d52c80aa8b Linting 2023-07-08 01:06:25 +02:00
Stijn De Clercq e6a615cad5 Migrate database 2023-07-07 16:23:36 +02:00
Stijn De Clercq c2a2fee50f Update all dependencies, move to pydantic 2.x and sqlalchemy 2.x 2023-07-07 16:16:33 +02:00
Stijn De Clercq 9998236e03
Merge pull request #174 from stijndcl/remove-wordle
Remove wordle
2023-07-07 15:14:23 +02:00
Stijn De Clercq ee6013da5d Remove wordle 2023-07-07 14:54:52 +02:00
Stijn De Clercq 4b25d5d519 Remove wordle 2023-07-07 14:51:50 +02:00
Stijn De Clercq c3a7ff8e4c
Merge pull request #172 from stijndcl/add-infosite
Add DB script to insert BSC and MSC infosites
2023-06-23 19:07:24 +02:00
Stijn De Clercq 18689c3de8 Add DB script to insert BSC and MSC 2023-06-23 19:01:54 +02:00
Stijn De Clercq 4de765ef62
Merge pull request #169 from stijndcl/events-qol
Events QOL changes
2023-02-17 00:08:06 +00:00
Stijn De Clercq 1af7e241e2 Small bugfix in timezone printing 2023-02-17 01:04:24 +01:00
Stijn De Clercq c0adc55be2 Make embed slightly nicer 2023-02-17 00:39:32 +01:00
Stijn De Clercq 264e7b5300 Tidy up events a bit 2023-02-17 00:35:40 +01:00
Stijn De Clercq c570cd2db2
Merge pull request #167 from stijndcl/events
Add event reminders
2023-02-16 23:01:45 +00:00
Stijn De Clercq deba3ababa Typing 2023-02-16 23:57:56 +01:00
stijndcl 4e205a02c7 Last fixes 2023-02-16 23:44:26 +01:00
stijndcl 4548f0e003 Fix listeners 2023-02-16 23:44:26 +01:00
stijndcl 1831446f65 Create first implementation of events 2023-02-16 23:44:26 +01:00
stijndcl 5deb312474 Start working on events 2023-02-16 23:44:26 +01:00
Stijn De Clercq 1ae7790ca2
Merge pull request #166 from stijndcl/fix-deadlines
Fix bug in deadlines, update isort pre-commit hook
2023-02-16 22:39:23 +00:00
Stijn De Clercq 96db71a185 Fix pre-commit hooks 2023-02-16 23:35:50 +01:00
Stijn De Clercq ce7f2eafd8
Merge pull request #165 from stijndcl/3.4.0
3.4.0
2023-02-02 21:37:11 +01:00
Stijn De Clercq c7b0bb8606
Merge pull request #163 from stijndcl/snipe-original
Make edit snipe keep the original message
2023-01-16 21:37:21 +01:00
Stijn De Clercq 73a9512339
Merge pull request #162 from stijndcl/sync-info
Add extra debug info to sync command
2023-01-16 21:34:39 +01:00
stijndcl 617e69b7a3 Keep the original message when sniping edits 2023-01-16 21:33:58 +01:00
stijndcl 29dcf845de Add extra debug info to sync command 2023-01-16 21:27:32 +01:00
Stijn De Clercq ad0076537d
Merge pull request #160 from stijndcl/fix-ci
Fix CI
2023-01-11 23:43:28 +01:00
stijndcl ff5de8e88b Update CI & fix broken test 2023-01-11 23:37:39 +01:00
stijndcl d7daeb02ff Forgot some 2023-01-11 23:30:49 +01:00
stijndcl a05d8e7138 Fix CI 2023-01-11 23:27:09 +01:00
Stijn De Clercq 5bfb1cecb2
Merge pull request #159 from stijndcl/3.3.0
3.3.0
2023-01-11 23:18:08 +01:00
Stijn De Clercq 3d429f21cc
Merge pull request #156 from stijndcl/mock
Re-write Mock
2022-11-21 22:52:43 +01:00
stijndcl 6c8ab9a2a0 Check escaped message length before mocking it 2022-11-21 22:49:23 +01:00
stijndcl accda93461 Add Mock back in 2022-11-21 22:46:19 +01:00
Stijn De Clercq 3bc9c15af7
Merge pull request #155 from stijndcl/3.2.0
3.2.0
2022-11-07 10:24:41 +01:00
Stijn De Clercq 8f8f7fed4a
Merge pull request #154 from stijndcl/deadlines
Filter out irrelevant deadlines
2022-11-05 22:31:08 +01:00
stijndcl 9e9be39358 Only show upcoming deadlines for courses the user is actually subscribed to 2022-11-05 22:26:48 +01:00
Stijn De Clercq c1721b951c
Merge pull request #153 from stijndcl/3.1.1
v3.1.1
2022-11-02 01:42:30 +01:00
Stijn De Clercq 465456f128
Merge pull request #152 from stijndcl/change-calendar-loading
Load calendars on startup & disable task
2022-11-02 01:38:47 +01:00
Stijn De Clercq 65671f271b
Merge pull request #151 from stijndcl/interactions-error-handler
Update the error handler for hybrid commands
2022-11-02 01:37:46 +01:00
stijndcl 4c4b7ec0cf Load calendars once on startup (before connecting) 2022-11-02 01:34:52 +01:00
stijndcl 8707f467d5 Typing 2022-11-02 01:32:55 +01:00
stijndcl d374f1e8ab Update the error handler for hybrid commands 2022-11-02 01:23:01 +01:00
Stijn De Clercq 87e5b1be9f
Merge pull request #150 from stijndcl/squash-newlines-in-announcements
Squash consecutive newlines
2022-11-01 21:16:11 +01:00
stijndcl 5505ce64c8 Squash consecutive newlines 2022-11-01 21:11:34 +01:00
Stijn De Clercq 21aa118ffa
Merge pull request #147 from stijndcl/dev
Didier v3.1
2022-10-26 08:58:51 +02:00
Stijn De Clercq 78bb9ee66d
Merge pull request #146 from stijndcl/fix-school-date-args
Fix school date args
2022-10-26 08:55:17 +02:00
stijndcl 462a8ffd3d Fix school date args 2022-10-26 08:51:51 +02:00
Stijn De Clercq 9a64523baa
Merge pull request #144 from stijndcl/fix-db-script-runner
Fix db script runner
2022-10-19 22:40:59 +02:00
stijndcl 405545a9b0 Fix db script runner 2022-10-19 22:37:17 +02:00
Stijn De Clercq 969c66e2bf
Merge pull request #143 from stijndcl/ufora
Create command to link to Ufora courses
2022-10-18 22:40:13 +02:00
stijndcl 560830b0d8 Create command to link to ufora courses 2022-10-18 22:35:32 +02:00
Stijn De Clercq 69aa50ef44
Merge pull request #142 from stijndcl/xkcd
Rewrite XKCD command
2022-10-18 22:28:03 +02:00
stijndcl 65785fef8f Add XKCD command back in 2022-10-18 22:23:55 +02:00
Stijn De Clercq 1c249adb46
Merge pull request #141 from stijndcl/free-games
Check for free games
2022-10-18 21:36:28 +02:00
stijndcl b5c97459f9 Typing 2022-10-18 21:32:56 +02:00
stijndcl 3509bd81e4 Optimize tasks 2022-10-18 10:06:13 +02:00
stijndcl 5c0ebb7eeb Add a few TODO's and fix small bug 2022-10-13 22:37:45 +02:00
stijndcl 855f60727b Scraping & parsing for Steam 2022-10-13 22:31:45 +02:00
stijndcl deefeb1106 Check for free games 2022-10-13 20:00:46 +02:00
Stijn De Clercq 625429daff
Merge pull request #139 from stijndcl/fix-missing-formatting
Add missing format specifier
2022-10-12 22:18:27 +02:00
stijndcl 8ae9b5f77c Add missing format specifier 2022-10-12 22:14:09 +02:00
Stijn De Clercq 41b5efd12d
Merge pull request #136 from stijndcl/fix-today-timezone
Make "today" tz-aware
2022-10-12 08:52:52 +02:00
stijndcl 4160693067 Forgot one 2022-10-12 08:49:22 +02:00
stijndcl 6c959e2b86 Make today() tz-aware 2022-10-12 08:45:35 +02:00
stijndcl 09dbe61dfe Fix typo 2022-10-06 10:40:14 +02:00
stijndcl d190185c12 Fix announcement timezones 2022-10-05 21:04:06 +02:00
stijndcl 27accf61b7 Fix announcement footer 2022-10-02 18:53:39 +02:00
stijndcl 950b7330d0 Add modsim role 2022-09-28 19:49:37 +02:00
stijndcl 3f8778b581 Fix typo 2022-09-27 18:41:28 +02:00
stijndcl a0c2cee857 Add extra course for Machine Learning 2022-09-27 18:34:07 +02:00
stijndcl 654dcb228d Fix role id's for some 2nd year courses 2022-09-27 18:22:44 +02:00
stijndcl d5db08e6ac Show error message if no definitions are found 2022-09-27 11:32:19 +02:00
stijndcl 105cee7e6e Fix memegen preview 2022-09-25 18:54:18 +02:00
stijndcl 5528ce7c2e Make memegen preview ephemeral 2022-09-25 18:40:26 +02:00
stijndcl 60181aadea Allow admins to set user's birthdays 2022-09-25 18:21:53 +02:00
stijndcl fa129efd0c Reduce db load for custom commands 2022-09-25 17:59:41 +02:00
stijndcl 5f72701714 Fix page splitting for menus 2022-09-25 17:41:22 +02:00
stijndcl 89b345c61b Improve error message handling 2022-09-25 17:22:02 +02:00
stijndcl ce7ba4285a Put slots in set at the end 2022-09-25 15:58:36 +02:00
stijndcl 3a44a1b679 Fix duplicate schedule slots 2022-09-25 15:57:02 +02:00
Stijn De Clercq ca38d06d98
Merge pull request #135 from stijndcl/didier-v3
Remove unnecessary setting, add check around schedule parsing
2022-09-25 15:29:19 +02:00
stijndcl 1df345838d Remove unnecessary setting, add check around schedule parsing 2022-09-25 15:25:29 +02:00
Stijn De Clercq 98b18f7ee3
Merge pull request #122 from stijndcl/didier-v3
Didier v3 - Rewrite from Scratch
2022-09-25 14:52:12 +02:00
stijndcl 3167c99645 Disable currency for now 2022-09-25 14:06:54 +02:00
stijndcl dd93080199 Cap autocompletion size 2022-09-25 01:59:55 +02:00
stijndcl eea44fcfa5 Final courses script 2022-09-25 01:56:50 +02:00
stijndcl 7e2b7b97ff Add more schedule settings 2022-09-25 00:17:36 +02:00
stijndcl c79c478ab4 Start working on new roles 2022-09-25 00:09:28 +02:00
stijndcl 225cc8129e Fix revision 2022-09-24 17:04:35 +02:00
stijndcl 961c125648 Typing 2022-09-24 17:01:58 +02:00
stijndcl 773491e2ff Clap 2022-09-23 20:30:00 +02:00
stijndcl bf32a5ef47 Create command to list custom commands, add shortcuts to memegen commands 2022-09-23 18:06:33 +02:00
Stijn De Clercq 8922489a41
Merge pull request #134 from stijndcl/reminders
Reminders
2022-09-23 15:11:53 +02:00
stijndcl 3e495d8291 Remove debug date 2022-09-23 15:07:13 +02:00
stijndcl 0a9f73af8c Make tasks log exceptions 2022-09-23 14:59:47 +02:00
stijndcl 185aaadce1 Merge sequential slots into one 2022-09-23 14:47:42 +02:00
stijndcl ddd632ffd5 Fix typo 2022-09-23 14:27:56 +02:00
stijndcl d03ece6f58 Create reminders, fix bugs in schedule parsing 2022-09-23 14:25:13 +02:00
stijndcl 00a146cb2b List memes 2022-09-22 17:28:23 +02:00
stijndcl 6cfe788df5 Fix typing 2022-09-22 17:01:56 +02:00
stijndcl f049f1c80b Improve docs for memegen 2022-09-22 17:01:29 +02:00
stijndcl f19a832725 Send logs in an embed 2022-09-22 16:58:48 +02:00
stijndcl d9272f17ab Create command to monitor task execution 2022-09-22 16:49:33 +02:00
stijndcl 7f21a1cf69 Defer faster, no longer error for courses than we don't know of 2022-09-22 16:38:23 +02:00
Stijn De Clercq dc1f0f6b55
Merge pull request #133 from stijndcl/covid
Rewrite Covid
2022-09-22 16:34:39 +02:00
stijndcl 2d0babbdcb Create covid command 2022-09-22 16:30:58 +02:00
stijndcl df884f55f1 Covid api requests 2022-09-22 02:04:34 +02:00
stijndcl abbb3026eb Run hooks 2022-09-22 01:18:40 +02:00
stijndcl 72415aeed0 Snipe 2022-09-22 01:11:24 +02:00
stijndcl 87caeec47b Change postgres defaults to match compose file 2022-09-20 22:50:58 +00:00
stijndcl 97e815cbff Adding & removing github links 2022-09-20 17:55:59 +02:00
stijndcl 9e3527ae8a Fix relationship, add github links, improve error messages 2022-09-20 17:34:49 +02:00
stijndcl 41c8c9d0ab Put tracebacks in a codeblock for readability & to escape markdown 2022-09-20 16:45:02 +02:00
stijndcl 23edc51dbf Command stats 2022-09-20 14:47:26 +02:00
stijndcl 7517f844d8 More easter eggs 2022-09-20 01:40:05 +02:00
stijndcl 1fe04b3687 Use importlib for db scripts 2022-09-20 01:07:36 +02:00
stijndcl 181118aa1d Easter eggs 2022-09-20 00:31:33 +02:00
stijndcl 2638c5a3c4 Inspire 2022-09-19 23:04:20 +02:00
Stijn De Clercq a23ee3671a
Merge pull request #132 from stijndcl/help-v3
Rewrite help
2022-09-19 19:00:04 +02:00
stijndcl 6ef4007f13 Small bugfix 2022-09-19 18:54:25 +02:00
stijndcl bef8742459 Add flags to help pages 2022-09-19 18:50:03 +02:00
stijndcl 5511046e35 Final docstrings 2022-09-19 18:15:10 +02:00
stijndcl 5d2d7c49c2 Hide empty cogs 2022-09-19 17:50:33 +02:00
stijndcl 7035f0773f Help messages for Fun 2022-09-19 17:42:51 +02:00
stijndcl 5cdb6c3f44 More docstrings 2022-09-19 17:23:37 +02:00
stijndcl da365e3bc1 Don't split code in codeblocks 2022-09-19 15:59:12 +02:00
stijndcl 65e1034372 Generate help embed from function signature 2022-09-19 14:29:14 +02:00
stijndcl 9c36f59e04 Improve help messages 2022-09-19 01:50:18 +02:00
stijndcl c5317b5d27 Create help command implementation 2022-09-19 01:28:18 +02:00
Stijn De Clercq bf28611ddc
Merge pull request #131 from stijndcl/schedules
Re-write schedules to be auto-generated
2022-09-18 17:50:38 +02:00
stijndcl 06b8c4e084 Fix migration & broken test 2022-09-18 17:45:37 +02:00
stijndcl 308c341b1a Fix env variable 2022-09-18 17:29:24 +02:00
stijndcl c33ee82539 Add more settings for schedules 2022-09-18 17:25:39 +02:00
stijndcl 65201cd705 Rename script to have alphabetical name ordering 2022-09-18 01:21:51 +02:00
stijndcl 0262d68fc9 Improve formatting 2022-09-18 01:18:35 +02:00
stijndcl 13f7d03bbb Sending schedules in discord, small fixes 2022-09-18 01:16:19 +02:00
stijndcl 14ccb42424 Parsing of schedules 2022-09-17 23:20:46 +02:00
stijndcl 8fea65e4ad Merge migrations, pull schedules daily 2022-09-17 19:22:27 +02:00
stijndcl ac24688a73 Increase coverage a bit 2022-09-03 01:26:32 +02:00
stijndcl 88bbb9773f Only allow author to stop menu pages 2022-09-02 01:27:17 +02:00
Stijn De Clercq f41e16796d
Merge pull request #130 from stijndcl/bookmarking
Bookmarking & fix menu bugs
2022-09-01 01:37:53 +02:00
stijndcl 149d132e6d Fix typing 2022-09-01 01:26:46 +02:00
stijndcl 152f84ed1c Deleting bookmarks, fix bug in menus 2022-09-01 01:02:18 +02:00
stijndcl f70736b4d5 Searching for bookmarks 2022-08-30 01:55:40 +02:00
stijndcl 12d2017cbe Creating bookmarks + message command 2022-08-30 01:32:46 +02:00
Stijn De Clercq 8308b4ad9a
Merge pull request #129 from stijndcl/remove-mongo
Remove Mongo again
2022-08-29 21:30:40 +02:00
stijndcl a51da649db Remove mongo from CI 2022-08-29 21:20:15 +02:00
stijndcl 29f83c1343 Add missing await 2022-08-29 21:16:31 +02:00
stijndcl 000fa93180 Various small fixes all around 2022-08-29 21:13:09 +02:00
stijndcl c4ef5cd619 Fix incorrect typehint 2022-08-29 20:50:51 +02:00
stijndcl 73d44de46e Fix wordle code 2022-08-29 20:49:29 +02:00
stijndcl 8a4baf6bb8 Remove mongo & fix tests 2022-08-29 20:24:42 +02:00
stijndcl 7b2109fb07 Add mypy to precommit 2022-08-29 18:33:44 +02:00
Stijn De Clercq b2bc497c50
Merge pull request #128 from stijndcl/menus
Create custom pagination module
2022-08-29 18:30:53 +02:00
stijndcl d245b2195f Fix typing 2022-08-29 18:26:30 +02:00
stijndcl 5d400fdcac Create custom pagination module 2022-08-29 16:21:47 +02:00
stijndcl dc2262b246 Start & cancel tasks in cog_[un]load instead 2022-08-29 14:21:21 +02:00
stijndcl 1752d651a9 Undo last change 2022-08-29 02:26:16 +02:00
stijndcl 994ff01de1 Allow specifying return type for http methods 2022-08-29 02:04:42 +02:00
stijndcl 14e0472954 Make custom colour for Google embeds 2022-08-29 02:01:05 +02:00
stijndcl ea6b204cf0 Add tests & fixes for date parsing 2022-08-29 01:51:59 +02:00
stijndcl b85f5a612a Catch missing fields in menu 2022-08-29 01:30:58 +02:00
stijndcl 9225f61e47 Fix typing 2022-08-29 01:22:05 +02:00
stijndcl c31767c25f Add OS info to dev readme 2022-08-29 01:17:06 +02:00
stijndcl 2de75fd168 Use transformer 2022-08-29 00:02:06 +02:00
stijndcl e1af53cf44 Create a transformer for dates 2022-08-28 23:33:52 +02:00
stijndcl b581c3e5dc Menu message & slash commands 2022-08-28 22:44:53 +02:00
stijndcl 0186a0793a fix typing issues 2022-08-28 22:15:03 +02:00
stijndcl 654fbcd46b Start working on food, make embeds prefer title instead of author, add custom colour for ghent university blue 2022-08-28 20:16:36 +02:00
stijndcl 6f0ac487cc Handle custom exceptions 2022-08-26 22:55:42 +02:00
stijndcl 8a42e24c34 Make meme preview ephemeral 2022-08-26 21:50:13 +02:00
Stijn De Clercq 773c7ac4d1
Merge pull request #127 from stijndcl/memegen
Memegen
2022-08-26 21:48:10 +02:00
stijndcl 966eb63165 broken type 2022-08-26 21:43:32 +02:00
stijndcl f9083e84ed Meme preview slash command 2022-08-26 21:35:49 +02:00
stijndcl a0c1b986cd Make fancy functions for database & http stuff, meme preview 2022-08-26 20:02:54 +02:00
stijndcl d1d10ee853 Fix naming & missing argument 2022-08-26 18:51:34 +02:00
stijndcl 8fb990cea8 Add missing translations, memegen message command 2022-08-26 18:32:53 +02:00
stijndcl 7d7ab98254 Memegen works 2022-08-25 11:04:25 +02:00
stijndcl dbb570420b Command to add memes 2022-08-25 02:07:02 +02:00
stijndcl 86dd6cb27b Move Pin over to /Discord instead of /School 2022-08-24 21:16:27 +02:00
stijndcl 2f40903579 Update to 2.0 release of dpy 2022-08-22 00:18:40 +02:00
stijndcl b26421b875 Fix bug, add autocomplete, make cache autocompletion slightly cleaner 2022-08-13 01:10:50 +02:00
stijndcl e2959c27ad Adding new deadlines 2022-08-13 00:41:47 +02:00
stijndcl 107e4fb580 Displaying deadlines 2022-08-13 00:07:48 +02:00
Stijn De Clercq a510e2fe4a
Merge pull request #125 from stijndcl/links
Rework Links
2022-08-12 21:25:37 +02:00
stijndcl 28cf094ea3 Typing 2022-08-10 01:12:28 +02:00
stijndcl a614e9a9f1 Rework links 2022-08-10 01:04:19 +02:00
stijndcl 94de47082b Add marco 2022-08-09 23:46:41 +02:00
Stijn De Clercq 8aedde46de
Merge pull request #124 from stijndcl/wordle
Wordle
2022-08-03 16:40:25 +02:00
stijndcl e36322b4a7 Add missing await 2022-08-03 16:36:24 +02:00
stijndcl 8dbf68cac0 Don't count testing code as coverage 2022-08-03 16:27:33 +02:00
stijndcl b74f794639 First tests for game stats 2022-07-30 18:27:58 +02:00
stijndcl bf41acd9f4 Make game stats crud functions, split mongo schemas out a bit 2022-07-30 17:50:09 +02:00
stijndcl e4e77502e8 Test all crud stuff up until now 2022-07-30 16:14:32 +02:00
stijndcl a0781a046b Add more tests 2022-07-30 00:24:03 +02:00
stijndcl c4c9461ca3 Fix typing 2022-07-27 22:08:00 +02:00
stijndcl d4dae7826d Require no auth for mongo in tests 2022-07-27 22:02:59 +02:00
stijndcl 5f9a57cd83 Try to fix actions, fix broken test 2022-07-27 21:49:58 +02:00
stijndcl bdaf8a1dc5 Add kwargs to embeds, hide wordle spoilers 2022-07-27 21:32:26 +02:00
stijndcl 4a137bcad8 Allow force-resetting the game 2022-07-27 21:25:07 +02:00
stijndcl db499f3742 WORDLE 2022-07-27 21:10:43 +02:00
stijndcl ea4181eac0 Fix broken time formatting, remove word field 2022-07-26 21:48:50 +02:00
stijndcl cbd3030565 Create initial wordle methods 2022-07-25 22:58:02 +02:00
Stijn De Clercq 2f4c2c347f
Merge pull request #123 from stijndcl/add-mongo
Add MongoDB
2022-07-25 21:32:32 +02:00
stijndcl 9da0bc2c5a Make mypy ignore motor 2022-07-25 21:26:43 +02:00
stijndcl e6b4c3fd76 Create base model for Mongo schemas 2022-07-25 21:20:09 +02:00
stijndcl 6bebd109bb Set up mongo connection in pytest & fix authentication url 2022-07-25 21:08:06 +02:00
stijndcl 52b452c85a Fix mongo connection 2022-07-25 20:33:20 +02:00
stijndcl 9abe5dd519 Rename database fixture 2022-07-25 19:12:27 +02:00
stijndcl 665d677941 Modify config files to support Mongo 2022-07-25 19:07:57 +02:00
stijndcl b54aed24e8 Log errors in Discord channels 2022-07-24 21:35:38 +02:00
stijndcl 2e3b4823d0 Update sync 2022-07-24 18:30:03 +02:00
stijndcl 424399b88a Translate to english 2022-07-24 17:09:42 +02:00
stijndcl edc6343e12 Create owner-guild-only commands, make sync a bit fancier 2022-07-24 16:39:27 +02:00
stijndcl 0834a4ccbc Write tests for tasks 2022-07-24 01:49:52 +02:00
stijndcl da0e60ac4f Send daily birthday notifications, add more settings & configs, fix small bugs in database 2022-07-23 23:21:32 +02:00
stijndcl 393cc9c891 Add support for lazy loading of user fields 2022-07-23 22:34:03 +02:00
stijndcl 66997b7556 Fix broken migration 2022-07-23 20:59:02 +02:00
stijndcl 8bc0f1fa7a Add birthday task, change migrations to use date instead of datetime 2022-07-23 20:35:42 +02:00
stijndcl adcf94c66e Tests for birthday commands, overwrite existing birthdays 2022-07-19 23:35:41 +02:00
stijndcl f49f32d2e9 Add birthday commands 2022-07-19 22:58:59 +02:00
Stijn De Clercq 016d87bcea
Merge pull request #121 from stijndcl/pre-commit
Add pre-commit hooks
2022-07-19 21:39:39 +02:00
stijndcl bb903fdad5 Update readme & config files 2022-07-19 21:33:18 +02:00
stijndcl e371e2cc5c Add pre-commit config, make tests a requirement for quality checks 2022-07-19 21:12:04 +02:00
Stijn De Clercq 3057222607
Merge pull request #120 from stijndcl/pytest-migrations
Small cleanup in migrations fix
2022-07-19 19:00:57 +02:00
stijndcl 8bd4495016 Small cleanup 2022-07-19 18:57:24 +02:00
Stijn De Clercq 6c225bacc1
Merge pull request #119 from stijndcl/pytest-migrations
Use migrations in tests
2022-07-19 18:53:07 +02:00
stijndcl 9401111bee Try to use migrations in tests 2022-07-19 18:49:22 +02:00
stijndcl f4056d8af6 Fix typing & linting 2022-07-18 23:39:14 +02:00
stijndcl c9dd275860 Fix failing test 2022-07-18 23:23:53 +02:00
stijndcl 0c810d84e9 Google search 2022-07-18 23:22:28 +02:00
stijndcl 1aeaa71ef8 Add extra tests 2022-07-18 22:00:39 +02:00
stijndcl 8227190a8d Fix mypy & tests 2022-07-16 00:19:05 +02:00
stijndcl 3debd18d82 Add dad jokes 2022-07-16 00:14:02 +02:00
stijndcl 3d0f771f94 Load year from settings 2022-07-15 23:14:56 +02:00
stijndcl 5b47397f29 Add study guide commands, get auto-completion for full course names based on aliases 2022-07-15 23:06:40 +02:00
stijndcl 72c3acbcc2 Add simple caching implementation for database queries that will be used in command autocompletion 2022-07-14 22:44:22 +02:00
stijndcl f0a05c8b4d Wrap result in a list 2022-07-14 22:03:56 +02:00
stijndcl 84bf1d7a26 Add crud functions to get ufora courses 2022-07-14 22:03:30 +02:00
stijndcl c8392342a6 Urban dictionary commands 2022-07-14 20:28:45 +02:00
stijndcl b9c5c6ab10 Add flake8 docstring plugin, formatting, create base class for embeds & models 2022-07-13 22:54:16 +02:00
stijndcl 8d6dbe1c94 remove pre-commit 2022-07-11 22:42:11 +02:00
stijndcl 04e2889d93 Add black pre-commit 2022-07-11 22:35:38 +02:00
stijndcl 81c882315a Add black pre-commit 2022-07-11 22:35:04 +02:00
stijndcl dd66087193 Switch to flake8 2022-07-11 22:23:38 +02:00
stijndcl 61128dda92 Write some tests for currency crud 2022-07-03 19:26:30 +02:00
stijndcl 8da0eb0b2a Investing 2022-07-03 18:35:30 +02:00
stijndcl fff35c6c44 Bank upgrades 2022-07-03 17:44:16 +02:00
stijndcl ba86d4a6f2 Make announcements async, work on bank upgrades 2022-07-03 17:19:24 +02:00
stijndcl 5b510d1f45 Command to find source code on github 2022-07-02 00:34:12 +02:00
Stijn De Clercq cb0b4a419e
Merge pull request #118 from stijndcl/currency-v3
Start of currency v3
2022-07-02 00:00:21 +02:00
Stijn De Clercq c7210152c6
Merge pull request #117 from stijndcl/help-v3
Setup of Help v3
2022-07-01 23:59:40 +02:00
stijndcl f33aed0ceb Send cog help 2022-07-01 23:58:39 +02:00
stijndcl ef493bb8d2 Start working on help 2022-07-01 23:13:18 +02:00
stijndcl c294bc8da5 Use abbreviated numbers in award 2022-07-01 16:06:12 +02:00
stijndcl 96916d2abd Re-create & test number converter 2022-07-01 16:06:12 +02:00
stijndcl fd72bb1774 Typing 2022-07-01 16:06:12 +02:00
stijndcl bd63f80a7d Editing custom commands 2022-07-01 16:06:12 +02:00
stijndcl bec893bd20 Add tests for users crud 2022-07-01 16:06:12 +02:00
stijndcl 032b636b02 Nightly, bank, award & dinks 2022-07-01 16:06:12 +02:00
stijndcl 4587a49311 Create database models 2022-07-01 16:06:12 +02:00
stijndcl 9552c38a70 Fix typo in toml file 2022-07-01 16:06:03 +02:00
Stijn De Clercq 76f1ba3543
Merge pull request #115 from stijndcl/codecov
Add CodeCov & increase coverage
2022-07-01 16:00:55 +02:00
stijndcl c95b7ed58f Remove discord stuff from tests 2022-07-01 15:59:33 +02:00
stijndcl 27d074d760 Increase coverage 2022-07-01 15:46:56 +02:00
stijndcl 9d04d62b1c Add codecov 2022-07-01 14:25:15 +02:00
stijndcl 60382b8eab Give up on migrations in tests 2022-06-30 15:20:54 +02:00
stijndcl b4a3a87e6e Use alembic in tests again? 2022-06-29 00:25:51 +02:00
stijndcl 5c1732d119 Fix env 2022-06-29 00:20:50 +02:00
stijndcl 5f2e26f154 Add test container 2022-06-29 00:14:44 +02:00
stijndcl ca9dd84ab5 stash 2022-06-27 19:53:44 +02:00
stijndcl d6a560851b Editing of custom commands, add posix flags 2022-06-25 01:57:52 +02:00
stijndcl 257eae6fa7 Remove pytest-alembic 2022-06-23 12:09:54 +02:00
stijndcl ee03cf7d8c Try to fix tables fixture 2022-06-23 12:04:29 +02:00
stijndcl 36909f04bd Use alembic test package 2022-06-23 11:56:25 +02:00
stijndcl 85a7750d09 No longer run migrations in tests 2022-06-23 11:53:21 +02:00
stijndcl 6b91e792e6 Use event loop fixture again 2022-06-23 11:38:58 +02:00
stijndcl cc8f8b1ee4 Try to fix tests 2022-06-23 11:24:12 +02:00
stijndcl 3ce823f209 Stash 2022-06-23 11:09:43 +02:00
stijndcl add9399944 Fix linting & typing 2022-06-22 02:09:16 +02:00
stijndcl 57e805e31c Try to fix async tests 2022-06-22 02:05:04 +02:00
stijndcl fc195e40b3 Fix syncing 2022-06-22 01:56:13 +02:00
stijndcl d8192cfa0a Adding custom commands & aliases 2022-06-22 00:49:00 +02:00
stijndcl efdc966611 Invoke custom commands 2022-06-22 00:22:26 +02:00
stijndcl fd57b5a79b Crud & tests for custom commands 2022-06-21 23:58:21 +02:00
stijndcl 53f58eb743 Write a few tests 2022-06-21 21:06:11 +02:00
stijndcl 5c2c62c6c4 Add sync command, clean up db sessions 2022-06-21 20:30:11 +02:00
stijndcl 868cd392c3 Fix mypy error 2022-06-21 18:58:33 +02:00
stijndcl 5a76cbd2ec Fix mypy error 2022-06-21 18:50:00 +02:00
stijndcl 000337107b Parse publication time of notifications 2022-06-21 18:44:47 +02:00
stijndcl d75831f848 Create task to purge old announcements 2022-06-19 00:51:24 +02:00
stijndcl d7262595c6 Store announcement date in db 2022-06-19 00:36:38 +02:00
stijndcl 6873cab955 Ufora announcements 2022-06-19 00:23:25 +02:00
stijndcl bacd2d77fb Fix typing 2022-06-17 01:51:06 +02:00
stijndcl eb71470edc Create ufora-related revision 2022-06-17 01:36:47 +02:00
stijndcl b23160b8e2 Add debug print 2022-06-17 01:14:22 +02:00
stijndcl eb182b71f4 Create connection fixture 2022-06-17 01:02:36 +02:00
stijndcl 53a3e0e75a Fix env variable 2022-06-17 00:54:00 +02:00
stijndcl 21aeb80c13 Add default token 2022-06-17 00:50:00 +02:00
stijndcl 9193e73af9 Add password 2022-06-17 00:47:10 +02:00
stijndcl 00e805d535 Fiddle with action 2022-06-17 00:43:55 +02:00
stijndcl 6d61056dc4 Add missing alembic file 2022-06-16 00:40:37 +02:00
stijndcl a1449a4c9c Linting 2022-06-16 00:34:27 +02:00
stijndcl 3d1aabf77c Change action to run everything in parallel with testing 2022-06-16 00:31:34 +02:00
stijndcl de0d543bf8 Fix bug in prefix, pin command, pin context menu 2022-06-16 00:29:38 +02:00
stijndcl 304ad850b7 Try new actions 2022-06-15 02:04:34 +02:00
stijndcl e09ec5c946 Loading cogs 2022-06-15 01:56:18 +02:00
stijndcl 37dd5ba3e8 Check current migration on startup 2022-06-11 01:26:19 +02:00
stijndcl 9518bbe168 Add db through docker, configure alembic & fix pylint warning 2022-06-11 01:15:05 +02:00
stijndcl 3a35718d84 Initialize database stuff & setup didier 2022-06-10 01:48:02 +02:00
stijndcl 0701fbfddb Configure tools & add initial dependencies 2022-06-09 01:44:53 +02:00
stijndcl 00481e42eb Project setup for clean rewrite 2022-06-09 01:32:13 +02:00
Stijn De Clercq 2ce32ee8a3
Disable freegames check cause epicgames is terrible 2022-05-26 17:20:56 +02:00
Stijn De Clercq 90dec2f5e4
Disable schedule reminders 2022-05-19 22:05:50 +02:00
Stijn De Clercq d64e08671b
Add compbio report 2022-04-29 11:19:02 +02:00
Stijn De Clercq 06af3ada48
Add compbio project 2022-04-24 19:49:56 +02:00
Stijn De Clercq b3ce348193
Re-enable les reminders & make abc online 2022-04-18 17:33:31 +02:00
Stijn De Clercq 09d282db19
Set compbio lb to ms instead of s 2022-04-12 11:34:44 +02:00
Stijn De Clercq f56cc40c41
Disable schedule reminder 2022-04-05 15:35:24 +02:00
Stijn De Clercq 002665910a
actually fix deadline 2022-04-05 15:22:36 +02:00
Stijn De Clercq 0c1b6f335c
fix deadline 2022-04-05 15:19:36 +02:00
Stijn De Clercq eb3a3fd7fa
Compbio 4 2022-04-01 09:36:23 +02:00
stijndcl 97d533e04f Fix url 2022-03-31 20:21:34 +02:00
stijndcl 8e050f9ab3 Update compbio leaderboard to assignment 4 2022-03-31 20:12:44 +02:00
stijndcl 0151913e61 Update compbio leaderboard to assignment 4 2022-03-31 20:12:18 +02:00
stijndcl b156f90ea0 fixes #109 2022-03-31 20:03:00 +02:00
Stijn De Clercq 82671896b6
Add wetrek project 2022-03-31 09:07:41 +02:00
Stijn De Clercq 868869beff
Cancel prolog 2022-03-21 17:13:34 +01:00
Stijn De Clercq 98dddf992e
Add sel peer reviews 2022-03-21 15:36:03 +01:00
Stijn De Clercq ffa594de75
Add compbio 3 2022-03-17 18:04:22 +01:00
stijndcl a2ab822803 Check for empty messages in edit snipe 2022-03-15 18:45:58 +01:00
stijndcl 4b003e150b Enable message content intent 2022-03-15 17:04:45 +01:00
stijndcl 9341554040 Ignore spammy error that i can't do anything about 2022-03-13 13:32:54 +01:00
stijndcl 0f973efe29 Add optional benchmark to compbio lb 2022-03-12 21:03:01 +01:00
stijndcl 3f2f9cd8f3 Fix typo in compbio leaderboard, defer compbio slash commands 2022-03-12 10:31:41 +01:00
Stijn De Clercq 635560dcd0
Merge pull request #108 from stijndcl/compbio_commands
Add commands to track compbio leaderboards
2022-03-11 14:17:12 +01:00
stijndcl 982a4bb056 Remove unnecessary variables 2022-03-11 14:16:02 +01:00
stijndcl c43710a429 Add commands to track compbio leaderboards 2022-03-11 14:11:59 +01:00
stijndcl d18860cae0 remove mask 2022-03-11 09:38:32 +01:00
Stijn De Clercq 2c8c20e9f2
Add prolog project 2022-03-10 09:40:08 +01:00
Stijn De Clercq 59fd59308d
Add infosec deadlines 2022-03-07 14:04:54 +01:00
Stijn De Clercq 9fc6c783ab
Add new compbio deadline 2022-03-04 10:28:16 +01:00
Stijn De Clercq 51f1e44537
Change ABC link to opencast 2022-02-21 21:50:13 +01:00
Stijn De Clercq b52d5a263b
Add sel milestone deadlines 2022-02-21 15:36:05 +01:00
Stijn De Clercq 2a0d7d45aa
Mark classes as on-campus again 2022-02-20 21:22:50 +01:00
Stijn De Clercq d2b0e630ec
Fix typo in schedule 2022-02-20 17:39:40 +01:00
Stijn De Clercq e97b4fa388
Add compbio deadline 2022-02-18 11:59:57 +00:00
Stijn De Clercq e6e811fc23
Update hydra api url 2022-02-17 10:16:13 +01:00
Stijn De Clercq 959ca627da Fix paginator send 2022-02-15 17:05:51 +01:00
Stijn De Clercq 5cccf81c59 Make compbio online 2022-02-15 13:11:13 +01:00
Stijn De Clercq 226570d5d0 Enable schedule reminders 2022-02-15 13:03:42 +01:00
Stijn De Clercq 7ff133a045 Cancel next sel2 2022-02-14 18:07:27 +01:00
Stijn De Clercq 543b5199b6 Fix changes in fork 2022-02-14 16:40:42 +01:00
Stijn De Clercq 585134a3fb Allow commands in dangerzone 2022-02-14 16:10:04 +01:00
Stijn De Clercq 781956b312 Make ABC online only 2022-02-12 15:41:35 +01:00
Stijn De Clercq eafc1a8674
Merge pull request #105 from stijndcl/pycord-port
Port to Pycord & add newly supported features
2022-02-11 23:21:56 +01:00
Stijn De Clercq b1fdd22058 Reply to original message if command invocation was a reply 2022-02-11 23:08:28 +01:00
Stijn De Clercq 0165700d9f Add slash command to send links, add normal commands for study guides & links 2022-02-11 22:54:40 +01:00
Stijn De Clercq 9819e82638 Add tests for courses, change study guide description 2022-02-11 22:26:23 +01:00
Stijn De Clercq c6958d22f3 Add command to generate course guides, add autocompletion for some commands, create json file with course information & abbreviations 2022-02-11 22:11:20 +01:00
Stijn De Clercq 6d7b47fee0
Merge pull request #104 from stijndcl/32_schedule
32 schedule
2022-02-11 21:02:00 +01:00
Stijn De Clercq e78e13e26a Add schedule for 2nd semester 2022-02-11 21:00:31 +01:00
Stijn De Clercq a734191973 Add command to join threads that Didier is not in yet 2022-02-10 19:55:04 +01:00
Stijn De Clercq 9a999fb34b Automatically join threads, add placeholder code for pin context menu 2022-02-10 19:34:40 +01:00
Stijn De Clercq 93ede132a2 Memegen slash commands + autocompletion 2022-02-06 17:58:57 +01:00
Stijn De Clercq a71232e292 Custom commands cleanup, remove train for my own sanity 2022-02-06 01:34:00 +01:00
Stijn De Clercq 81a0d90a12 Leaderboard cleanup 2022-02-06 01:11:40 +01:00
Stijn De Clercq ca687956f6 Rework last leaderboards 2022-02-05 23:21:47 +01:00
Stijn De Clercq 062d54722b Rework bc, rob & poke leaderboards 2022-02-05 22:55:09 +01:00
Stijn De Clercq 7ad2bf351e Remove broken libraries & functionality, format slash command usage 2022-02-05 21:24:08 +01:00
Stijn De Clercq 06dc3d3fb9 Create updated leaderboards 2022-02-05 19:36:25 +01:00
Stijn De Clercq 829729c8db Disable IPC 2022-02-05 14:43:19 +01:00
Stijn De Clercq eaed08168c Inspire slash command, defer jpl table 2022-02-05 14:33:11 +01:00
Stijn De Clercq 9eb0eb5a61 Change order of requirements in a desperate attempt that it would matter at all 2022-02-03 02:02:58 +01:00
Stijn De Clercq bca4fbf616 Add X Is X meme, small cleanups & style fixes 2022-02-03 02:01:09 +01:00
Stijn De Clercq 17964a23fb Fix bug in help pages because of slash commands 2022-02-03 01:52:26 +01:00
Stijn De Clercq 853b708ece Fix broken test import paths 2022-02-03 01:49:00 +01:00
Stijn De Clercq 3444414638 Port slash commands to pc 2022-02-03 01:43:54 +01:00
Stijn De Clercq e3a788f6c9 Escape asterisks in ufora notifications 2022-01-21 20:33:59 +01:00
Stijn De Clercq 8b99835f81
Merge pull request #103 from stijndcl/xkcd
Add xkcd command
2022-01-16 20:14:56 +01:00
Stijn De Clercq bf272f17c4 Add xkcd command 2022-01-16 19:55:07 +01:00
Stijn De Clercq 3c5221f32e
Merge pull request #102 from stijndcl/pin_ref
Pin ref
2022-01-05 19:39:25 +01:00
Stijn De Clercq 16a4ba0e83 Update help 2022-01-05 19:37:59 +01:00
Stijn De Clercq ea18dea411 Allow pins to use replies instead of arguments 2022-01-05 19:36:47 +01:00
Stijn De Clercq e130bf4a25 Disable schedule reminder 2021-12-29 13:56:26 +01:00
Stijn De Clercq f5e62a0fb9 Empty deadlines, fix error, add easter egg 2021-12-26 00:30:14 +01:00
Stijn De Clercq c154a4bb2a
Update deadlines.json 2021-12-24 13:31:40 +01:00
Stijn De Clercq 20eb37cf6c
Update deadlines.json 2021-12-24 13:30:15 +01:00
Stijn De Clercq 8b8aca1b2c
Update fp3 deadline 2021-12-16 11:16:35 +01:00
Stijn De Clercq b1d8747a8d update bs schedule 2021-12-15 21:32:30 +01:00
Stijn De Clercq 018268ee1f Fix typo 2021-12-02 13:35:55 +01:00
Stijn De Clercq 046b67a36e Make deadlines public 2021-12-02 13:33:49 +01:00
Stijn De Clercq 8350b01d85 Fix incorrect timeformatting 2021-11-29 22:12:53 +01:00
Stijn De Clercq dd60840576 Fix embed sorting & inlining 2021-11-29 22:06:02 +01:00
Stijn De Clercq 81bdde9632
Merge pull request #100 from stijndcl/new_commands
Command for deadlines, slash command for schedules
2021-11-29 21:55:32 +01:00
Stijn De Clercq 13df55ac53 Catch keyerror for corona vaccines 2021-11-29 21:55:07 +01:00
Stijn De Clercq c84cb3f944 Add slash command for schedule & fix broken schedule 2021-11-29 21:48:30 +01:00
Stijn De Clercq fccf4efa1f Add commands for deadlines 2021-11-29 21:37:04 +01:00
Stijn De Clercq 693fab7833 Fix typo 2021-11-28 16:30:37 +01:00
Stijn De Clercq 7bac54599a Update schedules & add some QOL & small fixes 2021-11-28 16:28:31 +01:00
Stijn De Clercq 9a2313d376 Send a head instead of get 2021-11-09 23:08:22 +01:00
Stijn De Clercq f052885ae0 Fix food api url, remove resto option 2021-11-05 23:36:12 +01:00
Stijn De Clercq 3c52d681b3 Fix casing 2021-11-02 12:57:41 +01:00
Stijn De Clercq 9f6b36e4e8 Change location for bs 2021-11-02 12:57:18 +01:00
Stijn De Clercq 570c1e4d1b Change current date time to 2 am to always be ahead of the base day 2021-11-01 00:28:02 +01:00
Stijn De Clercq 96cf11dcaf Update schedule 2021-10-31 23:59:40 +01:00
Stijn De Clercq b26eeb3fc8 Update schedule 2021-10-20 20:12:50 +02:00
Stijn De Clercq 89a5f50a1d Fix jpl table formatting 2021-10-18 18:39:02 +02:00
Stijn De Clercq 0f6a52c75d fixes #97 2021-10-04 20:15:01 +02:00
Stijn De Clercq 49c5110f8f change modsim room 2021-10-03 21:47:43 +02:00
Stijn De Clercq 5f51acd1ff Fix schedule room 2021-09-30 14:09:04 +02:00
Stijn De Clercq 6fb072ef58
Merge pull request #96 from MaartenS11/ad3-room-fix
Fixed AD3 room to be "Leslokaal 1.1" instead of "1.1 Alan Turing"
2021-09-29 09:16:55 +02:00
Maarten Steevens b36c6e57fb Fixed room to be "Leslokaal 1.1" instead of "1.1 Alan Turing" 2021-09-29 08:31:16 +02:00
Stijn De Clercq 287e2fd233 Schedule change, add warning triangle 2021-09-27 19:11:11 +02:00
Stijn De Clercq 2a8500e870
Merge pull request #95 from stijndcl/food_restos
Menu Slash Command
2021-09-26 22:13:58 +02:00
Stijn De Clercq 73cc6aa0e9 Add slash command, support multiple restos & add date to embed 2021-09-26 22:12:52 +02:00
Stijn De Clercq aae5616488
Merge pull request #93 from stijndcl/les_minors
Les minors
2021-09-26 21:29:54 +02:00
Stijn De Clercq f842045511 Customize les embed if dm 2021-09-26 21:27:52 +02:00
Stijn De Clercq 04d03fa2cb Customize les reminder per minor 2021-09-26 21:24:34 +02:00
Stijn De Clercq 1ee9900891 Fix food 2021-09-26 13:10:57 +02:00
Stijn De Clercq 7ab72aabfd
Merge pull request #92 from stijndcl/schedules_minors
Fix typo in notifications task
2021-09-22 21:50:37 +02:00
Stijn De Clercq 11065637eb Fix typo in notifications task 2021-09-22 21:49:45 +02:00
Stijn De Clercq 298a1e13d6
Merge pull request #91 from stijndcl/schedules_minors
Schedules minors, update Ufora id's
2021-09-20 19:09:35 +02:00
Stijn De Clercq 90dff3aa83 Add more ufora urls 2021-09-20 19:05:06 +02:00
Stijn De Clercq 43bce8d8fb Parse minors 2021-09-20 19:03:06 +02:00
Stijn De Clercq a48f15d464 Add burgie 2021-09-20 18:21:25 +02:00
Stijn De Clercq 75adf849f0 Add updated ufora RSS feeds 2021-09-20 01:58:50 +02:00
Stijn De Clercq 51811dd9f9 fix ba3 schedule 2021-09-20 01:42:13 +02:00
Stijn De Clercq ad1175f6cd Default test guilds to None instead of empty list 2021-09-05 16:25:15 +02:00
Stijn De Clercq 1c82bf959b Fix test guild parsing for empty strings 2021-09-05 16:22:28 +02:00
Stijn De Clercq 5e946ed5d3
Merge pull request #90 from stijndcl/slash_commands
First slash commands & small fixes/cleanups
2021-09-05 16:13:17 +02:00
Stijn De Clercq 2f5e5147dc Don't change status in on_ready 2021-09-05 16:09:10 +02:00
Stijn De Clercq 768f43ade9 Add some basic tests for urban dictionary 2021-09-05 15:56:48 +02:00
Stijn De Clercq 740ec40ace Use discord timestamps for whois diff 2021-09-04 12:47:33 +02:00
Stijn De Clercq 2e51af6f1c Add football slash commands, clean up some ugly stuff 2021-09-03 21:18:29 +02:00
Stijn De Clercq 0ec321a51b Make error handler use prints for ALL sandboxing people, log slash command errors in terminal 2021-09-03 20:47:25 +02:00
Stijn De Clercq a28bd116f0 Enable commands globally if not sandboxing, add support for test guilds in env, monitor slash command & context menu usage, create error handler for slash commands, log slash commands in terminal 2021-09-03 20:40:03 +02:00
Stijn De Clercq ef547a7090 Translate slash command 2021-09-03 18:53:05 +02:00
Stijn De Clercq 358f8693dd Clean up define, create define slash command 2021-09-03 18:26:26 +02:00
Stijn De Clercq c47f908e57 Show search result in google embed 2021-09-03 18:05:23 +02:00
Stijn De Clercq 831459a321 Separate loading slash commands from normal commands, split slash commands to separate cogs, make init_extensions support directories 2021-09-03 17:57:45 +02:00
Stijn De Clercq ed0649c953 Clean google search up a bit 2021-08-31 00:13:27 +02:00
Stijn De Clercq 537111d357 Create a slash command for google search 2021-08-30 23:21:34 +02:00
Stijn De Clercq eb6fc6513c Sync bank interest & daily reminders @ 4 am, as requested by @GyroFalc 2021-08-30 22:03:31 +02:00
Stijn De Clercq 968df71f98
Merge pull request #88 from stijndcl/fix_matches
Add support for postponed matches
2021-08-19 21:12:57 +02:00
Stijn De Clercq b083cfe0bf Add support for postponed matches 2021-08-19 21:12:29 +02:00
Stijn De Clercq b910018ddc
Merge pull request #87 from stijndcl/links
Fix JPL tournament code & links
2021-08-19 20:49:42 +02:00
Stijn De Clercq bf9b9c23b0 Fix matches url 2021-08-19 20:47:53 +02:00
Stijn De Clercq daae31a298 Update jpl code 2021-08-19 20:35:22 +02:00
Stijn De Clercq cbe1cf747f Fix error in food 2021-08-19 19:45:00 +02:00
Stijn De Clercq cdb8a04cde
Merge pull request #86 from stijndcl/ufora_notifications
Fixes for Ufora Notifications
2021-08-11 20:46:28 +02:00
Stijn De Clercq 3b1426b048 Ufora Notifications: fix timestamp (i think), unlimited length, FULL html-to-markdown support (eg. hyperlinks, file download links, ...), clean up some ugly stuff 2021-08-11 20:43:45 +02:00
Stijn De Clercq 9561b98f98
Merge pull request #85 from stijndcl/more_tests
Add test for jpl scraper, fix random import, fix reminders
2021-08-10 12:05:49 +02:00
Stijn De Clercq 1d0782bdc8 Fixed reminder & broken import 2021-08-10 12:02:39 +02:00
Stijn De Clercq 6ddaa6e488 Fix les embed reminder 2021-08-09 00:40:15 +02:00
Stijn De Clercq 02ea90ec6a Test sporza html scraper 2021-08-08 23:28:40 +02:00
Stijn De Clercq 49aaa76aff restructure scrapers, don't run jpl task on dev 2021-08-08 23:24:16 +02:00
Stijn De Clercq e07a2c28d1 Fix sporza matchday scraper 2021-08-08 23:06:59 +02:00
Stijn De Clercq 500da97b8b Disable schedule command cause it's not useful atm 2021-08-08 22:48:13 +02:00
Stijn De Clercq 153aa04490
Merge pull request #84 from stijndcl/les_rework
Les rework + tests
2021-08-08 22:47:01 +02:00
Stijn De Clercq 41a1527e72 Fix typo in test 2021-08-08 22:24:05 +02:00
Stijn De Clercq 07e48e4046 Rename job 2021-08-08 22:23:23 +02:00
Stijn De Clercq aa1e28937c Add extra tests for schedules & timeformatters 2021-08-08 22:22:11 +02:00
Stijn De Clercq a97a35a13b Fix job name 2021-08-08 20:43:05 +02:00
Stijn De Clercq 76df656128 run tests on commit 2021-08-08 20:40:50 +02:00
Stijn De Clercq 445ca84834 Write some tests, fix imports 2021-08-08 20:26:53 +02:00
Stijn De Clercq 85f29e7afa Check for semester end 2021-08-08 19:32:41 +02:00
Stijn De Clercq e9ea063876 Show custom embed for weekends 2021-08-08 18:33:32 +02:00
Stijn De Clercq a198a83153 Clean up imports 2021-08-08 18:24:53 +02:00
Stijn De Clercq 8c6d3682b4 Remove old les code, fix food 2021-08-08 18:24:11 +02:00
Stijn De Clercq 9cebb8280e Special online links 2021-08-08 18:14:56 +02:00
Stijn De Clercq e8301ce8a2 Creating embeds 2021-08-08 18:12:16 +02:00
Stijn De Clercq b3854324d4 Special weeks 2021-08-08 15:18:45 +02:00
Stijn De Clercq 1857bdefe9 Rework schedule a bit to make it more maintainable & easier to code 2021-08-06 23:47:50 +02:00
Stijn De Clercq 54d31c943a Support to show the schedule for a requested day AFTER holidays 2021-08-06 20:52:56 +02:00
Stijn De Clercq ee3ee5284d Work on main Schedule, support for holidays 2021-07-23 23:43:19 +02:00
Stijn De Clercq 49870d23eb Restart rework, for real this time ™️ 2021-07-23 21:03:14 +02:00
Stijn De Clercq ec30228929 check weeks & filter doubles out 2021-07-22 20:28:45 +02:00
Stijn De Clercq cec78f0d5f Schedule implementation 2021-07-22 20:28:45 +02:00
Stijn De Clercq bd3c3d0745 Create enum & base implementation for Courses 2021-07-22 20:28:45 +02:00
Stijn De Clercq 9ce4b0947c Create empty classes for rework, add basic implementation for Slot & Location 2021-07-22 20:28:45 +02:00
Stijn De Clercq b18c15a380
Merge pull request #80 from stijndcl/extra_snipe_tests
Additional snipe tests
2021-07-01 01:18:53 +02:00
Stijn De Clercq 02bd10adcb Additional snipe tests 2021-07-01 01:16:52 +02:00
Stijn De Clercq ab6819d963
Merge pull request #79 from stijndcl/snipe_exceptions
Don't snipe steam codes + tests
2021-06-30 22:11:03 +02:00
Stijn De Clercq 09cad58f43 Don't snipe steam codes, fixes #78
(bot commands can be sniped)
2021-06-30 21:55:17 +02:00
Stijn De Clercq ecb7de062b
Merge pull request #77 from stijndcl/snipe
Snipe
2021-06-22 19:35:30 +02:00
Stijn De Clercq 01266b1346 Snipe, fixes #43 2021-06-22 00:36:22 +02:00
Stijn De Clercq ce1e2e7272
Merge pull request #75 from stijndcl/bump_python_version
Bump python version
2021-06-20 11:51:53 +02:00
Stijn De Clercq f2b4abd4d2 Add env template 2021-06-20 11:50:46 +02:00
Stijn De Clercq 92a82a7175 Update quart & cors versions 2021-06-20 11:48:30 +02:00
Stijn De Clercq ca17a7333b Bump python version 2021-06-20 11:45:34 +02:00
Stijn De Clercq 7bbe4db26d
Merge pull request #74 from stijndcl/list_custom_commands
Show list of custom commands
2021-06-19 23:09:53 +02:00
Stijn De Clercq 7847cc1d29 Show list of custom commands 2021-06-19 23:08:42 +02:00
Stijn De Clercq a310d1696c
Merge pull request #72 from stijndcl/flask-backend
Create basics for backend server, + small fixes
2021-06-19 22:29:32 +02:00
Stijn De Clercq 4b6743a51f Fix requirements.txt (hopefully) & add some info to FAQ, fixes #47 2021-06-19 22:13:41 +02:00
Stijn De Clercq 543eb38417 Fix & re-enable Translate 2021-06-19 22:13:41 +02:00
Stijn De Clercq 26b356b08b Add venv to ignore (PyCharm ignores it automatically) 2021-06-19 22:13:41 +02:00
Stijn De Clercq 7e5c4031c0 Check for non-existing files & init with default values
fixes #63
2021-06-19 22:13:41 +02:00
Stijn De Clercq 418dc41126 Use env vars, fixes #62 2021-06-19 22:13:39 +02:00
Stijn De Clercq a08bfca4c7 Listing custom commands 2021-06-19 22:12:48 +02:00
Stijn De Clercq cedb284adc Fix ping route 2021-06-19 22:12:48 +02:00
Stijn De Clercq b37755f512 Start working on server backend, create main bot class 2021-06-19 22:12:46 +02:00
Stijn De Clercq 34fe8a0feb
Merge pull request #70 from stijndcl/command_stats
Command stats
2021-06-19 17:46:15 +02:00
Stijn De Clercq e56de111eb List total commands per day instead of total usages per command 2021-06-19 17:45:26 +02:00
Stijn De Clercq 119d7c8dca Add command stats call to on_command 2021-06-19 17:05:07 +02:00
Stijn De Clercq 97f6aa105d Add commands file to ignores 2021-06-19 17:03:57 +02:00
Stijn De Clercq 2e21c25361 Add todo 2021-06-15 00:25:11 +02:00
Stijn De Clercq 92a5e6454d Start working on server backend, create main bot class 2021-06-15 00:25:11 +02:00
Stijn De Clercq 0439b634d9
Merge pull request #64 from stijndcl/mention_decorator
Use client user id for poke easter egg
2021-06-15 00:24:36 +02:00
Stijn De Clercq 966b378e09 Use client user id for poke easter egg 2021-06-15 00:12:32 +02:00
Stijn De Clercq 3f5eb6a595 Fix error message in help, fix help not working with mention prefix, fix error when pinning system messages (by adding an easter egg) 2021-06-02 15:52:53 +02:00
Stijn De Clercq 10da82d211 Fix whois to use proper member casting & allow mention prefix 2021-05-30 14:38:56 +02:00
Stijn De Clercq a06fcdefe0
Merge pull request #58 from stijndcl/custom_commands
Custom commands
2021-05-17 20:39:37 +02:00
Stijn De Clercq 92048bcd85 Add unique constraint 2021-05-17 20:19:29 +02:00
Stijn De Clercq 3cd2456bbf Add commands tables to databases.md 2021-05-17 20:16:12 +02:00
Stijn De Clercq 4bcd00826e Adding commands, calling works 2021-05-17 20:10:07 +02:00
Stijn De Clercq 3cfc87b7e1 fetching from db & dataclass 2021-05-17 18:14:33 +02:00
Stijn De Clercq f6d06eb489 Catch discord error, make "all" match uppercase 2021-05-16 12:15:45 +02:00
Stijn De Clercq dc7e982491 Disable les + reminders 2021-05-14 02:08:28 +02:00
Stijn De Clercq 515a4d1105 REMOVE COMPARCH AGAIN 2021-05-09 22:56:31 +02:00
Stijn De Clercq d39b7f8868 Add comparch back in :s 2021-05-02 12:49:34 +02:00
Stijn De Clercq b473ff6e3d
Merge pull request #54 from stijndcl/empty_schedules
Remove comparch schedule
2021-04-25 14:17:33 +02:00
Stijn De Clercq 1ca508f623 Remove comparch schedule 2021-04-25 14:16:08 +02:00
Stijn De Clercq 245a900c87 Fix spaces before percentages 2021-04-24 19:21:24 +02:00
Stijn De Clercq d87af32f6d
Merge pull request #52 from stijndcl/vaccinations
Corona vaccinations
2021-04-24 19:19:40 +02:00
Stijn De Clercq 20ad83ba23 Corona vaccinations 2021-04-24 19:15:43 +02:00
Stijn De Clercq baa3478cec
Merge pull request #51 from stijndcl/fix_google
Fix google search
2021-04-21 09:45:10 +02:00
Stijn De Clercq f76d70c169 Fix google search 2021-04-21 09:44:26 +02:00
Stijn De Clercq 5703ee6fcf
Merge pull request #50 from stijndcl/enable_les
Enable les again
2021-04-18 23:26:39 +02:00
Stijn De Clercq 8e51364268 Change comparch schedule 2021-04-18 23:26:05 +02:00
Stijn De Clercq d240c3c50f Enable les again 2021-04-18 23:24:46 +02:00
Stijn De Clercq bc6a0d300c
Merge pull request #49 from stijndcl/mod_contributors
Replace isMe with isOwner to allow other people to debug their own bot
2021-04-14 18:16:29 +02:00
Stijn De Clercq 7118a80d5d Replace isMe with isOwner to allow other people to debug their own bot 2021-04-14 18:07:09 +02:00
Stijn De Clercq e3c0d2b444
Merge pull request #48 from TheMessik/depend_fix
added missing dependencies
2021-04-13 23:47:32 +02:00
Jozef Jankaj 9bb7c542cf added missing dependencies 2021-04-13 23:40:28 +02:00
Stijn De Clercq 05e3b0758a Fix requirements 2021-04-13 16:01:32 +02:00
Stijn De Clercq 56a2880b31
Merge pull request #45 from stijndcl/disable_les
Disable les
2021-04-05 15:28:49 +02:00
Stijn De Clercq f655748025 Disable les 2021-04-05 15:27:58 +02:00
Stijn De Clercq c1169b4496
Fix mm link 2021-04-02 13:03:14 +02:00
Stijn De Clercq 7f465fddae update multimedia schedule 2021-03-23 14:48:13 +01:00
Stijn De Clercq 1979703194 Unescape HTML, make announcement task run every 10 min instead of every 30 2021-03-12 10:05:19 +01:00
Stijn De Clercq 4477ec6c0b Update schedule 2021-03-04 14:25:14 +01:00
Stijn De Clercq 2bc949de83
Merge pull request #42 from stijndcl/ufora_notifications
Ufora notifications, Inspire
2021-03-03 18:49:48 +01:00
Stijn De Clercq 6c0f9fd017 Run every 30 min instead of every 2 2021-03-03 18:45:36 +01:00
Stijn De Clercq 2c634912ef Add three dots when truncating 2021-03-03 18:44:53 +01:00
Stijn De Clercq 981f7a1457 Change announcements channel 2021-03-03 18:40:31 +01:00
Stijn De Clercq db0e7b8f9b Add support for linebreaks, parse id's beforehand to check properly, add leading zeroes to timestamp 2021-03-03 18:37:09 +01:00
Stijn De Clercq 1372f0a996 Fix typo in urls 2021-03-03 18:16:23 +01:00
Stijn De Clercq 268bb80bfd Add markdown support for ufora notifications, store notifications using IDs 2021-03-03 18:13:22 +01:00
Stijn De Clercq c9bef3b300 Add notifications to ignored.md 2021-03-03 18:06:28 +01:00
Stijn De Clercq 779a84828b Announcements work + task 2021-03-03 18:04:31 +01:00
Stijn De Clercq ade7f8b72e Start working on ufora notifications, add new course id's to FAQ 2021-03-03 15:57:35 +01:00
Stijn De Clercq c374fce3f3 Properly disable launch 2021-03-03 15:44:18 +01:00
Stijn De Clercq 6568cfdcce Inspire command (fixes #40), disable Launch 2021-03-03 15:36:18 +01:00
Stijn De Clercq 66ec275188 Update schedule 2021-03-02 14:29:04 +01:00
Stijn De Clercq fcfb80e53a Update CA schedule 2021-03-01 20:20:41 +01:00
Stijn De Clercq 3f8f34aa9f Unpack shuffle shortcut too, cleans up code 2021-02-28 18:44:03 +01:00
Stijn De Clercq 6a671a4768 Forgot to unpack tuple in choose shortcut 2021-02-28 18:41:41 +01:00
Stijn De Clercq 6c4c8079c5 Merge branch 'master' of github.com:stijndcl/didier 2021-02-27 15:28:16 +01:00
Stijn De Clercq 84b43ade6a Fix error in rev 2021-02-27 15:28:00 +01:00
Stijn De Clercq 77573eccc3
Change SEL1 to MSTeams 2021-02-25 14:32:12 +01:00
Stijn De Clercq fe843ad4f0 Merge branch 'master' of github.com:stijndcl/didier 2021-02-25 10:46:05 +01:00
Stijn De Clercq 415ac2eeb3 Fix error in define 2021-02-25 10:45:48 +01:00
Stijn De Clercq 01a397add4
Update multimedia link 2021-02-22 14:30:20 +01:00
Stijn De Clercq a2b7602680 Make targetted Mess public because why not 2021-02-18 11:25:28 +01:00
Stijn De Clercq b971f661b4 Update WebDev link, whitelist Warzone
Fixes #37
2021-02-18 09:57:09 +01:00
Stijn De Clercq a9c8d31ceb
Merge pull request #36 from stijndcl/pin
Close small issues
2021-02-08 22:22:13 +01:00
Stijn De Clercq 78a015e30d
Update functions/reactWord.py 2021-02-08 22:19:40 +01:00
Stijn De Clercq a63642c21f
Update cogs/minesweeper.py 2021-02-08 20:54:37 +01:00
Stijn De Clercq 6c08a84bd0 Check react count before adding reactions, catches an exception
Fixes #32
2021-02-08 20:38:26 +01:00
Stijn De Clercq 09f61237a2 Make changelog use github instead of docs 2021-02-08 20:26:34 +01:00
Stijn De Clercq 9fb4cc11f2 Resize hard grid to prevent api limitations
Fixes #17
2021-02-08 20:22:22 +01:00
Stijn De Clercq 028a4e5e90 Use message converter for react
Fixes #9
2021-02-08 20:08:54 +01:00
Stijn De Clercq f2897dbac7 Update issue templates
Fixes #34
2021-02-08 19:04:07 +01:00
Stijn De Clercq 4614145df6 Add goal difference to jpl table
Fixes #33
2021-02-08 18:59:05 +01:00
Stijn De Clercq 64eaee4f5d Support roles for 2.2 courses 2021-02-08 18:50:56 +01:00
Stijn De Clercq 0c83472287 Pin, without votes to see where it goes
Fixes #23
2021-02-08 18:46:15 +01:00
Stijn De Clercq afca7bb451 Create db for pin 2021-02-08 18:32:59 +01:00
Stijn De Clercq 01b1c5d2b3 Update schedule, re-enable les reminders, allow school commands globally 2021-02-08 18:25:25 +01:00
Stijn De Clercq d13a5f77d8
Merge pull request #35 from stijndcl/update_schedules
Add schedule 2.2, re-enable Les
2021-02-06 17:19:10 +01:00
Stijn De Clercq e611cc1285 Add in placeholder zoom links for classes where online link isn't known yet 2021-02-06 17:04:32 +01:00
Stijn De Clercq 2593c3ba0d Re-enable les, fix schedule 2021-02-06 17:01:43 +01:00
Stijn De Clercq 706ea1fb35 Add schedule for 2.2, some online links are unknown & are Zoom until the professors say where they happen 2021-02-06 16:50:40 +01:00
Stijn De Clercq 469d591a29 Fix missing typecast in store 2021-02-03 19:32:43 +01:00
Stijn De Clercq e847b0e4b3 Fix issue in corona 2021-02-02 01:44:38 +01:00
Stijn De Clercq 9df8126455 Fix forgotten dict element in football 2021-01-27 21:53:56 +01:00
Stijn De Clercq 2bfd507bce
Merge pull request #31 from stijndcl/disable_translate
Small reworks
2021-01-26 22:28:16 +01:00
Stijn De Clercq 9898263a73 Fix comment 2021-01-26 22:26:49 +01:00
Stijn De Clercq 342db2df38 Add comments 2021-01-26 22:00:20 +01:00
Stijn De Clercq 7fe2689db1 Disable isme check on football cog 2021-01-26 21:59:22 +01:00
Stijn De Clercq 89fdc70813 Clean up jpl table 2021-01-26 21:58:49 +01:00
Stijn De Clercq 7c47c6af73 Clean memegen code up more, add more memes (#15) 2021-01-26 00:05:44 +01:00
Stijn De Clercq 77addbc30c Use meme class 2021-01-25 23:30:52 +01:00
Stijn De Clercq f2b62c3ce7 Clean up memegen code a bit 2021-01-25 23:21:40 +01:00
Stijn De Clercq 8238bdf6de Clean up Football matches a lot 2021-01-25 00:16:38 +01:00
Stijn De Clercq 2b96f3ec41 Scrape current jpl matchweek every few hours 2021-01-24 22:31:09 +01:00
Stijn De Clercq d9d8c6a842 Fix incorrect sorting in stats ca 2021-01-24 22:00:24 +01:00
Stijn De Clercq 25f4ac5314 Disable Translate until a fix for the module is found (#16) 2021-01-24 21:49:06 +01:00
320 changed files with 11549 additions and 354624 deletions

View File

@ -3,4 +3,4 @@ root = true
[{*.yml, *.yaml}]
indent_size = 2
indent_style = space
max_line_length=80
max_line_length=120

44
.flake8 100644
View File

@ -0,0 +1,44 @@
[flake8]
# Don't lint non-Python files
exclude =
.git,
.github,
.mypy_cache,
.pytest_cache,
__pycache__,
alembic,
htmlcov,
venv
# Disable rules that we don't care about (or conflict with others)
extend-ignore =
# Missing docstring in public module
D100, D104,
# Missing docstring in magic method
D105,
# Missing docstring in __init__
D107,
# First line of docstrings should end with a period
D400,
# First line of docstrings should be in imperative mood
D401,
# Whitespace before ":"
E203,
# Standard pseudo-random generators are not suitable for security/cryptographic purposes.
S311,
# Don't require docstrings when overriding a method,
# the base method should have a docstring but the rest not
ignore-decorators=overrides
max-line-length = 120
# Disable some rules for entire files
per-file-ignores =
# DALL000: Missing __all__, main isn't supposed to be imported
main.py: DALL000, run_db_scripts.py: DALL000,
# DALL000: Missing __all__, Cogs aren't modules
./didier/cogs/*: DALL000,
# DALL000: Missing __all__, tests aren't supposed to be imported
# S101: Use of assert, this is the point of tests
./tests/*: DALL000 S101,
# D103: Missing docstring in public function
# All of the colours methods are just oneliners to create a colour,
# there's no point adding docstrings (function names are enough)
./didier/utils/discord/colours.py: D103,

View File

@ -0,0 +1,17 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
labels: bug
assignees: stijndcl
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce**
Indicate how others can reproduce the issue.
**Screenshots**
If applicable, add screenshots to help explain your problem.

View File

@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] "
labels: enhancement
assignees: stijndcl
---
**Provide a clear description of the feature you'd like to request.**

123
.github/workflows/python.yml vendored 100644
View File

@ -0,0 +1,123 @@
name: Python CI
on:
push:
jobs:
dependencies:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
tests:
needs: [dependencies]
runs-on: ubuntu-20.04
services:
postgres:
image: postgres:14
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5433:5432
env:
POSTGRES_DB: didier_pytest
POSTGRES_USER: pytest
POSTGRES_PASSWORD: pytest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Run Pytest
run: |
coverage run -m pytest
coverage xml
- name: Upload coverage report to CodeCov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV }}
linting:
needs: [tests]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Linting
run: flake8
typing:
needs: [tests]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Typing
run: mypy
formatting:
needs: [tests]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Formatting
run: black --check didier database

173
.gitignore vendored
View File

@ -1,13 +1,162 @@
files/status.txt
files/readyMessage.txt
files/client.txt
files/lastTasks.json
files/c4.json
files/hangman.json
files/stats.json
files/lost.json
files/locked.json
files/database.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
.idea/
__pycache__
.env
# Debugging files
debug.py
# Schedule .ics files
/files/schedules/

View File

@ -0,0 +1,46 @@
default_language_version:
python: python3.9.5
repos:
- repo: https://github.com/ambv/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-json
- id: end-of-file-fixer
- id: pretty-format-json
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/PyCQA/autoflake
rev: v2.2.0
hooks:
- id: autoflake
name: autoflake (python)
args:
- "--remove-all-unused-imports"
- "--in-place"
- "--ignore-init-module-imports"
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
exclude: ^(alembic|.github)
args: [--config, .flake8]
additional_dependencies:
- "flake8-bandit"
- "flake8-bugbear"
- "flake8-docstrings"
- "flake8-dunder-all"
- "flake8-eradicate"
- "flake8-isort"
- "flake8-simplify"

View File

@ -1 +1 @@
3.6.9
3.9.5

1
.tool-versions 100644
View File

@ -0,0 +1 @@
python 3.9.5

105
alembic.ini 100644
View File

@ -0,0 +1,105 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
alembic/README 100644
View File

@ -0,0 +1 @@
Generic single-database configuration.

51
alembic/env.py 100644
View File

@ -0,0 +1,51 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context
from database.engine import postgres_engine
from database.schemas import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata, render_as_batch=True)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations(connectable: AsyncEngine):
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = context.config.attributes.get("connection", None) or postgres_engine
if isinstance(connectable, AsyncEngine):
asyncio.run(run_async_migrations(connectable))
else:
do_run_migrations(connectable)
run_migrations_online()

View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,94 @@
"""Migrate to 2.x
Revision ID: 09128b6e34dd
Revises: 1e3e7f4192c4
Create Date: 2023-07-07 16:23:15.990231
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "09128b6e34dd"
down_revision = "1e3e7f4192c4"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("bank", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("birthdays", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("bookmarks", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("command_stats", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op:
batch_op.alter_column("command_id", existing_type=sa.INTEGER(), nullable=False)
with op.batch_alter_table("deadlines", schema=None) as batch_op:
batch_op.alter_column("course_id", existing_type=sa.INTEGER(), nullable=False)
with op.batch_alter_table("github_links", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("nightly_data", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("reminders", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=False)
with op.batch_alter_table("ufora_announcements", schema=None) as batch_op:
batch_op.alter_column("course_id", existing_type=sa.INTEGER(), nullable=False)
batch_op.alter_column("publication_date", existing_type=sa.DATE(), nullable=False)
with op.batch_alter_table("ufora_course_aliases", schema=None) as batch_op:
batch_op.alter_column("course_id", existing_type=sa.INTEGER(), nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("ufora_course_aliases", schema=None) as batch_op:
batch_op.alter_column("course_id", existing_type=sa.INTEGER(), nullable=True)
with op.batch_alter_table("ufora_announcements", schema=None) as batch_op:
batch_op.alter_column("publication_date", existing_type=sa.DATE(), nullable=True)
batch_op.alter_column("course_id", existing_type=sa.INTEGER(), nullable=True)
with op.batch_alter_table("reminders", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
with op.batch_alter_table("nightly_data", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
with op.batch_alter_table("github_links", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
with op.batch_alter_table("deadlines", schema=None) as batch_op:
batch_op.alter_column("course_id", existing_type=sa.INTEGER(), nullable=True)
with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op:
batch_op.alter_column("command_id", existing_type=sa.INTEGER(), nullable=True)
with op.batch_alter_table("command_stats", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
with op.batch_alter_table("bookmarks", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
with op.batch_alter_table("birthdays", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
with op.batch_alter_table("bank", schema=None) as batch_op:
batch_op.alter_column("user_id", existing_type=sa.BIGINT(), nullable=True)
# ### end Alembic commands ###

View File

@ -0,0 +1,32 @@
"""Add second role to ufora courses
Revision ID: 11388e39bb90
Revises: a64876b41af2
Create Date: 2022-09-25 00:09:06.625622
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "11388e39bb90"
down_revision = "a64876b41af2"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("ufora_courses", schema=None) as batch_op:
batch_op.add_column(sa.Column("alternative_overarching_role_id", sa.BigInteger(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("ufora_courses", schema=None) as batch_op:
batch_op.drop_column("alternative_overarching_role_id")
# ### end Alembic commands ###

View File

@ -0,0 +1,57 @@
"""Remove wordle
Revision ID: 1e3e7f4192c4
Revises: 954ad804f057
Create Date: 2023-07-07 14:52:20.993687
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "1e3e7f4192c4"
down_revision = "954ad804f057"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("wordle_word")
op.drop_table("wordle_stats")
op.drop_table("wordle_guesses")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"wordle_guesses",
sa.Column("wordle_guess_id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("user_id", sa.BIGINT(), autoincrement=False, nullable=True),
sa.Column("guess", sa.TEXT(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.user_id"], name="wordle_guesses_user_id_fkey"),
sa.PrimaryKeyConstraint("wordle_guess_id", name="wordle_guesses_pkey"),
)
op.create_table(
"wordle_stats",
sa.Column("wordle_stats_id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("user_id", sa.BIGINT(), autoincrement=False, nullable=True),
sa.Column("last_win", sa.DATE(), autoincrement=False, nullable=True),
sa.Column("games", sa.INTEGER(), server_default=sa.text("0"), autoincrement=False, nullable=False),
sa.Column("wins", sa.INTEGER(), server_default=sa.text("0"), autoincrement=False, nullable=False),
sa.Column("current_streak", sa.INTEGER(), server_default=sa.text("0"), autoincrement=False, nullable=False),
sa.Column("highest_streak", sa.INTEGER(), server_default=sa.text("0"), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.user_id"], name="wordle_stats_user_id_fkey"),
sa.PrimaryKeyConstraint("wordle_stats_id", name="wordle_stats_pkey"),
)
op.create_table(
"wordle_word",
sa.Column("word_id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("word", sa.TEXT(), autoincrement=False, nullable=False),
sa.Column("day", sa.DATE(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint("word_id", name="wordle_word_pkey"),
sa.UniqueConstraint("day", name="wordle_word_day_key"),
)
# ### end Alembic commands ###

View File

@ -0,0 +1,246 @@
"""Initial migration
Revision ID: 515dc3f52c6d
Revises:
Create Date: 2022-09-18 00:30:56.348634
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "515dc3f52c6d"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"custom_commands",
sa.Column("command_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("indexed_name", sa.Text(), nullable=False),
sa.Column("response", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("command_id"),
sa.UniqueConstraint("name"),
)
with op.batch_alter_table("custom_commands", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_custom_commands_indexed_name"), ["indexed_name"], unique=False)
op.create_table(
"dad_jokes",
sa.Column("dad_joke_id", sa.Integer(), nullable=False),
sa.Column("joke", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("dad_joke_id"),
)
op.create_table(
"links",
sa.Column("link_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("url", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("link_id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"meme",
sa.Column("meme_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("template_id", sa.Integer(), nullable=False),
sa.Column("field_count", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("meme_id"),
sa.UniqueConstraint("name"),
sa.UniqueConstraint("template_id"),
)
op.create_table(
"tasks",
sa.Column("task_id", sa.Integer(), nullable=False),
sa.Column("task", sa.Enum("BIRTHDAYS", "SCHEDULES", "UFORA_ANNOUNCEMENTS", name="tasktype"), nullable=False),
sa.Column("previous_run", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("task_id"),
sa.UniqueConstraint("task"),
)
op.create_table(
"ufora_courses",
sa.Column("course_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("code", sa.Text(), nullable=False),
sa.Column("year", sa.Integer(), nullable=False),
sa.Column("compulsory", sa.Boolean(), server_default="1", nullable=False),
sa.Column("role_id", sa.BigInteger(), nullable=True),
sa.Column("overarching_role_id", sa.BigInteger(), nullable=True),
sa.Column("log_announcements", sa.Boolean(), server_default="0", nullable=False),
sa.PrimaryKeyConstraint("course_id"),
sa.UniqueConstraint("code"),
sa.UniqueConstraint("name"),
)
op.create_table("users", sa.Column("user_id", sa.BigInteger(), nullable=False), sa.PrimaryKeyConstraint("user_id"))
op.create_table(
"wordle_word",
sa.Column("word_id", sa.Integer(), nullable=False),
sa.Column("word", sa.Text(), nullable=False),
sa.Column("day", sa.Date(), nullable=False),
sa.PrimaryKeyConstraint("word_id"),
sa.UniqueConstraint("day"),
)
op.create_table(
"bank",
sa.Column("bank_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("dinks", sa.BigInteger(), server_default="0", nullable=False),
sa.Column("invested", sa.BigInteger(), server_default="0", nullable=False),
sa.Column("interest_level", sa.Integer(), server_default="1", nullable=False),
sa.Column("capacity_level", sa.Integer(), server_default="1", nullable=False),
sa.Column("rob_level", sa.Integer(), server_default="1", nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("bank_id"),
)
op.create_table(
"birthdays",
sa.Column("birthday_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("birthday", sa.Date(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("birthday_id"),
)
op.create_table(
"bookmarks",
sa.Column("bookmark_id", sa.Integer(), nullable=False),
sa.Column("label", sa.Text(), nullable=False),
sa.Column("jump_url", sa.Text(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("bookmark_id"),
sa.UniqueConstraint("user_id", "label"),
)
op.create_table(
"custom_command_aliases",
sa.Column("alias_id", sa.Integer(), nullable=False),
sa.Column("alias", sa.Text(), nullable=False),
sa.Column("indexed_alias", sa.Text(), nullable=False),
sa.Column("command_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["command_id"],
["custom_commands.command_id"],
),
sa.PrimaryKeyConstraint("alias_id"),
sa.UniqueConstraint("alias"),
)
with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_custom_command_aliases_indexed_alias"), ["indexed_alias"], unique=False)
op.create_table(
"deadlines",
sa.Column("deadline_id", sa.Integer(), nullable=False),
sa.Column("course_id", sa.Integer(), nullable=True),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("deadline", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["course_id"],
["ufora_courses.course_id"],
),
sa.PrimaryKeyConstraint("deadline_id"),
)
op.create_table(
"nightly_data",
sa.Column("nightly_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("last_nightly", sa.Date(), nullable=True),
sa.Column("count", sa.Integer(), server_default="0", nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("nightly_id"),
)
op.create_table(
"ufora_announcements",
sa.Column("announcement_id", sa.Integer(), nullable=False),
sa.Column("course_id", sa.Integer(), nullable=True),
sa.Column("publication_date", sa.Date(), nullable=True),
sa.ForeignKeyConstraint(
["course_id"],
["ufora_courses.course_id"],
),
sa.PrimaryKeyConstraint("announcement_id"),
)
op.create_table(
"ufora_course_aliases",
sa.Column("alias_id", sa.Integer(), nullable=False),
sa.Column("alias", sa.Text(), nullable=False),
sa.Column("course_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["course_id"],
["ufora_courses.course_id"],
),
sa.PrimaryKeyConstraint("alias_id"),
sa.UniqueConstraint("alias"),
)
op.create_table(
"wordle_guesses",
sa.Column("wordle_guess_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("guess", sa.Text(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("wordle_guess_id"),
)
op.create_table(
"wordle_stats",
sa.Column("wordle_stats_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("last_win", sa.Date(), nullable=True),
sa.Column("games", sa.Integer(), server_default="0", nullable=False),
sa.Column("wins", sa.Integer(), server_default="0", nullable=False),
sa.Column("current_streak", sa.Integer(), server_default="0", nullable=False),
sa.Column("highest_streak", sa.Integer(), server_default="0", nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("wordle_stats_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("wordle_stats")
op.drop_table("wordle_guesses")
op.drop_table("ufora_course_aliases")
op.drop_table("ufora_announcements")
op.drop_table("nightly_data")
op.drop_table("deadlines")
with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_custom_command_aliases_indexed_alias"))
op.drop_table("custom_command_aliases")
op.drop_table("bookmarks")
op.drop_table("birthdays")
op.drop_table("bank")
op.drop_table("wordle_word")
op.drop_table("users")
op.drop_table("ufora_courses")
op.drop_table("tasks")
op.drop_table("meme")
op.drop_table("links")
op.drop_table("dad_jokes")
with op.batch_alter_table("custom_commands", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_custom_commands_indexed_name"))
op.drop_table("custom_commands")
sa.Enum("BIRTHDAYS", "SCHEDULES", "UFORA_ANNOUNCEMENTS", name="tasktype").drop(op.get_bind())
# ### end Alembic commands ###

View File

@ -0,0 +1,36 @@
"""Add events table
Revision ID: 954ad804f057
Revises: 9fb84b4d9f0b
Create Date: 2023-02-02 22:20:23.107931
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "954ad804f057"
down_revision = "9fb84b4d9f0b"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"events",
sa.Column("event_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("notification_channel", sa.BigInteger(), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("event_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("events")
# ### end Alembic commands ###

View File

@ -0,0 +1,30 @@
"""Add free games
Revision ID: 9fb84b4d9f0b
Revises: 11388e39bb90
Create Date: 2022-10-13 19:17:58.032182
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "9fb84b4d9f0b"
down_revision = "11388e39bb90"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"free_games", sa.Column("free_game_id", sa.Integer(), nullable=False), sa.PrimaryKeyConstraint("free_game_id")
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("free_games")
# ### end Alembic commands ###

View File

@ -0,0 +1,39 @@
"""Add reminders
Revision ID: a64876b41af2
Revises: c1f9ee875616
Create Date: 2022-09-23 13:37:10.331840
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "a64876b41af2"
down_revision = "c1f9ee875616"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"reminders",
sa.Column("reminder_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("category", sa.Enum("LES", name="remindercategory"), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("reminder_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("reminders")
sa.Enum("LES", name="remindercategory").drop(op.get_bind())
# ### end Alembic commands ###

View File

@ -0,0 +1,36 @@
"""Easter eggs
Revision ID: b84bb10fb8de
Revises: 515dc3f52c6d
Create Date: 2022-09-20 00:23:53.160168
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "b84bb10fb8de"
down_revision = "515dc3f52c6d"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"easter_eggs",
sa.Column("easter_egg_id", sa.Integer(), nullable=False),
sa.Column("match", sa.Text(), nullable=False),
sa.Column("response", sa.Text(), nullable=False),
sa.Column("exact", sa.Boolean(), server_default="1", nullable=False),
sa.Column("startswith", sa.Boolean(), server_default="1", nullable=False),
sa.PrimaryKeyConstraint("easter_egg_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("easter_eggs")
# ### end Alembic commands ###

View File

@ -0,0 +1,54 @@
"""Command stats & GitHub links
Revision ID: c1f9ee875616
Revises: b84bb10fb8de
Create Date: 2022-09-20 17:18:02.289593
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "c1f9ee875616"
down_revision = "b84bb10fb8de"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"command_stats",
sa.Column("command_stats_id", sa.Integer(), nullable=False),
sa.Column("command", sa.Text(), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("slash", sa.Boolean(), nullable=False),
sa.Column("context_menu", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("command_stats_id"),
)
op.create_table(
"github_links",
sa.Column("github_link_id", sa.Integer(), nullable=False),
sa.Column("url", sa.Text(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("github_link_id"),
sa.UniqueConstraint("url"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("github_links")
op.drop_table("command_stats")
# ### end Alembic commands ###

14
codecov.yaml 100644
View File

@ -0,0 +1,14 @@
comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]
require_head: yes # [yes :: must have a head report to post]
coverage:
round: down
precision: 5
ignore:
- "./didier/cogs/*" # Cogs can't really be tested properly
- "./tests/*"

View File

@ -1,226 +0,0 @@
from data import constants
import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import timeFormatters, stringFormatters
from functions.database import birthdays
class Birthdays(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Birthday", aliases=["Bd", "Birthdays"], case_insensitive=True, invoke_without_command=True)
@help.Category(Category.Other)
async def birthday(self, ctx, member: discord.Member = None):
"""
Command to check the birthday of yourself/another person.
:param ctx: Discord Context
:param member: The member to check
"""
if member is not None:
# A member was tagged
nameStr = "**{}**'s".format(member.display_name)
res = birthdays.get_user(member.id)
else:
# No member passed -> check the user's birthday
nameStr = "Jouw"
res = birthdays.get_user(ctx.author.id)
if not res:
# Nothing found in the db for this member
return await ctx.send("{} verjaardag zit nog niet in de database.".format(nameStr))
# Create a datetime object of the upcoming birthday,
# and a formatted string displaying the date
dayDatetime, timeString = self.dmToDatetime(res[0][0], res[0][1])
# Find the weekday related to this day
weekday = timeFormatters.intToWeekday(dayDatetime.weekday()).lower()
return await ctx.send("{} verjaardag staat ingesteld op **{} {}**.".format(
nameStr, weekday, timeString
))
@birthday.command(name="Today", aliases=["Now"])
async def today(self, ctx):
"""
Command that lists all birthdays of the day.
:param ctx: Discord Context
"""
# Create a datetime object for today
dt = timeFormatters.dateTimeNow()
await ctx.send(self.getBirthdayOnDate(dt))
@birthday.command(name="Tomorrow", aliases=["Tm", "Tmw"])
async def tomorrow(self, ctx):
"""
Command that lists all birthdays of tomorrow.
:param ctx: Discord Context
"""
# Create a datetime object for tomorrow
dt = timeFormatters.dateTimeNow() + datetime.timedelta(days=1)
await ctx.send(self.getBirthdayOnDate(dt).replace("Vandaag", "Morgen").replace("vandaag", "morgen"))
@birthday.command(name="Week")
async def week(self, ctx):
"""
Command that lists all birthdays for the coming week.
:param ctx: Discord Context
"""
# Dict of all birthdays this week
this_week = {}
# Create a datetime object starting yesterday so the first line
# of the loop can add a day every time,
# as premature returning would prevent this from happening
# & get the day stuck
dt = timeFormatters.dateTimeNow() - datetime.timedelta(days=1)
# Create an embed
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Verjaardagen deze week")
# Add all people of the coming week
for dayCounter in range(7):
dt += datetime.timedelta(days=1)
res = birthdays.get_users_on_date(dt.day, dt.month)
# No birthdays on this day
if not res:
continue
# Add everyone from this day into the dict
this_week[str(dayCounter)] = {"day": dt.day, "month": dt.month, "users": []}
for user in res:
this_week[str(dayCounter)]["users"].append(user[0])
# No one found
if not this_week:
embed.description = "Deze week is er niemand jarig."
return await ctx.send(embed=embed)
COC = self.client.get_guild(int(constants.CallOfCode))
# For every day, add the list of users into the embed
for day, value in this_week.items():
dayDatetime, timeString = self.dmToDatetime(int(value["day"]), int(value["month"]))
weekday = timeFormatters.intToWeekday(dayDatetime.weekday())
embed.add_field(name="{} {}".format(weekday, timeString),
value=", ".join(COC.get_member(user).mention for user in value["users"]),
inline=False)
await ctx.send(embed=embed)
def getBirthdayOnDate(self, dt):
"""
Function to get all birthdays on a certain date.
Returns a string right away to avoid more code duplication.
:param dt: the date (Python datetime instance)
:return: A formatted string containing all birthdays on [dt]
"""
res = birthdays.get_users_on_date(dt.day, dt.month)
# Nobody's birthday
if not res:
return "Vandaag is er niemand jarig."
COC = self.client.get_guild(int(constants.CallOfCode))
# Create a list of member objects of the people that have a birthday on this date
people = [COC.get_member(int(user[0])) for user in res]
if len(people) == 1:
return "Vandaag is **{}** jarig.".format(people[0].display_name)
return "Vandaag zijn {} en {} jarig.".format(
", ".join("**" + user.display_name + "**" for user in people[:-1]),
people[-1].display_name
)
def dmToDatetime(self, day, month):
"""
Converts a day + month to a datetime instance.
:param day: the day in the date
:param month: the month in the date
:return: a datetime instance representing the next time this date occurs,
and a formatted string for this date
"""
now = timeFormatters.dateTimeNow()
year = now.year
# Add an extra year to the date in case it has already passed
if month < now.month or (month == now.month and day < now.day):
year += 1
# Create a datetime object for this birthday
timeString = "{}/{}/{}".format(
stringFormatters.leadingZero(str(day)),
stringFormatters.leadingZero(str(month)),
year
)
dayDatetime = datetime.datetime.strptime(timeString, "%d/%m/%Y")
return dayDatetime, timeString
@birthday.command(name="Set", usage="[DD/MM/YYYY]")
async def set(self, ctx, date=None, member: discord.Member = None):
"""
Command to add your birthday into the database.
:param ctx: Discord Context
:param date: the date of your birthday
:param member: another member whose birthday has to be added/changed
"""
# No date passed
if date is None:
return await ctx.send("Geef een datum op.")
# Invalid format used
if date.count("/") != 2:
return await ctx.send("Ongeldig formaat (gebruik DD/MM/YYYY).")
# Check if anything is wrong with the date
try:
day = int(date.split("/")[0])
month = int(date.split("/")[1])
year = int(date.split("/")[2])
# This is not used, but creating an invalid datetime object throws a ValueError
# so it prevents invalid dates like 69/420/360
dt = datetime.datetime(year=year, month=month, day=day)
# Assume no one in the Discord is more than 5 years younger, or 10 years older
# (which are also virtually impossible, but just to be sure)
if year >= timeFormatters.dateTimeNow().year - 15 or year < 1990:
raise ValueError
except ValueError:
return await ctx.send("Dit is geen geldige datum.")
# A member was tagged, check if I did it
if member is not None:
if str(ctx.author.id) != str(constants.myId):
return await ctx.send("Je kan andere mensen hun verjaardag niet instellen, {}.".format(ctx.author.display_name))
else:
birthdays.add_user(member.id, day, month, year)
return await ctx.message.add_reaction("")
# Birthday is already added
if birthdays.get_user(ctx.author.id) and str(ctx.author.id) != constants.myId:
return await ctx.send("Je verjaardag zit al in de database.")
# Add into the db
birthdays.add_user(ctx.author.id, day, month, year)
return await ctx.send("Je verjaardag is toegevoegd aan de database.")
def setup(client):
client.add_cog(Birthdays(client))

View File

@ -1,149 +0,0 @@
from converters.numbers import Abbreviated
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, timeFormatters
from functions.database import currency
import requests
class Bitcoin(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Bitcoin", aliases=["Bc"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def bc(self, ctx):
"""
Command that shows your Bitcoin bank.
:param ctx: Discord Context
"""
price = self.getPrice()
bc = float(currency.getOrAddUser(ctx.author.id)[8])
currentTime = timeFormatters.dateTimeNow()
currentTimeFormatted = currentTime.strftime('%m/%d/%Y om %H:%M:%S')
# Create the embed
embed = discord.Embed(colour=discord.Colour.gold())
embed.set_author(name="Bitcoin Bank van {}".format(ctx.author.display_name))
embed.add_field(name="Aantal Bitcoins:", value="{:,}".format(round(bc, 8)), inline=False)
embed.add_field(name="Huidige waarde:", value="{:,} Didier Dink{}"
.format(round(bc * price, 8), checks.pluralS(bc * price)), inline=False)
embed.set_footer(text="Huidige Bitcoin prijs: €{:,} ({})".format(price, str(currentTimeFormatted)))
# Add the Bitcoin icon to the embed
file = discord.File("files/images/bitcoin.png", filename="icon.png")
embed.set_thumbnail(url="attachment://icon.png")
await ctx.send(embed=embed, file=file)
@bc.command(name="Price")
async def price(self, ctx):
"""
Command that shows the current Bitcoin price.
:param ctx: Discord Context
"""
price = self.getPrice()
currentTime = timeFormatters.dateTimeNow()
currentTimeFormatted = currentTime.strftime('%m/%d/%Y om %H:%M:%S')
await ctx.send(
"Huidige Bitcoin prijs: **€{:,}** ({}).".format(price, str(currentTimeFormatted)))
@bc.command(name="Buy", usage="[Aantal]")
async def buy(self, ctx, amount: Abbreviated):
"""
Command to buy Bitcoins.
:param ctx: Discord Context
:param amount: the amount of Bitcoins the user wants to buy
"""
resp = checks.isValidAmount(ctx, amount)
# Not a valid amount: send the appropriate error message
if not resp[0]:
return await ctx.send(resp[1])
if amount == "all":
amount = resp[1]
# Calculate the amount of Bitcoins the user can buy with [amount] of Didier Dinks
price = self.getPrice()
purchased = round(float(amount) / price, 8)
# Update the db
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - float(amount))
currency.update(ctx.author.id, "bitcoins",
float(currency.getOrAddUser(ctx.author.id)[8]) + float(purchased))
await ctx.send("**{}** heeft **{:,}** Bitcoin{} gekocht voor **{:,}** Didier Dink{}!"
.format(ctx.author.display_name, purchased, checks.pluralS(purchased),
round(float(amount)), checks.pluralS(amount)))
@bc.command(name="Sell", usage="[Aantal]")
async def sell(self, ctx, amount: Abbreviated):
"""
Command to sell Bitcoins.
:param ctx: Discord Context
:param amount: the amount of Bitcoins the user wants to sell
"""
if amount == "all":
amount = float(currency.getOrAddUser(ctx.author.id)[8])
try:
amount = float(amount)
if amount <= 0:
raise ValueError
bc = float(currency.getOrAddUser(ctx.author.id)[8])
if bc == 0.0:
# User has no Bitcoins
await ctx.send("Je hebt geen Bitcoins, **{}**".format(ctx.author.display_name))
elif amount > bc:
# User is trying to sell more Bitcoins that he has
await ctx.send("Je hebt niet genoeg Bitcoins om dit te doen, **{}**"
.format(ctx.author.display_name))
else:
price = self.getPrice()
dinks = float(currency.dinks(ctx.author.id))
currency.update(ctx.author.id, "bitcoins", bc - amount)
currency.update(ctx.author.id, "dinks", dinks + (price * amount))
await ctx.send("**{}** heeft **{:,}** Bitcoin{} verkocht voor **{:,}** Didier Dink{}!"
.format(ctx.author.display_name, round(amount, 8), checks.pluralS(amount),
round((price * amount), 8), checks.pluralS(price * amount)))
except ValueError:
# Can't be parsed to float -> random string OR smaller than 0
await ctx.send("Geef een geldig bedrag op.")
@bc.command(aliases=["Lb", "Leaderboards"], hidden=True)
@help.Category(category=Category.Other)
async def leaderboard(self, ctx):
"""
Command that shows the Bitcoin Leaderboard.
Alias for Lb Bc.
:param ctx: Discord Context
"""
# Call the appropriate leaderboard function
await self.client.get_cog("Leaderboards").callLeaderboard("bitcoin", ctx)
def getPrice(self):
"""
Function to get the current Bitcoin price.
:return: the current Bitcoin price (float)
"""
result = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json").json()
currentPrice = result["bpi"]["EUR"]["rate_float"]
return float(currentPrice)
def setup(client):
client.add_cog(Bitcoin(client))

View File

@ -1,397 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, timeFormatters
import requests
class Corona(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Gets the information & calls other functions if necessary
@commands.group(name="Corona", usage="[Land]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def corona(self, ctx, country: str = "Belgium"):
"""
Command that shows the corona stats for a certain country.
:param ctx: Discord Context
:param country: the country to show the stats for
"""
dic = await self.getCountryStats(country)
if dic is None:
# Country was not found
await self.sendError(ctx)
return
# Vaccination stats
vaccine = self.getVaccineData(country, dic["today"]["population"], dic["yesterday"]["population"])
await self.sendEmbed(ctx, dic, vaccine)
@corona.command(aliases=["lb", "leaderboards"], hidden=True)
async def leaderboard(self, ctx):
"""
Command that shows the Corona Leaderboard.
Alias for Lb Corona.
:param ctx: Discord Context
:return: y
"""
await self.client.get_cog("Leaderboards").callLeaderboard("corona", ctx)
async def sendEmbed(self, ctx, dic, vaccines):
"""
Function that sends a Corona embed from a dictionary.
:param ctx: Discord Context
:param dic: the dictionary corresponding to this country
"""
embed = discord.Embed(colour=discord.Colour.red(), title="Coronatracker {}".format(dic["today"]["country"]))
embed.set_thumbnail(url="https://i.imgur.com/aWnDuBt.png")
# Total
embed.add_field(name="Totale Gevallen (Vandaag):",
value=self.createEmbedString(
dic["today"]["cases"],
dic["today"]["todayCases"],
self.trendIndicator(dic, "todayCases")
),
inline=False)
# Active
embed.add_field(name="Actieve Gevallen (Vandaag):",
value=self.createEmbedString(
dic["today"]["activeCases"],
dic["today"]["activeCases"] - dic["yesterday"]["activeCases"],
self.activeTrendIndicator(dic)
),
inline=False)
# Deaths
embed.add_field(name="Sterfgevallen (Vandaag):",
value=self.createEmbedString(
dic["today"]["deaths"],
dic["today"]["todayDeaths"],
self.trendIndicator(dic, "todayDeaths")
),
inline=False)
# Recovered
embed.add_field(name="Hersteld (Vandaag):",
value=self.createEmbedString(
dic["today"]["recovered"],
dic["today"]["todayRecovered"],
self.trendIndicator(dic, "todayRecovered")
),
inline=False)
# Test Cases
embed.add_field(name="Aantal uitgevoerde tests:",
value=self.createEmbedString(
dic["today"]["tests"],
dic["today"]["todayTests"]
),
inline=False)
# Vaccines
if vaccines is not None:
# embed.add_field(name="Aantal toegediende vaccins:",
# value=self.createEmbedString(
# vaccines["today"]["vaccines"],
# vaccines["today"]["todayVaccines"],
# self.trendIndicator(vaccines, "todayVaccines")
# ))
embed.add_field(name="Aantal toegediende vaccins:",
value="{:,}".format(vaccines["today"]["vaccines"]),
inline=False)
# Timestamp of last update
timeFormatted = timeFormatters.epochToDate(int(dic["today"]["updated"]) / 1000)
embed.set_footer(text="Laatst geüpdatet op {} ({} geleden)".format(
timeFormatted["date"], timeFormatted["timeAgo"]))
await ctx.send(embed=embed)
def createEmbedString(self, total, today, indicator="", isPercentage=False):
"""
Function that formats a string to add covid data into the embed,
separate function to make code more readable
"""
# + or - symbol | minus is included in the number so no need
symbol = "+" if today >= 0 else ""
perc = "%" if isPercentage else ""
return "{:,}{} **({}{:,}{})** {}".format(
total, perc, symbol, today, perc, indicator
)
@commands.command(name="Trends", aliases=["Ct"], usage="[Land]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def trends(self, ctx, country: str = "Belgium"):
"""
Command that gives more precise stats & changes.
:param ctx: Discord Context
:param country: the country to get the stats for
"""
dic = await self.getCountryStats(country)
if dic is None:
await self.sendError(ctx)
return
# Get the distribution for this country
distribution = self.distribution(dic)
embed = discord.Embed(colour=discord.Colour.red(), title="Coronatrends {}".format(dic["today"]["country"]))
embed.set_thumbnail(url="https://i.imgur.com/aWnDuBt.png")
# Calculate the trends & add them into the fields
embed.add_field(name="Totale Gevallen\n({:,})".format(dic["today"]["cases"]),
value=self.trend(dic, "cases"),
inline=True)
embed.add_field(name="Sterfgevallen\n({:,})".format(dic["today"]["deaths"]),
value=self.trend(dic, "deaths"),
inline=True)
embed.add_field(name="Hersteld\n({:,})".format(dic["today"]["recovered"]),
value=self.trend(dic, "recovered"))
embed.add_field(name="Totale Gevallen\nVandaag ({:,})".format(dic["today"]["todayCases"]),
value=self.trend(dic, "todayCases"),
inline=True)
embed.add_field(name="Sterfgevallen\nVandaag ({:,})".format(dic["today"]["todayDeaths"]),
value=self.trend(dic, "todayDeaths"),
inline=True)
embed.add_field(name="Hersteld\nVandaag ({:,})".format(dic["today"]["todayRecovered"]),
value=self.trend(dic, "todayRecovered"))
embed.add_field(name="Verdeling", value="Actief: {} | Overleden: {} | Hersteld: {}".format(
distribution[0], distribution[1], distribution[2]), inline=False)
# Timestamp of last update
timeFormatted = timeFormatters.epochToDate(int(dic["today"]["updated"]) / 1000)
embed.set_footer(text="Laatst geüpdatet op {} ({} geleden)".format(
timeFormatted["date"], timeFormatted["timeAgo"]))
await ctx.send(embed=embed)
async def getCountryStats(self, country):
"""
Function that gets the stats for a specific country.
:param country: the country to get the stats for
:return: a dictionary containing the info for today & yesterday
"""
# Check if Global or a country was passed
if country.lower() == "global":
country = "all?"
else:
country = "countries/{}?strict=false&".format(country)
today = requests.get("https://disease.sh/v3/covid-19/{}yesterday=false&allowNull=false".format(country)).json()
# Send error message
if "message" in today:
return None
yesterday = requests.get("https://disease.sh/v3/covid-19/{}yesterday=true&allowNull=false".format(country)) \
.json()
# Divide into today & yesterday to be able to calculate the changes
dic = {
"today": {
"country": today["country"] if country != "all?" else "Global",
"cases": today["cases"],
"activeCases": today["active"],
"todayCases": today["todayCases"],
"deaths": today["deaths"],
"todayDeaths": today["todayDeaths"],
"recovered": today["recovered"],
"todayRecovered": today["todayRecovered"],
"tests": today["tests"],
"todayTests": today["tests"] - yesterday["tests"],
"updated": today["updated"],
"population": today["population"]
},
"yesterday": {
"cases": yesterday["cases"],
"activeCases": yesterday["active"],
"todayCases": yesterday["todayCases"],
"deaths": yesterday["deaths"],
"todayDeaths": yesterday["todayDeaths"],
"recovered": yesterday["recovered"],
"todayRecovered": yesterday["todayRecovered"],
"tests": yesterday["tests"],
"updated": yesterday["updated"],
"population": yesterday["population"]
}
}
return dic
def getVaccineData(self, country: str, todayPopulation: int, yesterdayPopulation: int):
"""
Function that gets vaccination stats for a specicic country.
This information is later added to the embed.
"""
if todayPopulation == 0:
todayPopulation = 1
if yesterdayPopulation == 0:
yesterdayPopulation = 1
# "all" has a different endpoint
if country == "global":
vaccine: dict = requests.get("https://disease.sh/v3/covid-19/vaccine/coverage?lastdays=3").json()
else:
vaccine: dict = requests.get("https://disease.sh/v3/covid-19/vaccine/coverage/countries/{}?lastdays=3"
.format(country)).json()
# Country-specific is structured differently
if "timeline" in vaccine:
vaccine = vaccine["timeline"]
# Error message returned or not enough data yet
if "message" in vaccine or len(vaccine.keys()) != 3:
return None
timeFormat = "%m/%d/%y"
def getDt(dt):
"""
Function that calls fromString with an argument
so it can be used in map
"""
return timeFormatters.fromString(dt, timeFormat)
def toString(dt):
"""
Function to cast datetime back to string
"""
st: str = dt.strftime(timeFormat)
# Api doesn't add leading zeroes so the keys don't match anymore ---'
if st.startswith("0"):
st = st[1:]
return st
# Order dates
ordered = sorted(map(getDt, vaccine.keys()))
# Datetime objects are only required for sorting, turn back into strings
ordered = list(map(toString, ordered))
info = {"today": {}, "yesterday": {}}
# Total vaccines
info["today"]["vaccines"] = vaccine[ordered[2]]
# New vaccines today
info["today"]["todayVaccines"] = vaccine[ordered[2]] - vaccine[ordered[1]]
# % of total population
info["today"]["perc"] = round(100 * vaccine[ordered[2]] / todayPopulation, 2)
info["yesterday"]["vaccines"] = vaccine[ordered[1]]
info["yesterday"]["todayVaccines"] = vaccine[ordered[1]] - vaccine[ordered[0]]
return info
def distribution(self, dic):
"""
Calculates the percentage distribution for every key & shows an indicator.
:param dic: the today/yesterday dictionary for this country
:return: a list containing the distribution + indicator for active, recovered & deaths
"""
totalToday = dic["today"]["cases"] if dic["today"]["cases"] != 0 else 1
totalYesterday = dic["yesterday"]["cases"] if dic["yesterday"]["cases"] != 0 else 1
tap = round(100 * dic["today"]["activeCases"] / totalToday, 2) # Today Active Percentage
trp = round(100 * dic["today"]["recovered"] / totalToday, 2) # Today Recovered Percentage
tdp = round(100 * dic["today"]["deaths"] / totalToday, 2) # Today Deaths Percentage
yap = round(100 * dic["yesterday"]["activeCases"] / totalYesterday, 2) # Yesterday Active Percentage
yrp = round(100 * dic["yesterday"]["recovered"] / totalYesterday, 2) # Yesterday Recovered Percentage
ydp = round(100 * dic["yesterday"]["deaths"] / totalYesterday, 2) # Yesterday Deaths Percentage
return ["{}% {}".format(tap, self.indicator(tap, yap)),
"{}% {}".format(tdp, self.indicator(tdp, ydp)),
"{}% {}".format(trp, self.indicator(trp, yrp))]
async def sendError(self, ctx):
"""
Function that sends an error embed when an invalid country was passed.
:param ctx: Discord Context
"""
embed = discord.Embed(colour=discord.Colour.red())
embed.add_field(name="Error", value="Dit land staat niet in de database.", inline=False)
await ctx.send(embed=embed)
# Returns a number and a percentage of rise/decline
def trend(self, dic, key):
"""
Function that creates a string representing a number & percentage of
rise & decline for a certain key of the dict.
:param dic: the today/yesterday dictionary for this country
:param key: the key to compare
:return: a string showing the increase in numbers & percentages
"""
# Difference vs yesterday
change = dic["today"][key] - dic["yesterday"][key]
# Don't divide by 0
yesterday = dic["yesterday"][key] if dic["yesterday"][key] != 0 else 1
# Percentage
perc = round(100 * change / yesterday, 2)
# Sign to add to the number
sign = "+" if change >= 0 else ""
return "{}{:,} ({}{:,}%)".format(sign, change, sign, perc)
# Requires a bit of math so this is a separate function
def activeTrendIndicator(self, dic):
"""
Function that returns a rise/decline indicator for the active cases of the day.
This is a separate function as it requires some math to get right.
New cases have to take into account the deaths & recovered cases being
subtracted as well.
:param dic: the today/yesterday dictionary for this country
:return: a triangle emoji or empty string
"""
todayNew = dic["today"]["todayCases"] - dic["today"]["todayDeaths"] - dic["today"]["todayRecovered"]
yesterdayNew = dic["yesterday"]["todayCases"] - dic["yesterday"]["todayDeaths"] - dic["yesterday"][
"todayRecovered"]
return ":small_red_triangle:" if todayNew > yesterdayNew else \
(":small_red_triangle_down:" if todayNew < yesterdayNew else "")
# Returns an arrow indicating rise or decline
def trendIndicator(self, dic, key):
"""
Function that returns a rise/decline indicator for the target key.
:param dic: the today/yesterday dictionary for this country
:param key: the key to get the indicator for
:return: a triangle emoji or empty string
"""
return ":small_red_triangle:" if dic["today"][key] > dic["yesterday"][key] else \
(":small_red_triangle_down:" if dic["today"][key] < dic["yesterday"][key] else "")
# Also returns an indicator, but compares instead of pulling it out of the dic (for custom numbers)
def indicator(self, today, yesterday):
"""
Function that also returns an indicator but for two numbers
instead of comparing values out of the dictionary.
:param today: the number representing today
:param yesterday: the number representing yesterday
:return: a triangle emoji or empty string
"""
return ":small_red_triangle:" if today > yesterday else \
(":small_red_triangle_down:" if yesterday > today else "")
def setup(client):
client.add_cog(Corona(client))

View File

@ -1,113 +0,0 @@
import os
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import requests
class Define(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Define", aliases=["UrbanDictionary", "Ud"], usage="[Woord]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def define(self, ctx, *words):
"""
Command that looks up the definition of a word in the Urban Dictionary.
:param ctx: Discord Context
:param words: Word(s) to look up
"""
words = list(words)
if len(words) == 0:
return await ctx.send("Controleer je argumenten.")
query = " ".join(words)
answer = self.lookup(query)
embed = discord.Embed(colour=discord.Colour.from_rgb(220, 255, 0))
embed.set_author(name="Urban Dictionary")
embed.add_field(name="Woord", value=answer["word"], inline=True)
embed.add_field(name="Auteur", value=answer["author"], inline=True)
embed.add_field(name="Definitie", value=self.cleanString(answer["definition"]), inline=False)
embed.add_field(name="Voorbeeld", value=self.cleanString(answer["example"]), inline=False)
embed.add_field(name="Rating", value=str(round(self.ratio(answer), 2)) + "%")
embed.add_field(name="Link naar de volledige definitie",
value="[Urban Dictionary]({})".format(str(answer["link"])))
await ctx.send(embed=embed)
def lookup(self, word):
"""
Function that sends the API request to get the definition.
:param word: the woord to look up
:return: a dictionary representing the info of this word
"""
url = "https://mashape-community-urban-dictionary.p.rapidapi.com/define"
querystring = {"term": word}
headers = {
'x-rapidapi-host': "mashape-community-urban-dictionary.p.rapidapi.com",
'x-rapidapi-key': os.getenv("URBANDICTIONARY")
}
try:
if word.lower() == "didier":
raise Exception
response = requests.request("GET", url, headers=headers, params=querystring).json()["list"]
if len(response) > 0:
return {"word": response[0]["word"], "definition": response[0]["definition"],
"example": response[0]["example"], "thumbs_up": response[0]["thumbs_up"],
"thumbs_down": response[0]["thumbs_down"], "link": response[0]["permalink"],
"author": response[0]["author"]}
# No valid response
return self.defineDidier()
except Exception:
return self.defineDidier()
def cleanString(self, text: str):
"""
Function that cuts off definitions that are too long & strips out UD markdown
from an input string.
:param text: the input string to clean up
:return: the edited version of the string
"""
text = text.replace("[", "")
text = text.replace("]", "")
return text if len(text) < 1024 else text[:1021] + "..."
def ratio(self, dic):
"""
Function that alculates the upvote/downvote ratio of the definition.
:param dic: the dictionary representing the definition
:return: the upvote/downvote ratio (float)
"""
return (100 * int(dic["thumbs_up"])) / (int(dic["thumbs_up"]) + int(dic["thumbs_down"])) \
if int(dic["thumbs_down"]) != 0 else 100.0
def defineDidier(self):
"""
Function that returns a stock dictionary to define Didier
in case people call it, or no definition was found.
:return: a dictionary that defines Didier
"""
return {"word": "Didier", "definition": "Didier", "example": "1: Didier\n2: Hmm?", "thumbs_up": 69420,
"thumbs_down": 0, "author": "Didier",
"link": "https://upload.wikimedia.org/wikipedia/commons/a/a5"
"/Didier_Reynders_in_Iranian_Parliament_02.jpg"}
def setup(client):
client.add_cog(Define(client))

View File

@ -1,567 +0,0 @@
from converters.numbers import Abbreviated, abbreviated
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from enums.numbers import Numbers
from functions import checks
from functions.database import currency, prison, stats
from functions.numbers import getRep
import json
import math
import random
def calcCapacity(level):
"""
Function that calculates the rob capacity for a given level.
:param level: the level of the user
:return: the capacity the user can rob (float)
"""
cap = 200
for x in range(level):
cap *= (math.pow(1.03, x))
return round(cap)
class Dinks(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Award", aliases=["Reward"], usage="[@Persoon] [Aantal]", hidden=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def award(self, ctx, user: discord.User, amount: Abbreviated):
"""
Command that awards a user a certain amount of Didier Dinks.
:param ctx: Discord Context
:param user: the user to give the Didier Dinks to
:param amount: the amount of Didier Dinks to award [user]
"""
# No amount was passed
if amount is None:
return
# Update the db
currency.update(user.id, "dinks", float(currency.dinks(user.id)) + float(amount))
# Gets the abbreviated representation of the amount
rep = getRep(amount, Numbers.t.value)
await ctx.send("**{}** heeft **{}** zowaar **{}** Didier Dink{} beloond!"
.format(ctx.author.display_name, self.utilsCog.getDisplayName(ctx, user.id), rep, checks.pluralS(amount)))
@commands.group(name="Dinks", aliases=["Cash"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def dinks(self, ctx):
"""
Command that shows the user's Didier Dinks & Platinum Dinks
:param ctx: Discord Context
"""
dinks = currency.dinksAll(ctx.author.id)
answer = "**{}** heeft **{:,}** Didier Dink{}"\
.format(ctx.author.display_name, math.floor(dinks["dinks"]), checks.pluralS(dinks["dinks"]))
if dinks["platinum"] > 0:
answer += " en **{}** Platinum Dink{}".format(dinks["platinum"], checks.pluralS(dinks["platinum"]))
await ctx.send(answer + "!")
@dinks.command(aliases=["Lb", "Leaderboards"], hidden=True)
@commands.check(checks.allowedChannels)
async def leaderboard(self, ctx):
"""
Command that shows the Didier Dinks Leaderboard.
Alias for Lb Dinks.
:param ctx: Discord Context
"""
await self.client.get_cog("Leaderboards").callLeaderboard("dinks", ctx)
@commands.command(name="Nightly")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def nightly(self, ctx):
"""
Command to claim daily Didier Dinks.
:param ctx: Discord Context
"""
response = currency.nightly(int(ctx.author.id))
if response[0]:
# Claim successful
await ctx.send("Je hebt je dagelijkse **{:,}** Didier Dinks geclaimt. :fire:**{}**".format(
response[1], response[2]))
else:
# Already claimed today, react PIPO
await ctx.send("Je kan dit niet meerdere keren per dag doen.")
reactCog = self.client.get_cog("ReactWord")
await reactCog.react(ctx, "pipo")
@commands.command(name="Give", aliases=["Gift"], usage="[@Persoon] [Aantal]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def give(self, ctx, person: discord.Member, amount: Abbreviated):
"""
Command that gives your Didier Dinks to another user.
:param ctx: Discord Context
:param person: user to give the Didier Dinks to
:param amount: the amount of Didier Dinks to give
"""
# Invalid amount
if amount is None:
return
# Disable DM abuse
if ctx.guild is None:
return await ctx.send("Muttn")
valid = checks.isValidAmount(ctx, amount)
if not valid[0]:
return await ctx.send(valid[1])
amount = float(valid[1])
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - amount)
currency.update(person.id, "dinks", float(currency.dinks(person.id)) + amount)
rep = getRep(math.floor(amount), Numbers.t.value)
await ctx.send("**{}** heeft **{}** zowaar **{}** Didier Dink{} geschonken!"
.format(ctx.author.display_name, person.display_name,
rep, checks.pluralS(amount)))
@commands.group(name="Bank", aliases=["B"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def bank(self, ctx):
"""
Command that shows the user's Didier Bank.
:param ctx: Discord Context
"""
# 0 1 2 3 4 5 6 7 8 9 10
# ID dinks level investedamount investeddays profit defense offense bc nightly streak
response = currency.getOrAddUser(ctx.author.id)
# Calculate the cost to level your bank
interestLevelPrice = round(math.pow(1.28, int(response[2])) * 300)
ratio = round(float(1 * (1 + (int(response[2]) * 0.01))), 4)
# Calculate the amount of levels the user can purchase
counter = 0
sumPrice = float(math.pow(1.28, int(response[2])) * 300)
while float(response[1]) + float(response[3]) + float(response[5]) > sumPrice:
counter += 1
sumPrice += round(float(math.pow(1.28, int(response[2]) + counter) * 300), 4)
maxLevels = "" if counter == 0 else " (+{})".format(str(counter))
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Bank van {}".format(ctx.author.display_name))
embed.set_thumbnail(url=str(ctx.author.avatar_url))
embed.add_field(name="Level:", value=str(response[2]) + maxLevels, inline=True)
embed.add_field(name="Ratio:", value=str(ratio), inline=True)
embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(interestLevelPrice), inline=False)
embed.add_field(name="Momenteel geïnvesteerd:", value="{:,}".format(math.floor(float(response[3]))), inline=False)
embed.add_field(name="Aantal dagen geïnvesteerd:", value=str(response[4]), inline=True)
embed.add_field(name="Huidige winst na claim:", value="{:,}".format(math.floor(response[5])), inline=False)
await ctx.send(embed=embed)
@bank.command(name="Stats")
async def stats(self, ctx):
"""
Command that shows the user's bank stats.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
# Calculate the prices to level stats up
defense = int(response[6])
defenseLevelPrice = math.floor(math.pow(1.4, defense) * 365) if defense < 38 else 5 * calcCapacity(defense - 6)
offense = int(response[7])
capacity = calcCapacity(offense)
offenseLevelPrice = math.floor(math.pow(1.5, offense) * 369) if offense < 32 else 5 * capacity
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Bank van {}".format(ctx.author.display_name))
embed.add_field(name="Offense:", value=str(offense), inline=True)
embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(int(offenseLevelPrice)), inline=True)
embed.add_field(name="Capaciteit:", value="{:,}".format(int(capacity)), inline=True)
embed.add_field(name="Security:", value=str(defense), inline=True)
embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(int(defenseLevelPrice)), inline=True)
await ctx.send(embed=embed)
@bank.group(name="Upgrade", aliases=["U"], case_insensitive=True, usage="[Categorie]", invoke_without_command=True)
async def upgrade(self, ctx):
"""
Command group to upgrade bank stats,
calling the group itself does nothing.
:param ctx: Discord Context
"""
pass
@upgrade.command(name="Level", aliases=["L"], hidden=True)
async def level(self, ctx):
"""
Command that upgrades the user's bank level,
increasing interest.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
interestLevelPrice = float(math.pow(1.28, int(response[2])) * 300)
# Check if user has enough Didier Dinks to do this
if float(response[1]) >= interestLevelPrice:
currency.update(ctx.author.id, "dinks", float(response[1]) - interestLevelPrice)
currency.update(ctx.author.id, "banklevel", int(response[2]) + 1)
await ctx.send("**{}** heeft zijn bank geüpgradet naar level **{}**!"
.format(ctx.author.display_name, str(int(response[2]) + 1)))
else:
await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**."
.format(ctx.author.display_name))
@upgrade.command(aliases=["Cap", "Capacity", "O", "Offence"], hidden=True)
async def offense(self, ctx):
"""
Command that upgrades the user's bank offense,
increasing capacity & rob chances.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
offense = int(response[7])
capacity = calcCapacity(offense)
offenseLevelPrice = math.floor(math.pow(1.5, offense) * 369) if offense < 32 else 5 * capacity
# Check if user has enough Didier Dinks to do this
if float(response[1]) >= offenseLevelPrice:
currency.update(ctx.author.id, "dinks", float(response[1]) - offenseLevelPrice)
currency.update(ctx.author.id, "offense", int(response[7]) + 1)
await ctx.send("**{}** heeft de offense van zijn bank geüpgradet naar level **{}**!"
.format(ctx.author.display_name, int(response[7]) + 1))
else:
await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**."
.format(ctx.author.display_name))
@upgrade.command(aliases=["D", "Defence", "Def", "Security"], hidden=True)
async def defense(self, ctx):
"""
Command that upgrades the user's bank defense,
increasing chance of failed robs by others.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
defense = int(response[6])
defenseLevelPrice = math.floor(math.pow(1.4, defense) * 365) if defense < 38 else 5 * calcCapacity(defense - 6)
# Check if user has enough Didier Dinks to do this
if float(response[1]) >= defenseLevelPrice:
currency.update(ctx.author.id, "dinks", float(response[1]) - defenseLevelPrice)
currency.update(ctx.author.id, "defense", int(response[6]) + 1)
await ctx.send("**{}** heeft de security van zijn bank geüpgradet naar level **{}**!"
.format(ctx.author.display_name, int(response[6]) + 1))
else:
await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**."
.format(ctx.author.display_name))
@commands.command(name="Invest", aliases=["Deposit"], usage="[Aantal]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def invest(self, ctx, *amount: Abbreviated):
"""
Command that invests Didier Dinks into the user's bank.
:param ctx: Discord Context
:param amount: the amount of Didier Dinks to invest
"""
# Tuples don't support assignment
amount = list(amount)
if len(amount) != 1:
await ctx.send("Geef een geldig bedrag op.")
elif not checks.isValidAmount(ctx, amount[0])[0]:
await ctx.send(checks.isValidAmount(ctx, amount[0])[1])
else:
user = currency.getOrAddUser(ctx.author.id)
if amount[0] == "all":
amount[0] = user[1]
amount[0] = float(amount[0])
currency.update(ctx.author.id, "investedamount", float(user[3]) + amount[0])
currency.update(ctx.author.id, "dinks", float(user[1]) - amount[0])
await ctx.send("**{}** heeft **{:,}** Didier Dink{} geïnvesteerd!"
.format(ctx.author.display_name, math.floor(amount[0]), checks.pluralS(amount[0])))
@commands.command(name="Claim", usage="[Aantal]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def claim(self, ctx, *args):
"""
Command that claims profit out of the user's Didier Bank.
:param ctx:
:param args:
:return:
"""
user = currency.getOrAddUser(ctx.author.id)
args = list(args)
claimAll = False
if len(args) == 0:
args.append("all")
if args[0] == "all":
args[0] = float(user[5])
claimAll = True
if not claimAll:
args[0] = abbreviated(str(args[0]))
if args[0] is None:
return await ctx.send("Dit is geen geldig bedrag.")
try:
# Checks if it can be parsed to int
_ = int(args[0])
args[0] = float(args[0])
# Can't claim more than you have (or negative amounts)
if args[0] < 0 or args[0] > float(user[5]):
raise ValueError
currency.update(ctx.author.id, "profit", float(user[5]) - args[0])
currency.update(ctx.author.id, "dinks", float(user[1]) + args[0])
s = stats.getOrAddUser(ctx.author.id)
stats.update(ctx.author.id, "profit", float(s[7]) + args[0])
# If you claim everything, you get your invest back as well & your days reset
if claimAll:
currency.update(ctx.author.id, "dinks", float(user[1]) + float(user[3]) + float(user[5]))
currency.update(ctx.author.id, "investedamount", 0.0)
currency.update(ctx.author.id, "investeddays", 0)
await ctx.send("**{}** heeft **{:,}** Didier Dink{} geclaimt!"
.format(ctx.author.display_name, math.floor(args[0] + float(user[3])),
checks.pluralS(math.floor(args[0] + float(user[3])))))
else:
await ctx.send("**{}** heeft **{:,}** Didier Dink{} geclaimt!".format(
ctx.author.display_name, math.floor(args[0]), checks.pluralS(math.floor(args[0]))))
except ValueError:
await ctx.send("Geef een geldig bedrag op.")
@commands.group(name="Rob", usage="[@Persoon]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def rob(self, ctx, target: discord.User):
"""
Command to rob another user.
:param ctx: Discord Context
:param target: the target victim to be robbed
:return:
"""
canRob, caller, target = await self.canRob(ctx, target)
if not canRob:
return
threshold = 50 + round(int(target[6]) * 0.7)
rg = random.randint(0 + int(caller[7]), 100)
stat = stats.getOrAddUser(ctx.author.id)
# Rob succeeded
if rg > threshold:
capacity = float(calcCapacity(caller[7]))
remaining = capacity
# Try robbing out of invest first, then Dinks pouch
amount = capacity if float(target[3]) >= capacity else float(target[3])
remaining -= amount
currency.update(target[0], "investedamount", float(target[3]) - amount)
# Rob out of Dinks pouch
if amount != capacity and not float(target[1]) < 1:
if float(target[1]) >= remaining:
amount += remaining
currency.update(target[0], "dinks", float(target[1]) - remaining)
else:
amount += float(target[1])
currency.update(target[0], "dinks", 0.0)
# Update db
currency.update(caller[0], "dinks", float(caller[1]) + amount)
await ctx.send("**{}** heeft **{:,}** Didier Dink{} gestolen van **{}**!".format(
ctx.author.display_name, math.floor(amount), checks.pluralS(math.floor(amount)),
self.utilsCog.getDisplayName(ctx, target[0])
))
stats.update(ctx.author.id, "robs_success", int(stat[2]) + 1)
stats.update(ctx.author.id, "robs_total", float(stat[4]) + amount)
else:
# Rob failed
# Calculate what happens
fate = random.randint(1, 10)
# Leave Dinks behind instead of robbing
if fate < 8:
punishment = float(calcCapacity(caller[7]))/2
prisoned = round(float(caller[1])) < round(punishment)
# Doesn't have enough Dinks -> prison
if prisoned:
diff = round(punishment - float(caller[1]))
punishment = round(float(caller[1]))
days = 1 + round(int(caller[7]) // 10)
prison.imprison(caller[0], diff, days,
round(round(diff)//days))
# Update db
currency.update(target[0], "dinks", float(target[1]) + punishment)
currency.update(caller[0], "dinks", float(caller[1]) - punishment)
await ctx.send("**{}** was zo vriendelijk om **{}** zowaar **{:,}** Didier Dink{} te geven!"
.format(ctx.author.display_name,
self.utilsCog.getDisplayName(ctx, target[0]),
math.floor(punishment), checks.pluralS(math.floor(punishment))))
# Can't put this in the previous if- because the value of Punishment changes
if prisoned:
await ctx.send("Je bent naar de gevangenis verplaatst omdat je niet genoeg Didier Dinks had.")
elif fate == 9:
# Prison
totalSum = round(calcCapacity(caller[7]))
days = 1 + (int(caller[7])//10)
prison.imprison(caller[0], totalSum, days, totalSum/days)
await ctx.send("**{}** niet stelen, **{}** niet stelen!\nJe bent naar de gevangenis verplaatst.".format(
ctx.author.display_name, ctx.author.display_name
))
else:
# Escape
await ctx.send("Je poging is mislukt, maar je kon nog net op tijd vluchten, **{}**."
"\nAllez, 't is goed voor ene keer e, deugeniet.".format(ctx.author.display_name))
stats.update(ctx.author.id, "robs_failed", int(stat[3]) + 1)
@rob.command(name="Leaderboard", aliases=["Lb", "Leaderboards"], hidden=True)
async def rob_leaderboard(self, ctx):
"""
Command that shows the Rob Leaderboard.
Alias for Lb Rob.
:param ctx: Discord Context
"""
await self.client.get_cog("Leaderboards").callLeaderboard("rob", ctx)
@rob.command(name="Stats", hidden=True)
async def rob_stats(self, ctx):
"""
Command that shows the user's rob stats.
Alias for Stats Rob.
:param ctx: Discord Context
"""
await self.client.get_cog("Stats").callStats("rob", ctx)
@commands.command(name="Prison", aliases=["Jail"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def prison(self, ctx):
"""
Command that shows how long you have to sit in prison for.
:param ctx: Discord Context
"""
user = prison.getUser(ctx.author.id)
if len(user) == 0:
await ctx.send("Je zit niet in de gevangenis, **{}**.".format(ctx.author.display_name))
return
user = user[0]
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="De Gevangenis")
embed.add_field(name="Borgsom:", value="{:,}".format(math.floor(user[1])), inline=False)
embed.add_field(name="Resterende dagen:", value="{}".format((user[2])), inline=False)
await ctx.send(embed=embed)
@commands.command(name="Bail")
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def bail(self, ctx):
"""
Command to bail yourself out of prison.
:param ctx: Discord Context
"""
user = prison.getUser(ctx.author.id)
if len(user) == 0:
return await ctx.send("Je zit niet in de gevangenis, **{}**.".format(ctx.author.display_name))
user = user[0]
# Check if user can afford this
valid = checks.isValidAmount(ctx, math.floor(user[1]))
if not valid[0]:
return await ctx.send(valid[1])
dinks = currency.dinks(ctx.author.id)
prison.remove(ctx.author.id)
currency.update(ctx.author.id, "dinks", float(dinks) - float(user[1]))
await ctx.send("**{}** heeft zichzelf vrijgekocht!".format(ctx.author.display_name))
# Update the user's stats
s = stats.getOrAddUser(ctx.author.id)
stats.update(ctx.author.id, "bails", int(s[10]) + 1)
# Increase the bail in the stats file
with open("files/stats.json", "r") as fp:
s = json.load(fp)
s["rob"]["bail_paid"] += float(user[1])
with open("files/stats.json", "w") as fp:
json.dump(s, fp)
async def canRob(self, ctx, target):
"""
Function that performs checks to see if a user can rob another user.
In case the rob is not possible, it already sends an error message to show this.
Returns the database dictionaries corresponding to these two users as they are
needed in this function anyways, so it prevents an unnecessary database call
in the rob command.
:param ctx: Discord Context
:param target: the target victim to be robbed
:return: success: boolean, user1 ("Caller"): tuple, user2 ("Target"): tuple
"""
# Can't rob in DM's
if str(ctx.channel.type) == "private":
await ctx.send("Dat doe je niet, {}.".format(ctx.author.display_name))
return False, None, None
# Can't rob bots
if str(ctx.author.id) in constants.botIDs:
await ctx.send("Nee.")
# Can't rob in prison
if len(prison.getUser(ctx.author.id)) != 0:
await ctx.send("Je kan niemand bestelen als je in de gevangenis zit.")
return False, None, None
# Check the database for these users
user1 = currency.getOrAddUser(ctx.author.id)
user2 = currency.getOrAddUser(target.id)
# Can't rob without Didier Dinks
if float(user1[1]) < 1.0:
await ctx.send("Mensen zonder Didier Dinks kunnen niet stelen.")
return False, None, None
# Target has no Didier Dinks to rob
if float(user2[1]) < 1.0 and float(user2[3]) < 1.0:
await ctx.send("Deze persoon heeft geen Didier Dinks om te stelen.")
return False, None, None
# Passed all tests
return True, user1, user2
def setup(client):
client.add_cog(Dinks(client))

View File

@ -1,345 +0,0 @@
from data import constants
import datetime
import discord
from discord.ext import commands
from functions import checks, easterEggResponses
from functions.database import stats, muttn
import pytz
import time
import traceback
class Events(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
self.failedChecksCog = self.client.get_cog("FailedChecks")
self.lastFeatureRequest = 0
self.lastBugReport = 0
@commands.Cog.listener()
async def on_connect(self):
"""
Function called when the bot connects to Discord.
"""
print("Connected")
@commands.Cog.listener()
async def on_ready(self):
"""
Function called when the bot is ready & done leading.
"""
# Change status
with open("files/status.txt", "r") as statusFile:
status = statusFile.readline()
await self.client.change_presence(status=discord.Status.online, activity=discord.Game(str(status)))
# Print a message in the terminal to show that he's ready
with open("files/readyMessage.txt", "r") as readyFile:
readyMessage = readyFile.readline()
print(readyMessage)
# Add constants to the client as a botvar
self.client.constants = constants.Live if "zandbak" not in readyMessage else constants.Zandbak
@commands.Cog.listener()
async def on_message(self, message):
"""
Function called when someone sends a message the bot can see.
:param message: the discord.Message instance of the message
"""
# Check if the server is locked, if so only allow me (to unlock) & Didier (to send the message) to talk
if self.client.locked \
and message.guild is not None \
and str(message.author.id) != constants.myId \
and str(message.author.id) != constants.didierId:
# Auto unlock when someone sends a message past the current time
if time.time() > self.client.lockedUntil:
return await self.unlock(message.channel)
return await self.utilsCog.removeMessage(message)
# If FreeGamesCheck failed, remove the message & send the user a DM
if not checks.freeGamesCheck(message):
await self.failedChecksCog.freeGames(message)
# Boos React to people that call him Dider
if "dider" in message.content.lower() and str(message.author.id) not in [constants.myId, constants.didierId, constants.coolerDidierId]:
await message.add_reaction("<:boos:629603785840263179>")
# Check for other easter eggs
eER = easterEggResponses.control(self.client, message)
if eER:
await message.channel.send(eER)
# Earn XP & Message count
stats.sentMessage(message)
@commands.Cog.listener()
async def on_command(self, ctx):
"""
Function called whenever someone invokes a command.
Logs commands in your terminal.
:param ctx: Discord Context
"""
DM = ctx.guild is None
print("{} in {}: {}".format(ctx.author.display_name,
"DM" if DM else "{} ({})".format(ctx.channel.name, ctx.guild.name),
ctx.message.content))
@commands.Cog.listener()
async def on_command_error(self, ctx, err):
"""
Function called when a command throws an error.
:param ctx: Discord Context
:param err: the error thrown
"""
# Zandbak Didier shouldn't spam the error logs
if self.client.user.id == int(constants.coolerDidierId):
raise err
# Don't handle commands that have their own custom error handler
if hasattr(ctx.command, 'on_error'):
return
# Someone just mentioned Didier without calling a real command,
# don't care about this error
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure, commands.TooManyArguments, commands.ExpectedClosingQuoteError), ):
pass
# Someone used a command that was on cooldown
elif isinstance(err, commands.CommandOnCooldown):
await ctx.send("Je kan dit commando niet (meer) spammen.", delete_after=10)
# Someone forgot an argument or passed an invalid argument
elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument)):
await ctx.send("Controleer je argumenten.")
else:
# Remove the InvokeCommandError because it's useless information
x = traceback.format_exception(type(err), err, err.__traceback__)
errorString = ""
for line in x:
if "direct cause of the following" in line:
break
errorString += line.replace("*", "") + "\n" if line.strip() != "" else ""
await self.sendErrorEmbed(ctx, err, errorString)
@commands.Cog.listener()
async def on_raw_reaction_add(self, react):
"""
Function called when someone adds a reaction to a message.
:param react: the RawReactionEvent associated with the reaction
"""
# Ignore RPS adding reacts
if self.client.get_user(react.user_id).bot:
return
# Feature request
if str(react.emoji) == "":
await self.sendReactEmbed(react, "Feature Request")
# Bug report
elif str(react.emoji) == "🐛":
await self.sendReactEmbed(react, "Bug Report")
# Muttn react
elif str(react.emoji) == "<:Muttn:761551956346798111>":
await self.addMuttn(react)
@commands.Cog.listener()
async def on_raw_reaction_remove(self, react):
"""
Function called when someone removes a reaction from a message.
:param react: the RawReactionEvent associated with the reaction
"""
# Decrease Muttn counter
if str(react.emoji) == "<:Muttn:761551956346798111>":
await self.removeMuttn(react)
async def removeMuttn(self, react):
"""
Function that decreases the Muttn counter for someone.
:param react: the RawReactionEvent associated with the reaction
"""
# Get the Message instance of the message
channel = self.client.get_channel(react.channel_id)
message = await channel.fetch_message(react.message_id)
muttn.removeMuttn(message)
async def addMuttn(self, react):
"""
Function that checks the Muttn counter for a message.
:param react: the RawReactionEvent associated with the reaction
"""
count = -1
# Get the Message instance of the message
channel = self.client.get_channel(react.channel_id)
message = await channel.fetch_message(react.message_id)
# Get the amount of reacts on this message
for reaction in message.reactions:
if str(reaction.emoji) == "<:Muttn:761551956346798111>":
count = reaction.count
for user in await reaction.users().flatten():
# Remove bot reacts
if user.bot:
count -= 1
break
# React was removed in the milliseconds the fetch_message needs to get the info
if count <= 0:
return
# Update the db
muttn.muttn(message.author.id, count, message.id)
def reactCheck(self, react, msg):
"""
Function that checks if feature requests/bug reports have been sent already.
:param react: the RawReactionEvent associated with the reaction
:param msg: the message this react was placed on
"""
# # Blacklist NinjaJay after spamming
# if react.user_id in [153162010576551946]:
# return False
# Don't spam DM's when something has already been reported
# Check if the react's count is 1
for reaction in msg.reactions:
if reaction.emoji == react.emoji.name:
return reaction.count == 1
async def sendReactEmbed(self, react, messageType):
"""
Function that sends a message in Zandbak with what's going on.
:param react: the RawReactionEvent associated with the reaction
:param messageType: the type of message to send
"""
channel = self.client.get_channel(react.channel_id)
msg = await channel.fetch_message(react.message_id)
# Didn't pass the check, ignore it
if not self.reactCheck(react, msg):
return
typeChannels = {"Feature Request": int(constants.FeatureRequests), "Bug Report": int(constants.BugReports)}
# Add a 10 second cooldown to requests/reports to avoid spam
# even tho apparently the people don't care
if round(time.time()) - (
self.lastFeatureRequest if messageType == "Feature Request" else self.lastBugReport) < 10:
await channel.send("Je moet even wachten vooraleer je nog een {} maakt.".format(messageType.lower()))
await msg.add_reaction("🕐")
return
# Report on an empty message
elif msg.content == "":
await channel.send("Dit bericht bevat geen tekst.")
await msg.add_reaction("")
return
# Update the variables
if messageType == "Feature Request":
self.lastFeatureRequest = round(time.time())
else:
self.lastBugReport = round(time.time())
# Ignore people reacting on Didier's messages
if str(msg.author.id) != constants.didierId:
# Get the user's User instance & the channel to send the message to
COC = self.client.get_guild(int(constants.CallOfCode))
user = COC.get_member(react.user_id)
targetChannel = self.client.get_channel(typeChannels[messageType])
await targetChannel.send("{} door **{}** in **#{}** ({}):\n``{}``\n{}".format(
messageType,
user.display_name,
channel.name if str(channel.type) != "private" else "DM",
channel.guild.name if str(channel.type) != "private" else "DM",
msg.content, msg.jump_url
))
await msg.add_reaction("")
@commands.Cog.listener()
async def on_message_edit(self, before, after):
"""
Function called when a message is edited,
so people can't edit messages in FreeGames to cheat the system.
:param before: the message before it was edited
:param after: the message after it was edited
"""
# Run the message through the checks again
if not checks.freeGamesCheck(after):
await self.failedChecksCog.freeGames(after)
async def sendErrorEmbed(self, ctx, error: Exception, trace):
"""
Function that sends an error embed in #ErrorLogs.
:param ctx: Discord Context
:param error: the error thrown
:param trace: the stacktrace of the error
"""
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Error")
embed.add_field(name="Command:", value="{} in {}: {}".format(ctx.author.display_name,
ctx.channel.name if str(
ctx.channel.type) != "private" else "DM",
ctx.message.content), inline=False)
embed.add_field(name="Error:", value=str(error)[:1024], inline=False)
embed.add_field(name="Message:", value=str(trace)[:1024], inline=False)
# Add remaining parts in extra fields
# (embed field limits)
if len(str(trace)) < 5500:
trace_split = [str(trace)[i:i + 1024] for i in range(1024, len(str(trace)), 1024)]
for spl in trace_split:
embed.add_field(name="\u200b", value=spl, inline=False)
errorChannel = self.client.get_channel(762668505455132722)
await errorChannel.send(embed=embed)
@commands.command(hidden=True)
@commands.check(checks.isMe)
async def lock(self, ctx, until=None):
"""
Command that locks the server during online exams.
:param ctx: Discord Context
:param until: the timestamp until which to lock (HH:MM)
"""
# No timestamp passed
if until is None:
return
until = until.split(":")
# Create timestamps
now = datetime.datetime.now()
untilTimestamp = time.time()
# Gets the current amount of minutes into the day
nowMinuteCount = (now.hour * 60) + now.minute
# Gets the target amount of minutes into the day
untilMinuteCount = (int(until[0]) * 60) + int(until[1])
# Adds the remaining seconds onto the current time to calculate the end of the lock
untilTimestamp += (60 * (untilMinuteCount - nowMinuteCount)) - now.second
self.client.locked = True
self.client.lockedUntil = round(untilTimestamp)
await ctx.send("De server wordt gelocked tot **{}**.".format(
datetime.datetime.fromtimestamp(untilTimestamp,
pytz.timezone("Europe/Brussels")
).strftime('%H:%M:%S')))
@commands.command(hidden=True)
@commands.check(checks.isMe)
async def unlock(self, ctx):
"""
Command to unlock the server manually before the timer is over.
:param ctx: Discord Context
"""
self.client.locked = False
self.client.lockedUntil = -1
await ctx.send("De server is niet langer gelocked.")
def setup(client):
client.add_cog(Events(client))

View File

@ -1,28 +0,0 @@
from data import constants
from discord.ext import commands
# Cog that handles failure of checks
# Has to be a Cog to have access to the Client
class FailedChecks(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# User posted in #FreeGames without being allowed to do so
async def freeGames(self, ctx):
content = ctx.content
errorChannel = self.client.get_channel(int(constants.ErrorLogs))
await self.utilsCog.removeMessage(ctx)
await self.utilsCog.sendDm(ctx.author.id,
"Je bericht \n`{}`\n werd verwijderd uit #FreeGames omdat het geen link "
"bevatte.\nPost AUB enkel links in dit kanaal.\n*Als je bericht onterecht "
"verwijderd werd, stuur dan een DM naar DJ STIJN.*".format(content))
await errorChannel.send("`{}`\nDoor **{}** werd verwijderd uit #FreeGames.".format(content,
ctx.author.display_name))
def setup(client):
client.add_cog(FailedChecks(client))

View File

@ -1,178 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import stringFormatters, checks
from functions.database import faq
class Faq(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="FAQ", usage="[Categorie]* [@Personen]*", case_insensitive=True, invoke_without_command=True)
@help.Category(category=Category.Other)
async def faq(self, ctx, *args):
"""
Command group that controls the FAQ commands.
When this command is invoked, it sends a list of valid categories.
After invoking in a subject's channel (without passing a category),
it sends the FAQ for that subject instead.
:param ctx: Discord Context
:param args: args passed
"""
# A category was requested
# This is not the cleanest but 80 subcommands is a bit much
if len(args) != 0 and any("@" not in arg for arg in args):
return await self.faqCategory(ctx, args)
# Check if the command was used in a subject's channel
if ctx.channel.id in constants.faq_channels:
return await self.faqCategory(ctx, (constants.faq_channels[ctx.channel.id],))
# List of all categories with the first letter capitalized
resp = [stringFormatters.titleCase(cat[0]) for cat in faq.getCategories()]
# Sort alphabetically
resp.sort()
# Create an embed with all the categories
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="FAQ Categorieën")
embed.description = "\n".join(resp)
# Check if the embed has to be sent to the user
# or if the user tagged anyone
if len(ctx.message.mentions) == 0:
await ctx.author.send(embed=embed)
else:
embed.set_footer(text="Doorgestuurd door {}".format(ctx.author.display_name))
# Send it to everyone that was mentioned
for person in ctx.message.mentions:
if not person.bot:
await person.send(embed=embed)
@faq.command(hidden=True, name="Add", usage="[Category] [Question]* [Answer]*")
@commands.check(checks.isMe)
async def add(self, ctx, category, question=None, answer=None, answer_markdown=None):
"""
Command to add a FAQ to the database
:param ctx: Discord Context
:param category: the category to add the FAQ to
:param question: the question
:param answer: the answer
:param answer_markdown: a version of the answer with markdown applied
"""
# Add a new category
if question is None or answer is None:
faq.addCategory(category)
await ctx.send("**{}** is toegevoegd.".format(category))
else:
# Add a new question/answer couple to a category
faq.addQuestion(category, question, answer, answer_markdown)
await ctx.send("``{}\n{}`` is toegevoegd in {}.".format(question, answer, category))
# Quotes a specific line of the fac instead of DM'ing the entire thing
@faq.command(name="Quote", aliases=["Q"], usage="[Categorie] [Index]")
@help.Category(category=Category.Other)
async def quote(self, ctx, category, index):
"""
Command that quotes 1 line of the FAQ into the current channel.
:param ctx: Discord Context
:param category: the category of the FAQ
:param index: the index in the list to quote
:return:y
"""
# Check if a (valid) number was passed
try:
index = int(index)
if index < 1:
raise ValueError
except ValueError:
await ctx.send("Dit is geen geldig getal.")
# Create a list of categories
categories = [t[0] for t in faq.getCategories()]
# Check if a valid category was passed
if category.lower() not in categories:
return await ctx.send("Dit is geen geldige categorie.")
resp = faq.getCategory(category.lower())
# Check if this index exists in this category
if len(resp) < index:
return await ctx.send("Dit is geen geldig getal.")
# Sort by entry Id
resp.sort(key=lambda x: int(x[0]))
await ctx.send("**{}**\n{}".format(resp[index - 1][2], resp[index - 1][3]))
async def faqCategory(self, ctx, args):
"""
Function that sends everything from a category.
:param ctx: Discord Context
:param args: the args passed
"""
# Create a list of categories
categories = [t[0] for t in faq.getCategories()]
# Random word was passed as a category
if not any(arg.lower() in categories for arg in args):
return await self.sendErrorEmbed(ctx, "Dit is geen geldige categorie.")
elif len(args) - len(ctx.message.mentions) != 1:
# Multiple categories were requested, which is not allowed
return await self.sendErrorEmbed(ctx, "Controleer je argumenten.")
category = ""
# Find the category the user requested
for word in args:
if word.lower() in categories:
category = word
break
resp = faq.getCategory(category.lower())
# Sort by entry Id
resp.sort(key=lambda x: int(x[0]))
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="FAQ {}".format(stringFormatters.titleCase(category)))
# Add everything into the embed
for i, pair in enumerate(resp):
# Add custom markdown if it exists
embed.add_field(name="#{}: {}".format(str(i + 1), pair[2]), value=pair[3] if pair[4] is None else pair[4], inline=False)
# Check if anyone was tagged to send the embed to
if len(ctx.message.mentions) == 0:
await ctx.author.send(embed=embed)
else:
embed.set_footer(text="Doorgestuurd door {}".format(ctx.author.display_name))
# Author tagged some people to send it to
for person in ctx.message.mentions:
await person.send(embed=embed)
async def sendErrorEmbed(self, ctx, message: str):
"""
Function that sends an error embed.
:param ctx: Discord Context
:param message: the message to put in the embed
"""
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Error")
embed.description = message
await ctx.send(embed=embed)
def setup(client):
client.add_cog(Faq(client))

View File

@ -1,89 +0,0 @@
from bs4 import BeautifulSoup
import datetime
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, config
import requests
import tabulate
class Football(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return checks.isMe(ctx) and not self.client.locked
@commands.group(name="Jpl", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(Category.Sports)
async def jpl(self, ctx, *args):
pass
@jpl.command(name="Matches", aliases=["M"], usage="[Week]*")
async def matches(self, ctx, *args):
args = list(args)
if not args:
args = [str(config.get("jpl_day"))]
if all(letter.isdigit() for letter in args[0]):
current_day = requests.get("https://api.sporza.be/web/soccer/matchdays/161733/{}".format(args[0])).json()
current_day = current_day["groupedMatches"][0]["matches"]
# Create dictionaries for every match
matches_formatted = {}
for i, match in enumerate(current_day):
matchDic = {"home": match["homeTeam"]["name"], "away": match["awayTeam"]["name"]}
# Add date
matchDate = datetime.datetime.strptime(match["startDateTime"].split("+")[0], "%Y-%m-%dT%H:%M:%S.%f")
matchDic["date"] = matchDate.strftime("%d/%m")
matchDic["day"] = self.get_weekday(matchDate.weekday())
# TODO check back when there's active games (to find the key in the dict) & add the current time if not over
# Add scores
if match["status"] == "END": # Status != [not_yet_started] whatever it is
matchDic["score"] = "{} - {}".format(match["homeScore"], match["awayScore"])
else:
# If there's no score, show when the match starts
matchDic["score"] = "{}:{}".format(
("" if len(str(matchDate.hour)) == 2 else "0") + str(matchDate.hour), # Leading Zero
("" if len(str(matchDate.minute)) == 2 else "0") + str(matchDate.minute)) # Leading Zero
matches_formatted[i] = matchDic
# Put every formatted version of the matches in a list
matchList = list([self.format_match(matches_formatted[match]) for match in matches_formatted])
await ctx.send("```Jupiler Pro League - Speeldag {}\n\n{}```".format(args[0], tabulate.tabulate(matchList, headers=["Dag", "Datum", "Thuis", "Stand", "Uit", "Tijd"])))
else:
return await ctx.send("Dit is geen geldige speeldag.")
# TODO check back when there's active games & add the timestamp instead of EINDE
def format_match(self, match):
return [match["day"], match["date"], match["home"], match["score"], match["away"], "Einde"]
def get_weekday(self, day: int):
days = ["Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"]
return days[day]
@jpl.command(name="Table", aliases=["Ranking", "Rankings", "Ranks", "T"])
async def table(self, ctx, *args):
page_html = requests.get("https://sporza.be/nl/categorie/voetbal/jupiler-pro-league/").text
bs_parsed = BeautifulSoup(page_html, "html.parser")
rows = bs_parsed.find(summary="algemeen klassement").find_all("tr")[1:]
rowsFormatted = []
for row in rows:
rowsFormatted.append(self.createRowList(row))
await ctx.send("```Jupiler Pro League Klassement\n\n{}```".format(tabulate.tabulate(rowsFormatted, headers=["#", "Ploeg", "Punten", "M", "M+", "M-", "M="])))
# Formats the row into an list that can be passed to Tabulate
def createRowList(self, row):
scoresArray = list([td.renderContents().decode("utf-8") for td in row.find_all("td")])[:6]
# Insert the team name into the list
scoresArray.insert(1, row.find_all("a")[0].renderContents().decode("utf-8").split("<!--")[0])
return scoresArray
def setup(client):
client.add_cog(Football(client))

View File

@ -1,182 +0,0 @@
from data import paginatedLeaderboard
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, mock, stringFormatters
from functions.database import memes, trump, dadjoke
import json
import os
import random
import requests
class Fun(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Dadjoke", aliases=["Dj", "Dad"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Fun)
async def dadjoke(self, ctx):
"""
Command that sends a random dad joke.
:param ctx: Discord Context
"""
await ctx.send(dadjoke.getRandomJoke())
@commands.command(name="Stalin", aliases=["Ss", "StalinMotivation", "Motivate"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Quotes)
async def stalin(self, ctx):
"""
Command that sends a random Stalin quote.
:param ctx: Discord Context
"""
with open("files/StalinMotivation.json", "r") as fp:
file = json.load(fp)
await ctx.send(file[random.randint(1, len(file))])
@commands.command(name="Satan", aliases=["S8n", "SatanQuote"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Quotes)
async def satan(self, ctx):
"""
Command that sends a random Satan quote.
:param ctx: Discord Context
"""
with open("files/SatanQuotes.json", "r") as fp:
file = json.load(fp)
await ctx.send(file[random.randint(1, len(file))])
@commands.command(name="Trump")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Quotes)
async def trump(self, ctx):
"""
Command that sends a random Trump quote.
:param ctx: Discord Context
"""
quote = trump.getRandomQuote()
await ctx.send("**\"{}**\"\n{} - {}".format(quote[1], quote[2], quote[3]))
@commands.command(name="8-Ball", aliases=["8b", "8Ball"], ignore_extra=True)
@help.Category(category=Category.Quotes)
async def eightball(self, ctx):
"""
Command that sends a random 8-ball response.
:param ctx: Discord Context
"""
with open("files/eightball.json", "r") as fp:
file = json.load(fp)
await ctx.send(file[random.randint(0, len(file) - 1)])
@commands.command(name="Memegen", usage="[Meme] [Velden]")
@commands.cooldown(1, 5, commands.BucketType.guild)
@help.Category(category=Category.Fun)
async def memegen(self, ctx, name="", *fields):
"""
Command that generates memes.
:param ctx: Discord Context
:param name: the name of the meme
:param fields: the fields to add to the meme
"""
if len(fields) == 0:
return await ctx.send("Controleer je argumenten.")
# Get the meme info that corresponds to this name
result = memes.getMeme(name)
# No meme found
if not result[0]:
return await ctx.send(result[1])
# Convert to list to support item assignment
fields = list(fields)
# If there's only one field, the user isn't required to use quotes
if result[1][2] == 1:
fields = [" ".join(fields)]
# Apply mock to mocking spongebob memes
if result[1][1] == "mocking spongebob":
fields = list(map(mock.mock, fields))
# X, X everywhere only takes X as an argument
if result[1][1] == "x, x everywhere":
fields[0] = " ".join(fields)
fields.append(fields[0] + " everywhere")
# List of fields to send to the API
boxes = [{"text": ""}, {"text": ""}, {"text": ""}, {"text": ""}]
# Add all fields required & ignore the excess ones
for i in range(len(fields)):
if i > 3:
break
boxes[i]["text"] = fields[i]
# Check server status
req = requests.get('https://api.imgflip.com/get_memes').json()
if req["success"]:
caption = {
"template_id": result[1][0],
"username": os.getenv("IMGFLIPNAME"),
"password": os.getenv("IMGFLIPPASSWORD"),
"boxes[0][text]": boxes[0]["text"],
"boxes[1][text]": boxes[1]["text"],
"boxes[2][text]": boxes[2]["text"],
"boxes[3][text]": boxes[3]["text"]
}
# Send the POST to the API
memeReply = requests.post('https://api.imgflip.com/caption_image', caption).json()
if memeReply['success']:
await ctx.send(str(memeReply['data']['url']))
await self.utilsCog.removeMessage(ctx.message)
else:
await ctx.send(
"Error! Controleer of je de juiste syntax hebt gebruikt. Gebruik het commando "
"\"memes\" voor een lijst aan geaccepteerde meme-namen.")
else:
await ctx.send("Er is een fout opgetreden.")
@commands.command(name="Memes")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Fun)
async def memes(self, ctx):
"""
Command that shows a list of memes in the database.
:param ctx: Discord Context
"""
memeList = memes.getAllMemes()
# Turn the list into a list of [Name: fields]
memeList = [": ".join([stringFormatters.titleCase(meme[1]),
str(meme[2])]) for meme in sorted(memeList, key=lambda x: x[1])]
pages = paginatedLeaderboard.Pages(source=paginatedLeaderboard.Source(memeList, "Memes", discord.Colour.blue()),
clear_reactions_after=True)
await pages.start(ctx)
@commands.command(name="Pjoke")
@help.Category(category=Category.Fun)
async def pjoke(self, ctx):
"""
Command that sends a random programming joke.
:param ctx: Discord Context
"""
r = requests.get("https://official-joke-api.appspot.com/jokes/programming/random").json()
await ctx.send(r[0]["setup"] + "\n" + r[0]["punchline"])
def setup(client):
client.add_cog(Fun(client))

View File

@ -1,257 +0,0 @@
from converters.numbers import Abbreviated, abbreviated
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, dinks
from functions.database import currency, stats
import json
import math
import random
class Games(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Coinflip", aliases=["Cf"], usage="[Inzet]* [Aantal]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def coinflip(self, ctx, *args):
"""
Command to flip a coin, optionally for Didier Dinks.
:param ctx: Discord Context
:param args: bet & wager
"""
args = list(args)
choices = ["Kop", "Munt"]
result = random.choice(choices)
# No choice made & no wager
if len(args) == 0:
await ctx.send("**{}**!".format(result))
self.updateStats("cf", "h" if result == "Kop" else "t")
return
# Check for invalid args
if len(args) == 1 or args[0][0].lower() not in "kmht":
return await ctx.send("Controleer je argumenten.")
args[1] = abbreviated(args[1])
valid = checks.isValidAmount(ctx, args[1])
# Invalid amount
if not valid[0]:
return await ctx.send(valid[1])
# Allow full words, abbreviations, and English alternatives
args[0] = "k" if args[0][0].lower() == "k" or args[0][0].lower() == "h" else "m"
won = await self.gamble(ctx, args[0], result, valid[1], 2)
if won:
s = stats.getOrAddUser(ctx.author.id)
stats.update(ctx.author.id, "cf_wins", int(s[8]) + 1)
stats.update(ctx.author.id, "cf_profit", float(s[9]) + float(valid[1]))
self.updateStats("cf", "h" if result == "Kop" else "t")
@coinflip.command(name="Stats", hidden=True)
async def cf_stats(self, ctx):
return await self.client.get_cog("Stats").callStats("cf", ctx)
@commands.group(name="Dice", aliases=["Roll"], usage="[Inzet]* [Aantal]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def dice(self, ctx, *args):
"""
Command to roll a dice, optionally for Didier Dinks.
:param ctx: Discord Context
:param args: bet & wager
"""
args = list(args)
result = random.randint(1, 6)
# No choice made & no wager
if len(args) == 0:
self.updateStats("dice", result)
return await ctx.send(":game_die: **{}**!".format(result))
# Check for invalid args
if len(args) == 1 or not args[0].isdigit() or not 0 < int(args[0]) < 7:
return await ctx.send("Controleer je argumenten.")
args[1] = abbreviated(args[1])
valid = checks.isValidAmount(ctx, args[1])
# Invalid amount
if not valid[0]:
return await ctx.send(valid[1])
await self.gamble(ctx, args[0], str(result), valid[1], 6, ":game_die: ")
self.updateStats("dice", result)
@dice.command(name="Stats", hidden=True)
async def dice_stats(self, ctx):
await self.client.get_cog("Stats").callStats("dice", ctx)
async def gamble(self, ctx, bet, result, wager, factor, pre="", post=""):
"""
Function for gambling because it's the same thing every time.
:param ctx: Discord Context
:param bet: the option the user bet on
:param result: randomly generated result
:param wager: size of the bet of the user
:param factor: the factor by which the person's wager is amplified
:param pre: any string that might have to be pre-pended to the output string
:param post: any string that might have to be appended to the output string
:return: a boolean indicating whether or not the user has won
"""
# Code no longer first removes your bet to then add profit,
# resulting in triple coinflip profit (@Clement).
# Subtract one off of the factor to compensate for the initial wager
factor -= 1
answer = "**{}**! ".format(result)
won = False
# Check if won
if result[0].lower() == bet[0].lower():
won = True
answer += "Je wint **{:,}** Didier Dink{}"
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) + (float(wager) * factor))
else:
answer += "Je hebt je inzet (**{:,}** Didier Dink{}) verloren"
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - float(wager))
self.loseDinks(round(float(wager)))
# If won -> multiple dinkS, if lost, it's possible that the user only bet on 1 dinK
await ctx.send(pre + answer.format(round(float(wager) * factor if won else float(wager)),
checks.pluralS(float(wager) * factor if won else float(wager))) +
", **{}**!".format(ctx.author.display_name))
return won
@commands.group(name="Slots", usage="[Aantal]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def slots(self, ctx, wager: Abbreviated = None):
"""
Command to play slot machines.
:param ctx: Discord Context
:param wager: the amount of Didier Dinks to bet with
"""
valid = checks.isValidAmount(ctx, wager)
# Invalid amount
if not valid[0]:
return await ctx.send(valid[1])
ratios = dinks.getRatios()
def randomKey():
return random.choice(list(ratios.keys()))
def generateResults():
return [randomKey(), randomKey(), randomKey()]
# Generate the result
result = generateResults()
textFormatted = "{}\n{}\n:yellow_square:{}:yellow_square:\n:arrow_right:{}:arrow_left::part_alternation_mark:\n" \
":yellow_square:{}:yellow_square: :red_circle:\n{}\n{}".format(
dinks.slotsHeader, dinks.slotsEmptyRow,
"".join(generateResults()), "".join(result), "".join(generateResults()),
dinks.slotsEmptyRow, dinks.slotsFooter)
await ctx.send(textFormatted)
# Everything different -> no profit
if len(set(result)) == 3:
await ctx.send("Je hebt je inzet (**{:,}** Didier Dinks) verloren, **{}**.".format(
math.floor(float(valid[1])), ctx.author.display_name
))
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - math.floor(float(valid[1])))
return
# Calculate the profit multiplier
multiplier = 1.0
for symbol in set(result):
multiplier *= ratios[symbol][result.count(symbol) - 1]
await ctx.send(":moneybag: Je wint **{:,}** Didier Dinks, **{}**! :moneybag:".format(
round(float(valid[1]) * multiplier, 2), ctx.author.display_name
))
currency.update(ctx.author.id, "dinks",
float(currency.dinks(ctx.author.id)) + (float(valid[1]) * multiplier) - math.floor(
float(valid[1])))
# Current Dinks - wager + profit
# Returns list of profits
@slots.command(name="Chart", aliases=["Symbols", "Profit"])
async def chart(self, ctx):
"""
Command to show the profit distributions for the Didier Slotmachines.
:param ctx: Discord Context
"""
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Slots Profit Chart")
ratios = dinks.getRatios()
# Add every symbol into the embed
for ratio in ratios:
embed.add_field(name=ratio, value="1: x{}\n2: x{}\n3: x{}".format(
str(ratios[ratio][0]), str(ratios[ratio][1]), str(ratios[ratio][2])
))
await ctx.send(embed=embed)
@commands.group(name="Lost", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def lost(self, ctx):
"""
Command that shows the amount of Didier Dinks that have been lost due to gambling.
:param ctx: Discord Context
"""
await ctx.send("Er zijn al **{:,}** Didier Dinks verloren sinds 13/03/2020."
.format(dinks.lost()))
@lost.command(name="Today")
async def today(self, ctx):
"""
Command that shows the amount of Didier Dinks lost today.
:param ctx: Discord Context
"""
await ctx.send("Er zijn vandaag al **{:,}** Didier Dinks verloren."
.format(dinks.lostToday()))
def updateStats(self, game, key):
"""
Function to update the stats file for a game.
:param game: the game to change the stats for
:param key: the key in the game's dict to update
"""
with open("files/stats.json", "r") as fp:
s = json.load(fp)
s[game][str(key)] += 1
with open("files/stats.json", "w") as fp:
json.dump(s, fp)
def loseDinks(self, amount):
"""
Function that adds Didier Dinks to the lost file.
:param amount: the amount of Didier Dinks lost
"""
with open("files/lost.json", "r") as fp:
fc = json.load(fp)
fc["lost"] = fc["lost"] + round(amount)
fc["today"] = fc["today"] + round(amount)
with open("files/lost.json", "w") as fp:
json.dump(fc, fp)
def setup(client):
client.add_cog(Games(client))

View File

@ -1,53 +0,0 @@
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
from functions.scraping import google_search
class Google(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Google", aliases=["Gtfm", "Search"], usage="[Query]", case_insensitive=True)
@help.Category(Category.Other)
async def google(self, ctx, *query):
if not query:
return await ctx.reply("Je hebt geen query opgegeven.", mention_author=True)
results, status = google_search(" ".join(query))
if results is None:
return await ctx.send("Er ging iets fout (Response {})".format(status))
# Filter out all Nones
elements = list(filter(lambda x: x is not None, results))
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Google Search")
# Empty list of results
if len(elements) == 0:
embed.description = "Geen resultaten gevonden."
return await ctx.reply(embed=embed, mention_author=False)
# Cut excess results out
if len(elements) > 10:
elements = elements[:10]
links = []
for index, (link, title) in enumerate(elements):
links.append("{}: [{}]({})".format(index + 1, title, link))
embed.description = "\n".join(links)
await ctx.reply(embed=embed, mention_author=False)
def setup(client):
client.add_cog(Google(client))

View File

@ -1,170 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import json
import os
import random
def randomWord():
lineb = random.randrange(os.stat("files/words-dutch.txt").st_size)
with open("files/words-dutch.txt", encoding="latin-1") as file:
file.seek(lineb)
file.readline()
return file.readline().rstrip().upper()
class Hangman(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Hangman", aliases=["Hm"], usage="[Letter]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def hangman(self, ctx, letter=None):
if letter and letter.strip():
greek = "αβΓγΔδεζηΘθικΛλμνΞξΠπρΣσςτυΦφχΨψΩω"
if len(letter) == 1 and (letter.isalpha() or letter.isdigit()) and letter not in greek:
await self.guessLetter(ctx, letter)
else:
await ctx.send("Dit is geen geldige letter.")
return
await self.gameStatus(ctx)
async def guessLetter(self, ctx, letter):
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
if letter.upper() in file["guessed"]:
await ctx.send("Deze letter is al eens geprobeerd.")
return
file["guessed"] += letter.upper()
if letter.upper() not in file["word"]:
file["guesses"] += 1
if int(file["guesses"] >= 9):
await ctx.send(self.endGame(file["word"]))
return
with open("files/hangman.json", "w") as fp:
json.dump(file, fp)
await self.gameStatus(ctx)
@hangman.command(name="Start", usage="[Woord]*")
async def start(self, ctx, *, word=None):
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
# Can't play two games at once
if file["word"]:
await ctx.send("Er is al een spel bezig.")
return
if word:
if not all(letter.isalpha() or letter.isdigit() or letter in [" "] for letter in word):
await ctx.send("Dit is geen geldig woord.")
return
# Can only supply your own words in DM
if str(ctx.channel.type) != "private":
await ctx.message.delete()
await ctx.author.send("Het is niet slim om hangman games aan te maken waar iedereen het kan zien."
"\nGebruik dit commando hier zodat niemand het woord op voorhand weet.")
return
# Choose a random word when none were passed
if not word:
word = randomWord()
with open("files/hangman.json", "w") as fp:
json.dump({"guessed": [], "guesses": 0, "word": word}, fp)
await self.gameStatus(ctx)
async def gameStatus(self, ctx):
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
if not file["word"]:
await ctx.send("Er is geen spel bezig.")
return
guessed = " ".join(letter for letter in file["guessed"] if letter not in file["word"])
filled = self.fillWord(file)
if filled.replace(" ", "") == file["word"]:
self.clearGame()
await ctx.send("**Het woord is geraden.**")
await ctx.message.add_reaction("")
return
await ctx.send("{}\n{}\nFoute letters: {}".format(filled, self.hangManString(file["guesses"]), guessed))
@hangman.command(name="Guess", usage="[Woord]")
async def guess(self, ctx, *, word=None):
if not word:
await ctx.send("Geef een woord op.")
return
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
if not file["word"]:
await ctx.send("Er is geen spel bezig.")
return
if word.upper() == file["word"].upper():
self.clearGame()
await ctx.send("**Het woord is geraden**")
await ctx.message.add_reaction("")
else:
file["guesses"] += 1
await ctx.send("**{}** is een foute gok.".format(word))
await ctx.message.add_reaction("")
if file["guesses"] >= 9:
await ctx.send(self.endGame(file["word"]))
else:
with open("files/hangman.json", "w") as fp:
json.dump(file, fp)
await self.gameStatus(ctx)
# Create a representation of the word by filling in letters that have been guessed, and dots otherwise
def fillWord(self, file):
return "**" + " ".join(
letter if letter in file["guessed"] else "." if letter.isalpha() or letter.isdigit() else letter for letter
in file["word"]) + "**"
def endGame(self, word):
self.clearGame()
return self.hangManString(9) + "\nHet woord was **{}**.".format(word)
def clearGame(self):
with open("files/hangman.json", "w") as fp:
json.dump({"guessed": [], "guesses": 0, "word": ""}, fp)
def hangManString(self, number):
dic = {
0: "\n \n \n \n",
1: "\n \n \n \n ===",
2: "\n |\n |\n |\n ===",
3: "--+---+\n |\n |\n |\n ===",
4: "--+---+\n | O\n |\n |\n ===",
5: "--+---+\n | O\n | |\n |\n ===",
6: "--+---+\n | O\n | /|\n |\n ===",
7: "--+---+\n | O\n | /|\\\n |\n ===",
8: "--+---+\n | O\n | /|\\\n | /\n ===",
9: "--+---+\n | O\n | /|\\\n | / \\\n ===",
}
return dic[number]
def setup(client):
client.add_cog(Hangman(client))

View File

@ -1,197 +0,0 @@
from data import constants
import discord
from discord.ext import commands
from enums.help_categories import categories, getCategory, Category
import json
class HelpCommand(commands.MinimalHelpCommand):
def __init__(self, **options):
super().__init__(**options)
self.ctx = None
self.target = None
self.is_mod = False
# Slightly modifying the original callback in order to
# allow sending help to DM's & checking for Enums
# while removing cog help & checking for mentions
async def command_callback(self, ctx, *, command=None):
self.ctx = ctx
self.is_mod = str(ctx.author.id) == constants.myId
bot = ctx.bot
if ctx.bot.locked:
return
if len(ctx.message.mentions) > 5:
return await ctx.send("Je kan Help maar naar maximaal 5 mensen doorsturen.")
# Send help categories
if command is None:
return await self.send_bot_help(self.get_bot_mapping())
# Check if command is a category
if command.lower() == "mod" and not self.is_mod:
return await self.send_error_message("Je hebt geen toegang tot deze commando's.")
category = getCategory(command, self.is_mod)
if category:
return await self.send_category_help(category)
# Cut the mentions out & split based on subcommand
spl = command.split(" ")
spl = spl[:len(spl) - len(self.ctx.message.mentions)]
# Turn dic to lowercase to allow proper name searching
all_commands = dict((k.lower(), v) for k, v in bot.all_commands.items())
if spl[0].lower() not in all_commands:
return await self.send_error_message(await self.command_not_found(spl[0]))
cmd = all_commands[spl[0].lower()]
# Check if the entered command path exists
for key in spl[1:]:
try:
all_commands = dict((k.lower(), v) for k, v in cmd.all_commands.items())
if key.lower() not in all_commands:
raise AttributeError
found = all_commands[key.lower()]
except AttributeError:
return await self.send_error_message(await self.subcommand_not_found(cmd, key))
cmd = found
# Subcommands should have the parent command's category
temp = cmd
while temp.parent is not None:
temp = temp.parent
# Don't allow non-mods to see mod commands
try:
if temp.callback.category == Category.Mod and not self.is_mod:
return await self.send_error_message("Je hebt geen toegang tot dit commando.")
except AttributeError:
return await self.send_error_message("Dit is geen (openbaar) commando.")
if isinstance(cmd, commands.Group):
return await self.send_group_help(cmd)
else:
return await self.send_command_help(cmd)
def get_bot_mapping(self):
return categories(self.is_mod)
# Sends list of commands in a category
async def send_category_help(self, category):
# Get a list of all commands in this category
category_commands = [command.name if not command.callback.unpack else command
for command in self.ctx.bot.commands
if hasattr(command.callback, "category") and command.callback.category == category]
# Unpack any groups that have to be unpacked
for command in list(category_commands):
if not isinstance(command, str):
category_commands.remove(command)
category_commands.extend([self.get_name(c) for c in self.unpack_group(command)])
embed = self.create_help_embed(category.value)
embed.add_field(name="Commands", value="\n".join(sorted(category_commands)))
for person in await self.get_destination():
await person.send(embed=embed)
async def send_bot_help(self, mapping):
embed = self.create_help_embed("Help")
embed.add_field(name="Categorieën", value="\n".join(sorted(mapping)))
await self.ctx.send(embed=embed)
async def send_command_help(self, command):
with open("files/help.json", "r") as fp:
helpFile = json.load(fp)
try:
helpDescription = helpFile[self.get_name(command).lower()]
except KeyError:
helpDescription = "Indien je dit leest is DJ STIJN vergeten om dit commando in de help page te zetten. Stuur hem een DM om hem eraan te herinneren."
embed = self.create_help_embed("Help")
embed.add_field(name=await self.get_command_signature(command),
value=await self.add_aliases_formatting(sorted(command.aliases)) + helpDescription)
for person in await self.get_destination():
await person.send(embed=embed)
async def send_group_help(self, group):
with open("files/help.json", "r") as fp:
helpFile = json.load(fp)
embed = self.create_help_embed(group.name + " Commando's")
try:
helpDescription = helpFile[self.get_name(group).lower()]
except KeyError:
helpDescription = "Indien je dit leest is DJ STIJN vergeten om dit commando in de help page te zetten. Stuur hem een DM om hem eraan te herinneren."
embed.add_field(name=await self.get_command_signature(group),
value=await self.add_aliases_formatting(sorted(group.aliases)) + helpDescription,
inline=False)
# Signature: Aliases - Usage
for subcommand in self.unpack_group(group):
embed.add_field(name=await self.get_command_signature(subcommand),
value=await self.add_aliases_formatting(sorted(subcommand.aliases)) +
helpFile[self.get_name(subcommand).lower()], inline=False)
for person in await self.get_destination():
await person.send(embed=embed)
# Allow mentioning people to send it to them instead
async def get_destination(self):
if self.ctx.message.mentions:
return set(mention for mention in self.ctx.message.mentions if not mention.bot)
return [self.ctx.author]
async def command_not_found(self, string):
return "Er bestaat geen commando met de naam **{}**".format(string)
async def subcommand_not_found(self, command, string):
return "**{}** heeft geen subcommando met de naam **{}**.".format(command.name, string)
async def get_command_signature(self, command):
return "{} {}".format(self.get_name(command), command.usage if command.usage is not None else "")
async def add_aliases_formatting(self, aliases):
return "*Alias: {}*\n".format(", ".join(aliases)) if aliases else ""
async def send_error_message(self, error):
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Help")
embed.add_field(name="Error", value=error)
await self.ctx.author.send(embed=embed)
def unpack_group(self, group):
# Create a list of all command objects in this group, in case they aren't hidden, sorted by name
subcommands = [group.all_commands.get(command) for command in group.all_commands]
subcommands.sort(key=lambda x: x.name)
subcommands = filter(lambda x: not x.hidden, subcommands)
return list(set(subcommands))
def get_name(self, command):
return command.qualified_name if command.parents else command.name
def create_help_embed(self, title):
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name=title)
embed.set_footer(text="Syntax: Didier Help [Categorie] of Didier Help [Commando]")
return embed
class Help(commands.Cog):
def __init__(self, client):
self.client = client
self._original_help_command = client.help_command
client.help_command = HelpCommand(command_attrs={"aliases": ["rtfm"]})
client.help_command.cog = self
def cog_unload(self):
self.client.help_command = self._original_help_command
def setup(client):
client.add_cog(Help(client))

View File

@ -1,54 +0,0 @@
from datetime import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import pytz
import requests
class Launch(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Launch", aliases=["SpaceX"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def launch(self, ctx, *args):
resp = self.getNextLaunch()
resp: dict = resp[list(resp.keys())[0]]
embed = discord.Embed(
colour=discord.Colour.blue()
)
embed.set_author(name="🚀 Volgende SpaceX lancering 🚀")
embed.add_field(name="Naam:", value=resp["name"], inline=False)
embed.add_field(name="Tijdstip:", value=resp["time"])
await ctx.send(embed=embed)
def getNextLaunch(self):
resp = requests.get("https://launchlibrary.net/1.3/launch?next=1&name=falcon").json()
if "status" in resp and (resp["status"] == "fail" or resp["status"] == "error"):
return {"error": "fail" if resp["status"].lower() == "fail" else "none found"}
resp = resp["launches"]
ret = {}
for launch in resp:
ret[launch["id"]] = {
"name": launch["name"],
"time": self.parseDate(launch["net"][:-4]) if launch["tbdtime"] == 0 else "TBA",
"TBA": launch["tbdtime"] == "0"
}
return ret
def parseDate(self, timestr):
d = datetime.strptime(timestr, "%B %d, %Y %H:%M:%S").timestamp()
return str(
datetime.fromtimestamp(int(d) + 7200, pytz.timezone("Europe/Brussels")).strftime('%B %d %Y om %H:%M:%S'))
def setup(client):
client.add_cog(Launch(client))

View File

@ -1,208 +0,0 @@
from data import paginatedLeaderboard
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from enums.numbers import Numbers
from functions import checks, xp
from functions.database import currency, stats, poke, muttn
import math
import requests
# TODO some sort of general leaderboard because all of them are the same
class Leaderboards(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*",
invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def leaderboard(self, ctx):
categories = ["Bitcoin", "Corona", "Dinks", "Messages", "Poke", "Rob", "XP"]
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Leaderboard Categorieën")
embed.description = "\n".join(categories)
await ctx.channel.send(embed=embed)
@leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True)
async def dinks(self, ctx):
entries = currency.getAllRows()
platDinks = currency.getAllPlatDinks()
# Take platinum dinks into account
for i, user in enumerate(entries):
if str(user[0]) in platDinks:
# Tuples don't support assignment, cast to list
user = list(user)
user[1] += platDinks[str(user[0])] * Numbers.q.value
entries[i] = user
boardTop = []
for i, user in enumerate(sorted(entries, key=lambda x: (float(x[1]) + float(x[3])), reverse=True)):
if i == 0 and float(user[1]) + float(user[3]) == 0.0:
return await self.emptyLeaderboard(ctx, "Dinks Leaderboard", "Er zijn nog geen personen met Didier Dinks.")
elif float(user[1]) + float(user[3]) > 0.0:
# Get the username in this guild
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, math.floor(float(user[1]) + float(user[3]))))
else:
boardTop.append("{} ({:,})".format(name, math.floor(float(user[1]) + float(user[3]))))
await self.startPaginated(ctx, boardTop, "Dinks Leaderboard")
@leaderboard.command(name="Corona", hidden=True)
async def corona(self, ctx):
result = requests.get("http://corona.lmao.ninja/v2/countries").json()
result.sort(key=lambda x: int(x["cases"]), reverse=True)
board = []
for land in result:
if land["country"] == "Belgium":
board.append("**{} ({:,})**".format(land["country"], land["cases"]))
else:
board.append("{} ({:,})".format(land["country"], land["cases"]))
await self.startPaginated(ctx, board, "Corona Leaderboard", discord.Colour.red())
@leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True)
async def bitcoin(self, ctx):
users = currency.getAllRows()
boardTop = []
for i, user in enumerate(sorted(users, key=lambda x: x[8], reverse=True)):
# Don't create an empty leaderboard
if i == 0 and float(user[8]) == 0.0:
return await self.emptyLeaderboard(ctx, "Bitcoin Leaderboard", "Er zijn nog geen personen met Bitcoins.")
elif float(user[8]) > 0.0:
# Only add people with more than 0
# Get the username in this guild
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, round(user[8], 8)))
else:
boardTop.append("{} ({:,})".format(name, round(user[8], 8)))
await self.startPaginated(ctx, boardTop, "Bitcoin Leaderboard")
@leaderboard.command(name="Rob", hidden=True)
async def rob(self, ctx):
users = list(stats.getAllRows())
boardTop = []
for i, user in enumerate(sorted(users, key=lambda x: x[4], reverse=True)):
# Don't create an empty leaderboard
if i == 0 and float(user[4]) == 0.0:
return await self.emptyLeaderboard(ctx, "Rob Leaderboard", "Er heeft nog niemand Didier Dinks gestolen.")
elif float(user[4]) > 0.0:
# Only add people with more than 0
# Get the username in this guild
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, math.floor(float(user[4]))))
else:
boardTop.append("{} ({:,})".format(name, math.floor(float(user[4]))))
await self.startPaginated(ctx, boardTop, "Rob Leaderboard")
@leaderboard.command(name="Poke", hidden=True)
async def poke(self, ctx):
s = stats.getAllRows()
blacklist = poke.getAllBlacklistedUsers()
boardTop = []
for i, user in enumerate(sorted(s, key=lambda x: x[1], reverse=True)):
if i == 0 and int(user[1]) == 0:
return await self.emptyLeaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.")
elif int(user[1]) == 0:
break
# Don't include blacklisted users
elif str(user[0]) not in blacklist:
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,})**".format(name, round(int(user[1]))))
else:
boardTop.append("{} ({:,})".format(name, round(int(user[1]))))
await self.startPaginated(ctx, boardTop, "Poke Leaderboard")
@leaderboard.command(name="Xp", aliases=["Level"], hidden=True)
async def xp(self, ctx):
s = stats.getAllRows()
boardTop = []
for i, user in enumerate(sorted(s, key=lambda x: x[12], reverse=True)):
if int(user[12]) == 0:
break
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} (Level {:,} | {:,} XP)**".format(name,
xp.calculate_level(round(int(user[12]))),
round(int(user[12]))))
else:
boardTop.append("{} (Level {:,} | {:,} XP)".format(name,
xp.calculate_level(round(int(user[12]))),
round(int(user[12]))))
await self.startPaginated(ctx, boardTop, "XP Leaderboard")
@leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True)
async def messages(self, ctx):
s = stats.getAllRows()
boardTop = []
message_count = stats.getTotalMessageCount()
for i, user in enumerate(sorted(s, key=lambda x: x[11], reverse=True)):
if int(user[11]) == 0:
break
perc = round(int(user[11]) * 100 / message_count, 2)
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({:,} | {}%)**".format(name, round(int(user[11])), perc))
else:
boardTop.append("{} ({:,} | {}%)".format(name, round(int(user[11])), perc))
await self.startPaginated(ctx, boardTop, "Messages Leaderboard")
@leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True)
async def muttn(self, ctx):
users = muttn.getAllRows()
boardTop = []
for i, user in enumerate(sorted(users, key=lambda x: x[1], reverse=True)):
if i == 0 and int(user[1]) == 0:
return await self.emptyLeaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.")
if float(user[1]) == 0:
break
name = self.utilsCog.getDisplayName(ctx, user[0])
if int(user[0]) == int(ctx.author.id):
boardTop.append("**{} ({})%**".format(name, round(float(user[1]), 2)))
else:
boardTop.append("{} ({}%)".format(name, round(float(user[1]), 2)))
await self.startPaginated(ctx, boardTop, "Muttn Leaderboard")
async def callLeaderboard(self, name, ctx):
await [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0](ctx)
async def startPaginated(self, ctx, source, name, colour=discord.Colour.blue()):
pages = paginatedLeaderboard.Pages(source=paginatedLeaderboard.Source(source, name, colour),
clear_reactions_after=True)
await pages.start(ctx)
async def emptyLeaderboard(self, ctx, name, message, colour=discord.Colour.blue()):
embed = discord.Embed(colour=colour)
embed.set_author(name=name)
embed.description = message
await ctx.send(embed=embed)
def setup(client):
client.add_cog(Leaderboards(client))

View File

@ -1,156 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import itertools
import random
class Minesweeper(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Minesweeper", aliases=["Ms"], usage="[Niveau]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def minesweeper(self, ctx, difficulty="m"):
if difficulty[0].lower() not in "emh":
await ctx.send("Geef een geldige moeilijkheidsgraad op.")
return
await ctx.send(self.createGame(difficulty[0].lower()))
def createGame(self, difficutly):
# [X, Y, BombCount]
dimensions = {
"e": [5, 5, 4],
"m": [10, 10, 20],
"h": [13, 13, 35]
}
numbers = {
0: "||:zero:||",
1: "||:one:||",
2: "||:two:||",
3: "||:three:||",
4: "||:four:||",
5: "||:five:||",
6: "||:six:||",
7: "||:seven:||",
8: "||:eight:||",
}
# Initialize an empty grid
grid = [[" " for _ in range(dimensions[difficutly][0])] for _ in range(dimensions[difficutly][1])]
# Generate every possible position on the grid
positions = set(itertools.product([x for x in range(len(grid))], repeat=2))
# Place the bombs in the grid randomly
for i in range(dimensions[difficutly][2]):
bombPosition = random.choice(list(positions))
positions.remove(bombPosition)
grid[bombPosition[0]][bombPosition[1]] = "||:boom:||"
# Add in the numbers representing the amount of bombs nearby
for i, row in enumerate(grid):
for j, cell in enumerate(row):
if cell == " ":
grid[i][j] = numbers[self.countBombs(grid, [i, j])]
# Reveal the biggest empty space to the player
self.revealSpaces(grid, self.findBiggestEmptySpace(grid))
# Join the grid into a string
li = [" ".join(row) for row in grid]
return "\n".join(li)
# Counts the amount of bombs near a given cell
def countBombs(self, grid, cell):
positions = [
[1, -1], [1, 0], [1, 1],
[0, -1], [0, 1],
[-1, -1], [-1, 0], [-1, 1]
]
count = 0
for position in positions:
if 0 <= cell[0] + position[0] < len(grid) and 0 <= cell[1] + position[1] < len(grid[0]):
if "boom" in grid[cell[0] + position[0]][cell[1] + position[1]]:
count += 1
return count
# Finds the biggest spot of 0's on the grid to reveal at the start
def findBiggestEmptySpace(self, grid):
spaces = []
biggest = []
# Floodfill
for i, row in enumerate(grid):
for j, cell in enumerate(row):
# Only check cells that aren't part of a space yet
if not any(cell in space for space in spaces) and "zero" in cell:
li = [[i, j]]
changed = True
while changed:
changed = False
for added in li:
neighb = self.neighbours(grid, added)
# Add all neighbours that have not yet been added to this space
for neighbour in neighb:
if neighbour not in li:
li.append(neighbour)
changed = True
spaces.append(li)
# If it's bigger than the current biggest, make it the new biggest
if len(li) > len(biggest):
biggest = li
return biggest
# Returns all neighbouring cells containing a 0
def neighbours(self, grid, cell):
positions = [
[1, 0],
[0, -1], [0, 1],
[-1, 0]
]
neighb = []
for position in positions:
if 0 <= cell[0] + position[0] < len(grid) and 0 <= cell[1] + position[1] < len(grid[0]):
if "zero" in grid[cell[0] + position[0]][cell[1] + position[1]]:
neighb.append([cell[0] + position[0], cell[1] + position[1]])
return neighb
# Take away the spoiler marks from the biggest empty space to help the player start
def revealSpaces(self, grid, emptySpaces):
positions = [
[1, -1], [1, 0], [1, 1],
[0, -1], [0, 1],
[-1, -1], [-1, 0], [-1, 1]
]
for space in emptySpaces:
grid[space[0]][space[1]] = ":zero:"
# Reveal all spaces around this one
for position in positions:
# Check if the space is not zero & is contained inside the grid & the space hasn't been cut before
if 0 <= space[0] + position[0] < len(grid) and \
0 <= space[1] + position[1] < len(grid[0]) and \
"zero" not in grid[space[0] + position[0]][space[1] + position[1]] and \
"||" in grid[space[0] + position[0]][space[1] + position[1]]:
# Cut the spoiler from this cell
grid[space[0] + position[0]][space[1] + position[1]] = grid[space[0] + position[0]][
space[1] + position[1]][2:-2]
def setup(client):
client.add_cog(Minesweeper(client))

View File

@ -1,199 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, config, timeFormatters
from functions.database import memes, githubs, twitch, dadjoke
import json
import os
class ModCommands(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog('Utils')
@commands.command(name="Remove", aliases=["Rm"], hidden=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def remove(self, ctx, message: str):
spl = message.split("/")
channel = self.client.get_channel(int(spl[-2]))
message = await channel.fetch_message(int(spl[-1]))
await message.delete()
# Load a cog
@commands.group(name="Load", usage="[Cog]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def load(self, ctx, extension: str):
try:
self.client.load_extension("cogs.{}".format(extension))
await self.sendDm(constants.myId, "Loaded **{}**".format(extension))
except discord.ext.commands.errors.ExtensionAlreadyLoaded:
await self.sendDm(constants.myId, "**{}** has already been loaded".format(extension))
@commands.command(name="Config", aliases=["Setup", "Set"], case_insensitive=True, usage="[Categorie] [Value]",
invoke_without_commands=True)
@commands.check(checks.isMe)
@help.Category(Category.Mod)
async def set(self, ctx, category, value):
if config.config(category, value):
await ctx.message.add_reaction("")
# Load all cogs except for modCommands
@load.command(name="All")
async def loadAll(self, ctx):
for file in os.listdir("./cogs"):
if file.endswith(".py") and not file == "modCommands.py":
await self.load(ctx, file[:-3])
# Unload a cog
@commands.group(name="Unload", usage="[Cog]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def unload(self, ctx, extension: str):
try:
self.client.unload_extension("cogs.{}".format(extension))
await self.sendDm(constants.myId, "Unloaded **{}**".format(extension))
except discord.ext.commands.errors.ExtensionNotLoaded:
await self.sendDm(constants.myId, "**{}** has already been unloaded".format(extension))
# Unload all cogs except for modCommands
@unload.command(name="All")
async def unloadAll(self, ctx):
for file in os.listdir("./cogs"):
if file.endswith(".py") and not file == "modCommands.py":
await self.unload(ctx, file[:-3])
# Reloads a cog
@commands.command(name="Reload", aliases=["Update"], usage="[Cog]")
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def reload(self, ctx, cog):
await self.unload(ctx, cog)
await self.load(ctx, cog)
await ctx.message.add_reaction("")
# Repeat what was said
@commands.command(name="Repeat", usage="[Text]")
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def repeat(self, ctx, *text):
await self.utilsCog.removeMessage(ctx.message)
await ctx.send(" ".join(text))
# Add a reaction to a message
@commands.command(name="Reac", aliases=["Reacc"], usage="[Emoji] [Id]")
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def reac(self, ctx, emoji, messageId):
channel = ctx.channel
# Check if the URL or the Id was passed
if messageId.count("/") > 3:
spl = messageId.split("/")
channel = self.client.get_channel(int(spl[-2]))
if channel is None:
return await ctx.send("Ik kan geen kanaal zien met dit Id.")
messageId = int(spl[-1])
await self.utilsCog.removeMessage(ctx.message)
message = await channel.fetch_message(messageId)
if message is None:
return await ctx.send("Ik kan geen bericht zien met dit Id.")
await message.add_reaction(emoji)
# Adds stuff into their databases
@commands.group(name="Add", usage="[Category] [Args]", case_insensitive=True, invoke_without_command=False)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def add(self, ctx):
pass
@add.command(name="Dadjoke", aliases=["Dj", "Dad"], usage="[Joke]")
async def dadjoke(self, ctx, *, joke):
dadjoke.addJoke(joke)
await ctx.send("Added ``{}``.".format(joke))
await ctx.message.add_reaction("")
@add.command(name="8-Ball", aliases=["8b", "Eightball", "8Ball"], usage="[Response]")
async def eightball(self, ctx, message):
with open("files/eightball.json", "r") as fp:
file = json.load(fp)
file.append(message)
with open("files/eightball.json", "w") as fp:
json.dump(file, fp)
# Adds a meme into the database
@add.command(name="Meme", aliases=["Mem"], usage="[Id] [Name] [Aantal Velden]")
async def meme(self, ctx, memeid, meme, fields):
await ctx.send(memes.insert(memeid, meme, fields)[1])
# Adds a person's GitHub into the database
@add.command(name="GitHub", aliases=["Gh", "Git"], usage="[Id] [Link]")
async def github(self, ctx, userid, link):
# Allow tagging to work as well
if len(ctx.message.mentions) == 1:
userid = ctx.message.mentions[0].id
githubs.add(userid, link)
await ctx.send("{}'s GitHub is toegevoegd aan de database.".format(self.utilsCog.getDisplayName(ctx, userid)))
# Adds a person's Twitch into the database
@add.command(name="Twitch", aliases=["Stream", "Streamer", "Tw"])
async def twitch(self, ctx, userid, link):
# Allow tagging to work as well
if len(ctx.message.mentions) == 1:
userid = ctx.message.mentions[0].id
twitch.add(userid, link)
await ctx.send("{}'s Twitch is toegevoegd aan de database.".format(self.utilsCog.getDisplayName(ctx, userid)))
@commands.command(name="WhoIs", aliases=["Info", "Whodis"], usage="[@User]")
@help.Category(Category.Mod)
async def whois(self, ctx, user):
if ctx.message.mentions:
user = ctx.message.mentions[0].id
user = await self.client.fetch_user(int(user))
if user is None:
return
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name=user.display_name, icon_url=user.avatar_url)
embed.add_field(name="Discriminator", value="#{}".format(user.discriminator))
embed.add_field(name="Discord id", value=user.id)
embed.add_field(name="Bot", value="Nee" if not user.bot else "Ja")
created_local = timeFormatters.epochToDate(user.created_at.timestamp())
embed.add_field(name="Account aangemaakt", value="{}\n({} geleden)".format(
created_local["date"], timeFormatters.diffYearBasisString(round(created_local["dateDT"].timestamp()))
), inline=False)
# Check if the user is in the current guild
if ctx.guild is not None:
member_instance = ctx.guild.get_member(user.id)
if member_instance is not None:
joined_local = timeFormatters.epochToDate(member_instance.joined_at.timestamp())
embed.add_field(name="Lid geworden van {} op".format(ctx.guild.name), value="{}\n({} Geleden)".format(
joined_local["date"], timeFormatters.diffYearBasisString(round(joined_local["dateDT"].timestamp()))
))
embed.add_field(name="Mention String", value=member_instance.mention, inline=False)
await ctx.send(embed=embed)
# Send a DM to a user -- Can't re-use Utils cog in (un)load because the cog might not be loaded
async def sendDm(self, userid, message: str):
user = self.client.get_user(int(userid))
await user.send(message)
def setup(client):
client.add_cog(ModCommands(client))

View File

@ -1,37 +0,0 @@
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
from functions.database import muttn
class Muttn(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Muttn", aliases=["HowMuttn", "M", "Mutn", "Mutten"], usage="[@Persoon]", case_insensitive=True, invoke_without_command=True)
@help.Category(Category.Fun)
async def muttn(self, ctx, member: discord.Member = None):
if member is None:
member = ctx.author
user = muttn.getOrAddUser(member.id)
embed = discord.Embed(colour=discord.Colour.blue(), title=member.display_name)
embed.set_author(name="Muttn-O'-Meter")
embed.add_field(name="Percentage", value="{}%".format(round(float(user[1]), 2)))
embed.add_field(name="Aantal {}'s".format("<:Muttn:761551956346798111>"), value=str(user[2]))
await ctx.send(embed=embed)
@muttn.command(name="Leaderboard", aliases=["Lb"], hidden=True)
async def lb(self, ctx):
await self.client.get_cog("Leaderboards").callLeaderboard("muttn", ctx)
def setup(client):
client.add_cog(Muttn(client))

View File

@ -1,153 +0,0 @@
from data import constants
import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, clap, mock, sunrise, timeFormatters
import pytz
import time
import urllib.parse
# Random things that are usually oneliners & don't belong in any other categories
class Oneliners(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog('Utils')
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Age", usage="[Formaat]*")
@help.Category(category=Category.Didier)
async def age(self, ctx, specification=None):
allowedSpecifications = ["d", "days", "m", "months", "w", "weeks", "y", "years"]
if specification is not None and specification.lower() not in allowedSpecifications:
await ctx.send("**{}** is geen geldig formaat.".format(specification))
return
if specification is None:
timeString = timeFormatters.diffYearBasisString(constants.creationDate)
else:
ageSeconds = round(time.time()) - constants.creationDate
timeFormat = timeFormatters.getFormat(specification)
timeString = str(timeFormatters.timeIn(ageSeconds, timeFormat)[0])
timeString += " " + timeFormatters.getPlural(int(timeString), timeFormat)
await ctx.send("Didier is **{}** oud.".format(timeString))
@commands.command(name="Clap", usage="[Tekst]")
@help.Category(category=Category.Other)
async def clap(self, ctx, *args):
await ctx.send(clap.clap("".join(args)))
await self.utilsCog.removeMessage(ctx.message)
@commands.command(name="Reverse", aliases=["Rev"], usage="[Tekst]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def reverse(self, ctx, *args):
await ctx.send(" ".join(args)[::-1])
@commands.command(name="Government", aliases=["Gov", "Regering"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def government(self, ctx):
now = timeFormatters.dateTimeNow()
newGov = datetime.datetime.fromtimestamp(1601539200, tz=pytz.timezone("Europe/Brussels"))
delta = now - newGov
zin = "Na **494** dagen is er weer een regering, **47** dagen te vroeg om het record te breken. Very sad times.\nMAAR hoelang denk je dat de nieuwe regering het gaat volhouden? Place your bets! Momenteel zitten we aan **{}** dag{}.".format(
delta.days, "en" if delta.days != 1 else ""
)
# now = int(time.time())
# valVorige = 1545350400
# verkiezingen = 1558828800
# valDiff = now - valVorige
# verkiezingenDiff = now - verkiezingen
# zin = (
# "We zitten al **%d** dagen zonder regering, en proberen al **%d** dagen een nieuwe te vormen.\nHet "
# "huidige wereldrecord is "
# "**541** dagen, dus nog **%d** dagen tot we het gebroken hebben." %
# (valDiff // 86400, verkiezingenDiff // 86400, 541 - int(verkiezingenDiff // 86400)))
await ctx.send(zin)
@commands.command()
async def inject(self, ctx):
await ctx.send("**{}** heeft wat code geïnjecteerd.".format(ctx.author.display_name))
@commands.command(name="Mock", usage="[Tekst]")
@help.Category(category=Category.Other)
async def mock(self, ctx, *text):
await ctx.channel.send("{} - **{}**".format(mock.mock(" ".join(text)), ctx.author.display_name))
await self.utilsCog.removeMessage(ctx.message)
@commands.command(name="Molest", usage="[@Persoon]")
async def molest(self, ctx):
if constants.didierId in ctx.message.content:
await ctx.send("Nee.")
elif str(ctx.author.id) in ctx.message.content or ctx.message.content == "molest me":
await ctx.send("I didn't know you swing that way, " + ctx.author.display_name)
elif "171671190631481345" in ctx.message.content:
await ctx.send("Nee")
else:
await ctx.send("https://imgur.com/a/bwA6Exn")
@commands.command(name="Changelog", aliases=["Cl", "Change", "Changes"])
@help.Category(category=Category.Didier)
async def changelog(self, ctx, *args):
await ctx.send("V2.0: <https://docs.google.com/document/d/1oa-9oc9yFnZ0X5sLJTWfdahtaL0vF8acLl-xMXA3a40/edit#>\n"
"V2.1: https://docs.google.com/document/d/1ezdJBTnKWoog4q9yJrgwfF4iGOn-PZMoBZgSNVYPtqg/edit#")
@commands.command(name="Todo", aliases=["List", "Td"])
@help.Category(category=Category.Didier)
async def todo(self, ctx, *args):
await ctx.send("https://trello.com/b/PdtsAJea/didier-to-do-list")
@commands.command(name="LMGTFY", aliases=["Dsfr"], usage="[Query]")
@help.Category(category=Category.Other)
async def lmgtfy(self, ctx, *, query=None):
if query:
await ctx.send("https://lmgtfy.com/?q={}&iie=1".format(urllib.parse.quote(query)))
@commands.command(name="Neck", aliases=["Necc"], usage="[Lengte]*")
@help.Category(category=Category.Fun)
async def neck(self, ctx, size=None):
if not size:
size = 1
try:
size = int(size)
if not 0 < size < 16:
raise ValueError
except ValueError:
return await ctx.send("Geef een geldig getal op.")
await ctx.send("<:WhatDidYou:744476950654877756>" + ("<:DoTo:744476965951504414>" * size) + "<:MyDrink:744476979939508275>")
@commands.command()
async def open(self, ctx):
# await ctx.send(file=discord.File("files/images/open_source_bad.jpg"))
await ctx.send("Shut, it already is.")
@commands.command()
async def sc(self, ctx, *args):
await ctx.send("http://take-a-screenshot.org/")
@commands.command(aliases=["os", "sauce", "src"])
async def source(self, ctx):
# await ctx.send("<https://bit.ly/31z3BuH>")
await ctx.send("https://github.com/stijndcl/didier")
@commands.command(aliases=["sunrise", "sunshine"])
async def sun(self, ctx):
s = sunrise.Sun()
await ctx.send(":sunny:: **{}**\n:crescent_moon:: **{}**".format(s.sunrise(), s.sunset()))
@commands.command(name="Tias", aliases=["TryIt"])
async def tias(self, ctx, *args):
await ctx.send("***Try it and see***")
def setup(client):
client.add_cog(Oneliners(client))

View File

@ -1,113 +0,0 @@
from data import constants
import datetime
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, timeFormatters
from functions.database import poke, stats
class Poke(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Poke", usage="[@Persoon]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def poke(self, ctx, member=None):
if not await self.pokeChecks(ctx):
return
member = ctx.message.mentions[0]
await ctx.send("**{}** heeft **{}** getikt. **{}** is hem!".format(
ctx.author.display_name, member.display_name, member.display_name))
# Add into the database
poke.update(ctx.author.id, member.id)
stats.update(member.id, "poked", int(stats.getOrAddUser(member.id)[1]) + 1)
@poke.command(name="Blacklist", aliases=["Bl"])
async def blacklist(self, ctx):
if poke.blacklisted(ctx.author.id):
await ctx.send("Je hebt jezelf al geblacklisted, {}.".format(ctx.author.display_name))
return
if str(poke.get()[0]) == str(ctx.author.id):
await ctx.send("Je kan jezelf niet blacklisten als je aan de beurt bent, {}.".format(
ctx.author.display_name))
return
poke.blacklist(ctx.author.id)
await ctx.send("**{}** heeft zichzelf geblacklisted en kan niet meer getikt worden.".format(
ctx.author.display_name))
@poke.command(aliases=["wl"], hidden=True)
async def whitelist(self, ctx, *user):
user = ctx.message.mentions[0].id
if not poke.blacklisted(user):
await ctx.send("Deze persoon is niet geblacklisted.")
return
poke.blacklist(user, False)
await ctx.send("**{}** heeft {} gewhitelisted.".format(
ctx.author.display_name, self.utilsCog.getDisplayName(ctx, user)))
@poke.command(name="Current")
async def current(self, ctx):
p = poke.get()
pokedTimeStamp = timeFormatters.epochToDate(int(p[1]))["dateDT"]
timeString = timeFormatters.diffDayBasisString(pokedTimeStamp)
await ctx.send("Het is al **{}** aan **{}**.".format(timeString, self.utilsCog.getDisplayName(ctx, p[0])))
@poke.command(hidden=True)
async def me(self, ctx):
await ctx.send("Liever niet.")
@poke.command(hidden=True)
@commands.check(checks.isMe)
async def reset(self, ctx):
new = poke.reset()
await ctx.send("Poke is gereset. <@!{}> is hem!".format(str(new)))
@poke.command(aliases=["Lb", "Leaderboards"], hidden=True)
async def leaderboard(self, ctx):
await self.client.get_cog("Leaderboards").callLeaderboard("poke", ctx)
async def pokeChecks(self, ctx):
if len(ctx.message.mentions) == 0:
await ctx.send("Dit is geen geldige persoon.")
return False
if len(ctx.message.mentions) > 1:
await ctx.send("Je kan maar 1 persoon tegelijk tikken.")
return False
if ctx.message.mentions[0].id == ctx.author.id:
await ctx.send("Je kan jezelf niet tikken, {}.".format(ctx.author.display_name))
return False
if str(ctx.message.mentions[0].id) == constants.didierId:
await ctx.send("Je kan me niet tikken, {}.".format(ctx.author.display_name))
return False
if str(ctx.message.mentions[0].id) in constants.botIDs:
await ctx.send("Je kan geen bots tikken, {}.".format(ctx.author.display_name))
return False
# Check database things
p = poke.get()
if str(p[0]) != str(ctx.author.id):
await ctx.send("Het is niet jouw beurt, {}.".format(ctx.author.display_name))
return False
if str(ctx.message.mentions[0].id) == str(p[2]):
await ctx.send("Je mag niet terugtikken, {}.".format(ctx.author.display_name))
return False
if poke.blacklisted(ctx.message.mentions[0].id):
await ctx.send("Deze persoon heeft zichzelf geblacklisted en kan niet meer getikt worden.")
return False
return True
def setup(client):
client.add_cog(Poke(client))

View File

@ -1,44 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import requests
import urllib.parse
class QR(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="QR", usage="[Tekst]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def QR(self, ctx, *link):
if len(link) != 1:
await ctx.send(file=discord.File("files/images/ngguuqr.png"))
await self.client.get_cog("Utils").removeMessage(ctx.message)
else:
self.generate("".join(link))
await ctx.send(file=discord.File("files/images/qrcode.png"))
self.remove()
await self.client.get_cog("Utils").removeMessage(ctx.message)
def generate(self, link):
fileContent = requests.get(
"https://image-charts.com/chart?chs=999x999&cht=qr&chl={}&choe=UTF-8&chof=.png".format(
urllib.parse.quote(link))).content
with open("files/images/qrcode.png", "wb+") as fp:
fp.write(fileContent)
def remove(self):
import os
os.remove("files/images/qrcode.png")
def setup(client):
client.add_cog(QR(client))

View File

@ -1,135 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import colours
import random
import requests
import json
class Random(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Creates an alias
@commands.command(name="Choice", aliases=["Choose"], usage="[Argumenten]")
async def choose(self, ctx, *options):
await self.choice(ctx, options)
@commands.command(name="Shuffle", usage="[Argumenten]")
async def _shuffle(self, ctx, *options):
await self.shuffle(ctx, options)
@commands.group(name="Random", aliases=["R", "Rand", "RNG"], case_insensitive=True, invoke_without_command=True)
@help.Category(category=Category.Random, unpack=True)
async def random(self, ctx):
pass
@random.command(name="Choice", usage="[Argumenten]")
async def choice(self, ctx, *options):
if not options or not options[0]:
await ctx.send("Geef een geldige reeks op.")
return
await ctx.send(random.choice(options))
@random.command(name="Number", aliases=["Int"], usage="[Van]* [Tot]*")
async def number(self, ctx, to=100, start=1):
# This allows number(to) to work, as well as number(start, to)
if start > to:
start, to = to, start
await ctx.send(random.randint(start, to))
@number.error
async def on_number_error(self, ctx, error):
if isinstance(error, discord.ext.commands.BadArgument):
await ctx.send("Dit is geen geldig getal.")
else:
raise error
@random.command(name="Name")
async def name(self, ctx):
try:
name = requests.get("https://randomuser.me/api/").json()
except json.decoder.JSONDecodeError:
await ctx.send("Er ging iets mis. Probeer het opnieuw.")
return
name = name["results"][0]["name"]
await ctx.send("{} {} {}".format(name["title"], name["first"], name["last"]))
@random.command(name="Identity", aliases=["Id"])
async def identity(self, ctx):
try:
identity = requests.get("https://randomuser.me/api/").json()
except json.decoder.JSONDecodeError:
await ctx.send("Er ging iets mis. Probeer het opnieuw.")
return
identity = identity["results"][0]
name = identity["name"]
name = "{} {} {}".format(name["title"], name["first"], name["last"])
gender = identity["gender"]
street = "{} {}".format(identity["location"]["street"]["number"], identity["location"]["street"]["name"])
location = "{}, {}, {}, {}".format(street, identity["location"]["city"],
identity["location"]["state"], identity["location"]["country"])
age = identity["dob"]["age"]
await ctx.send("{}\n{}, {}\n{}".format(name, age, gender, location))
@random.command(name="Shuffle", aliases=["Order"], usage="[Argumenten]")
async def shuffle(self, ctx, *args):
if not args:
await ctx.send("Geef een geldige reeks op.")
return
# Allows shuffle alias to pass in it's args too
if isinstance(args[0], tuple):
args = args[0]
args = list(args)
random.shuffle(args)
await ctx.send(" - ".join(args))
@random.command(name="Colour", aliases=["Color"])
async def colour(self, ctx):
r, g, b = colours.randomRGB()
embed = discord.Embed(colour=discord.Colour.from_rgb(r, g, b))
embed.set_author(name="Random Colour")
embed.add_field(name="RGB", value="{}, {}, {}".format(r, g, b), inline=False)
embed.add_field(name="HEX", value=colours.RGBToHEX(r, g, b), inline=False)
embed.add_field(name="HSL", value="{}°, {}%, {}%".format(*colours.RGBToHSL(r, g, b)), inline=False)
embed.add_field(name="HSV", value="{}°, {}%, {}%".format(*colours.RGBToHSV(r, g, b)), inline=False)
await ctx.send(embed=embed)
@random.command(name="Timestamp", aliases=["Time", "Ts"])
async def timestamp(self, ctx):
hour = str(random.randint(0, 23))
hour = ("0" if len(hour) == 1 else "") + hour
minutes = str(random.randint(0, 23))
minutes = ("0" if len(minutes) == 1 else "") + minutes
await ctx.send("{}:{}".format(hour, minutes))
@random.command(name="Fact", aliases=["Knowledge"])
async def fact(self, ctx):
randomFact = requests.get("https://uselessfacts.jsph.pl/random.json?language=en").json()
await ctx.send(randomFact["text"])
@commands.command(name="Yes/No", aliases=["Yn"])
@help.Category(Category.Random)
async def yesno(self, ctx):
await ctx.send(random.choice(["Ja.", "Nee."]))
def setup(client):
client.add_cog(Random(client))

View File

@ -1,75 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import reactWord
class ReactWord(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="React", usage="[Tekst] [Message id/url]*")
@help.Category(category=Category.Other)
async def react(self, ctx, *words):
words = list(words)
target = False
channel = ctx.channel
# Check if the URL or the Id was passed
if str(words[-1]).count("/") > 3:
spl = str(words[-1]).split("/")
channel = self.client.get_channel(int(spl[-2]))
if channel is None:
return await ctx.send("Ik kan geen kanaal zien met dit id.")
words[-1] = spl[-1]
# Get the message object if an Id was passed, otherwise react to the message itself
try:
message = await channel.fetch_message(words[-1])
if message is None:
return await ctx.send("Ik kan geen bericht zien met dit id.")
target = True
except discord.HTTPException:
message = ctx.message
# Reactions that were added before this command was executed
previousReactions = ([x.emoji for x in message.reactions]) if len(message.reactions) != 0 else []
eligible, arr = reactWord.check(list(words), previousReactions)
if not eligible:
await ctx.send(arr[0])
else:
if target:
await self.utilsCog.removeMessage(ctx.message)
for reac in arr:
await message.add_reaction(reac)
@commands.command(name="Character", aliases=["Char"], usage="[Karakter]")
@help.Category(category=Category.Other)
async def char(self, ctx, char: str = None):
# Nothing passed
if char is None:
return await ctx.send("Controleer je argumenten")
char = char.lower()
# Not 1 char passed
if len(char) != 1 or char not in reactWord.allowedCharacters():
return await ctx.send("Dit is geen geldig karakter.")
var = reactWord.getAllVariants(char)
return await ctx.send("**Karakter**: {}\nOpties (**{}**): {}".format(
char, len(var), " ".join(var)
))
def setup(client):
client.add_cog(ReactWord(client))

View File

@ -1,138 +0,0 @@
import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import requests
import time
class Release(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Gets upcoming game releases
@commands.group(name="Releases", usage="[Pagina]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def releases(self, ctx, page="1"):
for char in page:
if not char.isdigit():
await ctx.send("Geef een geldige pagina op.")
return
dates = self.getDates()
resp = requests.get("https://api.rawg.io/api/games?dates={},{}&page_size=25&page={}&ordering=released".format(
dates[0], dates[1], page
)).json()
try:
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Volgende Game Releases | Pagina {}".format(page))
embed.description = "\n".join(
["{} (#{}): {}".format(result["name"], result["id"], self.rewriteDate(result["released"]))
for result in resp["results"]])
embed.set_footer(text="Voor gedetailleerde info: Didier Game Info [id]")
except KeyError:
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Game Releases")
embed.add_field(name="Error", value="Er ging iets fout.")
await ctx.send(embed=embed)
def getDates(self):
today = datetime.datetime.fromtimestamp(time.time())
nextMonth = datetime.datetime.fromtimestamp(time.time() + 2629743)
return ["-".join([str(today.year), self.leadingZero(str(today.month)), self.leadingZero(str(today.day))]),
"-".join([str(nextMonth.year),
self.leadingZero(str(nextMonth.month)), self.leadingZero(str(nextMonth.day))])]
def leadingZero(self, num):
return num if len(num) == 2 else "0" + num
# Shows more detailed information for a game
@releases.command(name="Info", aliases=["Details"], usage="[Game Id]")
async def info(self, ctx, *, game_id):
game_id = self.create_slug(game_id)
resp = requests.get("https://api.rawg.io/api/games/{}".format(str(game_id))).json()
if "redirect" in resp:
resp = requests.get("https://api.rawg.io/api/games/{}".format(resp["slug"])).json()
if "Not found." in resp.values():
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Game Info")
embed.description = "Er is geen game gevonden met deze id of naam."
else:
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Game Info")
embed.add_field(name="Naam", value=resp["name"])
embed.add_field(name="Id", value=resp["id"])
embed.add_field(name="Datum", value="TBA" if resp["tba"] else self.rewriteDate(resp["released"]))
embed.add_field(name="Platforms", value=", ".join(self.getPlatforms(resp)))
embed.add_field(name="Stores", value=", ".join(self.getStores(resp)))
embed.add_field(name="Genres", value=", ".join(self.getGenres(resp)))
embed.add_field(name="Tags", value=self.getTags(resp), inline=False)
embed.add_field(name="Description", value=self.writeDescription(resp["description_raw"]), inline=False)
await ctx.send(embed=embed)
# Turns name into a slug
def create_slug(self, game_id):
try:
# Check if it's a number
game_id = int(game_id)
return str(game_id)
except ValueError:
game_id = game_id.lower().replace(" ", "-").replace(":", "").replace("'", "")
return game_id
def rewriteDate(self, date):
date = date.split("-")
return "-".join(reversed(date))
def getGenres(self, release):
return sorted([genre["name"] for genre in release["genres"]])
# Returns a list of all platforms this game is available on
def getPlatforms(self, release):
return sorted([platform["platform"]["name"] for platform in release["platforms"]])
# Returns a list of all stores this game is available on
def getStores(self, release):
return sorted(store["store"]["name"] for store in release["stores"])
# Returns a list of all tags associated with this game
def getTags(self, release):
if len(release["tags"]) == 0:
return "N/A"
li = sorted([tag["name"] for tag in release["tags"]])
st = li[0]
for tag in li[1:]:
if len(st) + 2 + len(tag) > 1024:
break
st += ", " + tag
return st if st else "N/A"
# Truncates the description if necessary
def writeDescription(self, description):
if len(description) > 700:
return description[:697] + "..."
return description if description else "N/A"
@info.error
async def info_on_error(self, ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.send("Geef een geldig getal op.")
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send("Controleer je argumenten.")
else:
raise error
def setup(client):
client.add_cog(Release(client))

View File

@ -1,56 +0,0 @@
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
from functions.database import remind
class Remind(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Remind", aliases=["Remindme"], usage="[Categorie]", case_insensitive=True, invoke_without_command=True)
@help.Category(Category.Other)
async def remind(self, ctx):
"""
Command group to remind the user of a certain thing every day.
:param ctx: Discord Context
"""
rows = remind.getOrAddUser(ctx.author.id)
# TODO use a loop for this when not lazy
categories = [remind.getIcon(rows[1]) + " Nightly", remind.getIcon(rows[2]) + " Les"]
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Remind Categorieën")
embed.description = "\n".join(sorted(categories))
await ctx.send(embed=embed)
@remind.command(name="Nightly")
async def nightly(self, ctx):
"""
Command to get a daily Nightly reminder
"""
if remind.switchReminder(ctx.author.id, "nightly"):
await ctx.send("Vanaf nu word je er dagelijks aan herinnerd om Didier Nightly te doen.")
else:
await ctx.send("Je zal er niet langer aan herinnerd worden om Didier Nightly te doen.")
@remind.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"])
async def les(self, ctx):
"""
Command to get a daily reminder with an embed of your schedule
"""
if remind.switchReminder(ctx.author.id, "les"):
await ctx.send("Vanaf nu krijg je dagelijks je lessenrooster toegestuurd.")
else:
await ctx.send("Je zal je lessenrooster niet langer toegestuurd krijgen.")
def setup(client):
client.add_cog(Remind(client))

View File

@ -1,108 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.courses import years
from enums.help_categories import Category
from functions import checks, eten, les
import json
class School(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Eten", aliases=["Food", "Menu"], usage="[Dag]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.School)
async def eten(self, ctx, *day):
day = les.getWeekDay(None if len(day) == 0 else day)[1]
# Create embed
menu = eten.etenScript(day)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Menu voor {}".format(day))
if "gesloten" in menu[0].lower():
embed.description = "Restaurant gesloten"
else:
embed.add_field(name="Soep:", value=menu[0], inline=False)
embed.add_field(name="Hoofdgerechten:", value=menu[1], inline=False)
if menu[2]:
embed.add_field(name="Groenten:", value=menu[2], inline=False)
embed.set_footer(text="Omwille van de coronamaatregelen is er een beperkter aanbod, en kan je enkel nog eten afhalen. Ter plaatse eten is niet meer mogelijk.")
await ctx.send(embed=embed)
@commands.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"], usage="[Jaargang]* [Dag]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.School)
async def les(self, ctx, *day):
embed = discord.Embed(colour=discord.Colour.blue())
# These are hardcoded because they don't change & don't matter anymore in a month
embed.set_author(name="Examenrooster 2de Bachelor")
embed.add_field(name="Algoritmen en Datastructuren 2",
value="Woensdag 06/01/2021\n13:00 - 16:00\nFlanders Expo, Hal 4D", inline=False)
embed.add_field(name="Functioneel Programmeren",
value="Dinsdag 12/01/2021\n13:00 - 16:00\nFlanders Expo, Hal 5C", inline=False)
embed.add_field(name="Systeemprogrammeren",
value="Maandag 18/01/2021\n08:30 - 11:30\nOnline, schriftelijk", inline=False)
embed.add_field(name="Communicatienetwerken",
value="Vrijdag 22/01/2021\n13:00 - 16:00\nFlanders Expo Hal 4A", inline=False)
embed.add_field(name="Statistiek en Probabiliteit",
value="Vrijdag 29/01/2021\n08:30 - 11:30\nCampus Ardoyen Locus, PC Resto Locus", inline=False)
await ctx.send(embed=embed)
# Les code
# parsed = les.parseArgs(day)
#
# # Invalid arguments
# if not parsed[0]:
# return await ctx.send(parsed[1])
#
# day, dayDatetime, semester, year = parsed[1:]
#
# # Customize the user's schedule
# schedule = self.customizeSchedule(ctx, year, semester)
#
# # Create the embed
# embed = les.createEmbed(day, dayDatetime, semester, year, schedule)
#
# await ctx.send(embed=embed)
# Add all the user's courses
def customizeSchedule(self, ctx, year, semester):
schedule = les.getSchedule(semester, year)
COC = self.client.get_guild(int(constants.CallOfCode))
if COC is None:
return schedule
member = COC.get_member(ctx.author.id)
for role in member.roles:
for univYear in years:
for course in univYear:
if course.value["year"] < year and course.value["id"] == role.id and course.value["semester"] == semester:
with open("files/schedules/{}{}.json".format(course.value["year"], course.value["semester"]),
"r") as fp:
sched2 = json.load(fp)
for val in sched2:
if val["course"] == course.value["name"]:
val["custom"] = course.value["year"]
schedule.append(val)
return schedule
def setup(client):
client.add_cog(School(client))

View File

@ -1,102 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
from functions.database import githubs, twitch
class SelfPromo(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="GitHub", aliases=["Git", "GitHubs", "Gh"], case_insensitive=True, usage="[@Persoon]*", invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def github(self, ctx, member: discord.Member = None):
# Get a specific member's GitHub
if member:
user_git = githubs.get_user(member.id)
if not user_git:
return await ctx.send("**{}** heeft zijn GitHub link nog niet doorgegeven.".format(member.display_name))
return await self.createPersonalPromo(ctx, member, user_git[0][0], discord.Colour.from_rgb(250, 250, 250), "GitHub")
l = githubs.getAll()
await self.createPromoEmbed(ctx, l, discord.Colour.from_rgb(250, 250, 250), "GitHub", "files/images/github.png")
@github.command(name="Add", aliases=["Insert", "Register", "Set"], usage="[Link]")
async def githubadd(self, ctx, link):
if "github.com" not in link.lower() and "github.ugent.be" not in link.lower() and "gitlab.com" not in link.lower():
link = "https://github.com/{}".format(link)
githubs.add(ctx.author.id, link)
await ctx.message.add_reaction("")
@commands.group(name="Twitch", aliases=["Streams"], case_insensitive=True, usage="[@Persoon]", invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def twitch(self, ctx, member: discord.Member = None):
# Get a specific member's GitHub
if member:
user_twitch = twitch.get_user(member.id)
if not user_twitch:
return await ctx.send("**{}** heeft zijn Twitch link nog niet doorgegeven.".format(member.display_name))
return await self.createPersonalPromo(ctx, member, user_twitch[0][0], discord.Colour.from_rgb(100, 65, 165), "Twitch")
l = twitch.getAll()
await self.createPromoEmbed(ctx, l, discord.Colour.from_rgb(100, 65, 165), "Twitch", "files/images/twitch.png")
@twitch.command(name="Add", aliases=["Insert", "Register", "Set"], usage="[Link]")
async def twitchadd(self, ctx, link):
if "twitch.tv" not in link.lower():
link = "https://www.twitch.tv/{}".format(link)
twitch.add(ctx.author.id, link)
await ctx.message.add_reaction("")
# Creates embed with everyone's links & a fancy image
async def createPromoEmbed(self, ctx, users, colour, type, imageUrl=None):
# Image file
file = None
# Sort users by Discord name
users = [[self.utilsCog.getMember(ctx, user[0]), user[1]] for user in users if self.utilsCog.getMember(ctx, user[0]) is not None]
users.sort(key=lambda x: x[0].name)
embed = discord.Embed(colour=colour)
if imageUrl is not None:
# Link
if "https" in imageUrl:
embed.set_thumbnail(url=imageUrl)
else:
# Local file
file = discord.File(imageUrl, filename="icon.png")
embed.set_thumbnail(url="attachment://icon.png")
embed.set_author(name="{} Links".format(type))
for user in users:
embed.add_field(name="{} ({})".format(
user[0].display_name, user[0].name
), value=user[1], inline=False)
embed.set_footer(text="Wil je je eigen {0} hierin? Gebruik {0} Add [Link] of stuur een DM naar DJ STIJN.".format(type))
if file is not None:
await ctx.send(embed=embed, file=file)
else:
await ctx.send(embed=embed)
async def createPersonalPromo(self, ctx, user, link, colour, type):
embed = discord.Embed(colour=colour)
embed.set_author(name="{} Links".format(type), icon_url=user.avatar_url)
embed.add_field(name="{} link van {}".format(type, user.display_name), value=link)
await ctx.send(embed=embed)
def setup(client):
client.add_cog(SelfPromo(client))

View File

@ -1,131 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
from functions.database import stats
import json
class Stats(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Stats", usage="[Categorie]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def stats(self, ctx):
s = stats.getOrAddUser(ctx.author.id)
# Calculate the percentages
robAttempts = int(s[2]) + int(s[3]) if int(s[2]) + int(s[3]) != 0 else 1
robSuccessPercent = round(100 * int(s[2]) / robAttempts, 2)
robFailedPercent = round(100 * int(s[3]) / robAttempts, 2)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="{}'s Stats".format(ctx.author.display_name))
embed.add_field(name="Geslaagde Rob Pogingen", value="{} ({})%".format(s[2], robSuccessPercent))
embed.add_field(name="Gefaalde Rob Pogingen", value="{} ({})%".format(s[3], robFailedPercent))
embed.add_field(name="Aantal Dinks Gestolen", value="{:,}".format(round(s[4])))
embed.add_field(name="Aantal Nightlies", value=str(s[6]))
embed.add_field(name="Langste Nightly Streak", value=str(s[5]))
embed.add_field(name="Totale Profit", value="{:,}".format(round(s[7])))
embed.add_field(name="Aantal keer gepoked", value=str(s[1]))
embed.add_field(name="Aantal Gewonnen Coinflips", value=str(s[8]))
embed.add_field(name="Totale winst uit Coinflips", value="{:,}".format(round(s[9])))
embed.add_field(name="Aantal Bails", value="{:,}".format(int(s[10])))
await ctx.send(embed=embed)
@stats.command(aliases=["Coinflip"], hidden=True)
async def cf(self, ctx):
with open("files/stats.json", "r") as fp:
s = json.load(fp)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Coinflip Stats")
embed.description = "**Kop**: {:,} ({}%)\n**Munt**: {:,} ({}%)".format(
s["cf"]["h"], self.percent(s["cf"], "h"), s["cf"]["t"], self.percent(s["cf"], "t"))
await ctx.send(embed=embed)
@stats.command(aliases=["Roll"], hidden=True)
async def dice(self, ctx):
with open("files/stats.json", "r") as fp:
s = json.load(fp)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Dice Stats")
embed.description = "\n".join(["**{}**: {:,} ({}%)".format(
i, s["dice"][i], self.percent(s["dice"], i)) for i in sorted(s["dice"].keys())])
await ctx.send(embed=embed)
@stats.command(hidden=True)
async def rob(self, ctx):
with open("files/stats.json", "r") as fp:
s = json.load(fp)["rob"]
totalAttempts = s["robs_success"] + s["robs_failed"]
successPercent = round(100 * s["robs_success"] / totalAttempts, 2)
failedPercent = round(100 * s["robs_failed"] / totalAttempts, 2)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Rob Stats")
embed.description = "**Geslaagd**: {:,} ({}%)\n**Gefaald**: {:,} ({}%)\n**Borg betaald**: {:,}".format(
s["robs_success"], successPercent, s["robs_failed"], failedPercent, round(s["bail_paid"])
)
await ctx.send(embed=embed)
@stats.command(name="Channels", aliases=["C", "CA"], usage="[#Channel]*", hidden=True)
@commands.check(checks.isMod)
async def channels(self, ctx, channel: discord.TextChannel = None):
res = stats.channel_activity(channel)
embed = discord.Embed(colour=discord.Colour.blue())
if channel:
embed.set_author(name="Channel Activity - {}".format(channel.name))
channel_instance = self.client.get_channel(int(res[0][0]))
embed.add_field(name="Aantal berichten", value="{:,}".format(round(float(res[0][1]), 2)), inline=False)
try:
last_message = await channel_instance.fetch_message(channel_instance.last_message_id)
except discord.NotFound:
last_message = None
if last_message is None:
embed.add_field(name="Laatste bericht", value="[Verwijderd]", inline=False)
else:
embed.add_field(name="Laatste bericht", value="[Jump URL]({})".format(last_message.jump_url), inline=False)
elif ctx.guild:
embed.set_author(name="Channel Activity - {}".format(ctx.guild))
description = ""
for c in sorted(res, key=lambda x: int(x[1]), reverse=True):
if not any(tc.id == int(c[0]) for tc in ctx.guild.text_channels):
continue
channel_instance = self.client.get_channel(int(c[0]))
description += "{}: {:,}\n".format(channel_instance.mention, round(float(c[1]), 2))
embed.description = description
else:
return await ctx.send("Dit commando werkt niet in DM's.")
return await ctx.send(embed=embed)
async def callStats(self, name, ctx):
await [command for command in self.stats.commands if command.name == name][0](ctx)
def percent(self, dic, stat):
total = sum([int(dic[s]) for s in dic])
if total == 0:
total = 1
return round(100 * int(dic[stat]) / total, 2)
def setup(client):
client.add_cog(Stats(client))

View File

@ -1,123 +0,0 @@
from converters.numbers import Abbreviated
from data import storePages
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from enums.numbers import Numbers
from functions import checks
from functions.database import store, currency
from functions.numbers import getRep
class Store(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Store", aliases=["Shop"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def store(self, ctx):
entries = store.getAllItems()
await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx)
@store.command(name="Buy", aliases=["Get"], hidden=True)
async def storeBuy(self, ctx, item, amount: Abbreviated = 1):
if amount is None:
return
await self.buy(ctx, item, amount)
@commands.command(name="Buy", aliases=["Get"], usage="[Item id] [Aantal]*")
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def buy(self, ctx, item, amount: Abbreviated = 1):
if amount is None:
return
try:
item = int(item)
except ValueError:
return await ctx.send("Dit is geen geldig id.")
success, message = store.buy(ctx, ctx.author.id, item, amount)
if not success:
return await ctx.send(message)
rep = getRep(message["price"], Numbers.t.value)
return await ctx.send("**{}** heeft **{} {}{}** gekocht voor **{}** Didier Dink{}.".format(
ctx.author.display_name, amount, message["name"], checks.pluralS(amount),
rep, checks.pluralS(message["price"])
))
@store.command(name="Sell", hidden=True)
async def storeSell(self, ctx, itemid, amount: Abbreviated = 1):
if amount is None:
return
await self.sell(ctx, itemid, amount)
@commands.command(name="Sell", usage="[Item id] [Aantal]")
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def sell(self, ctx, itemid, amount: Abbreviated = 1):
if amount is None:
return
try:
itemid = int(itemid)
except ValueError:
return await ctx.send("Dit is geen geldig id.")
inv = store.inventory(ctx.author.id)
if not inv or not any(int(item[0]) == itemid for item in inv):
return await ctx.send("Je hebt geen item met dit id.")
item_tuple = None
for item in inv:
if item[0] == itemid:
item_tuple = item
break
if amount.lower() == "all":
amount = int(item_tuple[2])
if int(item_tuple[2]) < amount:
return await ctx.send("Je hebt niet zoveel {}s.".format(item_tuple[1]))
store.sell(int(ctx.author.id), itemid, int(amount), int(item_tuple[2]))
price = int(store.getItemPrice(itemid)[0])
returnValue = round(0.8 * (price * amount))
currency.update(ctx.author.id, "dinks", currency.dinks(ctx.author.id) + returnValue)
await ctx.send("**{}** heeft **{} {}{}** verkocht voor **{}** Didier Dinks!".format(
ctx.author.display_name, amount, item_tuple[1], "s" if amount != 1 else "",
getRep(returnValue, Numbers.t.value)
))
@commands.command(name="Inventory", aliases=["Inv", "Items"])
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def inventory(self, ctx, *args):
inv = store.inventory(ctx.author.id)
inv = sorted(inv, key=lambda x: x[1])
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Inventory van {}".format(ctx.author.display_name))
embed.set_thumbnail(url=str(ctx.author.avatar_url))
if len(inv) == 0:
embed.description = "Je hebt nog niets gekocht!\n" \
"Koop iets in de Store wanneer DJ STIJN niet langer te lui is om er iets in te steken."
else:
embed.description = "\n".join("#{} {}: {}".format(item[0], item[1], item[2]) for item in inv)
await ctx.send(embed=embed)
def setup(client):
client.add_cog(Store(client))

View File

@ -1,234 +0,0 @@
from data import constants
from data.remind import Reminders
from discord.ext import commands, tasks
from enums.numbers import Numbers
from functions import timeFormatters
from functions.database import currency, poke, prison, birthdays, stats
import json
import random
import requests
import time
class Tasks(commands.Cog):
def __init__(self, client):
self.client = client
self.bankInterest.start()
self.resetPrison.start()
self.resetLost.start()
# self.resetPoke.start()
self.checkBirthdays.start()
self.updateMessageCounts.start()
self.sendReminders.start()
@tasks.loop(hours=1.0)
async def bankInterest(self):
"""
Task that gives daily interest
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["interest"]) > 10000:
users = currency.getAllRows()
bitcoinPrice = self.getCurrentBitcoinPrice()
for user in users:
# People in prison don't get interest
if len(prison.getUser(int(user[0]))) != 0:
continue
if float(user[3]) != 0.0:
currency.update(user[0], "investeddays", int(user[4]) + 1)
profit = ((float(user[3]) + float(user[5])) * (1 + (float(user[2]) * 0.01))) - float(user[3])
# Can't exceed 1 quadrillion
# Check BC as well so they can't put everything into BC to cheat the system
if float(user[1]) + float(user[3]) + float(user[5]) + profit + (float(user[8]) * bitcoinPrice) > Numbers.q.value:
# In case adding profit would exceed 1q, only add the difference
profit = Numbers.q.value - float(user[1]) - float(user[3]) - float(user[5]) - (float(user[8]) * bitcoinPrice)
# Don't reduce the current profit if Dinks were gained some other way (rob, bc, ...)
if profit > 0:
currency.update(user[0], "profit", float(user[5]) + profit)
await self.client.get_user(int(user[0])).send("Je hebt de invest-limiet van 1Q Didier Dinks bereikt.\nIndien je nog meer Didier Dinks wil sparen, kan je 1q Didier Dinks omruilen voor een Platinum Dink in de shop.")
else:
currency.update(user[0], "profit", float(user[5]) + profit)
lastTasks["interest"] = int(round(time.time()))
with open("files/lastTasks.json", "w") as fp:
json.dump(lastTasks, fp)
@bankInterest.before_loop
async def beforeBankInterest(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def resetLost(self):
"""
Task that resets Lost Today
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["lost"]) > 10000:
with open("files/lost.json", "r") as fp:
fc = json.load(fp)
fc["today"] = 0
with open("files/lost.json", "w") as fp:
json.dump(fc, fp)
lastTasks["lost"] = round(time.time())
with open("files/lastTasks.json", "w") as fp:
json.dump(lastTasks, fp)
@resetLost.before_loop
async def beforeResetLost(self):
await self.client.wait_until_ready()
@tasks.loop(hours=6.0)
async def resetPoke(self):
"""
Task that resets Poke
"""
if int(time.time()) - int(poke.get()[1]) > 259200:
await self.client.get_guild(int(self.client.constants.CallOfCode))\
.get_channel(int(self.client.constants.DidierPosting))\
.send("Poke is gereset door inactiviteit. <@!{}> is hem!".format(int(poke.reset())))
@resetPoke.before_loop
async def beforeResetPoke(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def resetPrison(self):
"""
Task that lowers prison time daily
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["prison"]) > 10000:
prison.dailyLowers()
with open("files/lastTasks.json", "w") as fp:
lastTasks["prison"] = round(time.time())
json.dump(lastTasks, fp)
@resetPrison.before_loop
async def beforeResetPrison(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def checkBirthdays(self):
"""
Task that wishes people a happy birthday
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 6 and int(time.time()) - int(lastTasks["birthdays"]) > 10000:
dt = timeFormatters.dateTimeNow()
res = birthdays.get_users_on_date(dt.day, dt.month)
COC = self.client.get_guild(int(constants.CallOfCode))
people = [COC.get_member(int(user[0])) for user in res]
general = COC.get_channel(int(constants.CoCGeneral))
lastTasks["birthdays"] = round(time.time())
with open("files/lastTasks.json", "w") as fp:
json.dump(lastTasks, fp)
if not people:
return
if len(people) == 1:
return await general.send("Gelukkige verjaardag {}!".format(people[0].mention))
return await general.send("Gelukkige verjaardag {} en {}!".format(
", ".join(user.mention for user in people[:-1]),
people[-1].mention
))
@checkBirthdays.before_loop
async def beforecheckBirthdays(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def updateMessageCounts(self):
"""
Task that updates the activity counter for channels
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["channels"]) > 10000:
channels = stats.channel_activity()
for channel in channels:
stats.lower_channel(int(channel[0]), 0.95 * float(channel[1]))
with open("files/lastTasks.json", "w") as fp:
lastTasks["channels"] = round(time.time())
json.dump(lastTasks, fp)
@updateMessageCounts.before_loop
async def beforeupdateMessageCounts(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def sendReminders(self):
"""
Task that sends people daily reminders
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 7 and int(time.time()) - int(lastTasks["remind"]) > 10000:
reminders = Reminders()
weekday = self.getCurrentWeekday()
for category in reminders.categories:
# Check if this reminder is temporarily disabled
if category["disabled"]:
continue
# Checks if this reminder can be sent on weekdays
if (not category["weekends"]) and weekday > 4:
continue
for user in category["users"]:
userInstance = self.client.get_user(user)
# User can't be fetched for whatever reason, ignore instead of crashing
if userInstance is None:
continue
# Check if a special embed has to be attached for this reminder
if "embed" not in category:
await userInstance.send(random.choice(category["messages"]))
else:
await userInstance.send(random.choice(category["messages"]), embed=category["embed"])
with open("files/lastTasks.json", "w") as fp:
lastTasks["remind"] = round(time.time())
json.dump(lastTasks, fp)
@sendReminders.before_loop
async def beforeSendReminders(self):
await self.client.wait_until_ready()
def getCurrentHour(self):
return timeFormatters.dateTimeNow().hour
def getCurrentWeekday(self):
return timeFormatters.dateTimeNow().weekday()
def getCurrentBitcoinPrice(self):
result = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json").json()
currentPrice = result["bpi"]["EUR"]["rate_float"]
return float(currentPrice)
def setup(client):
client.add_cog(Tasks(client))

View File

@ -1,31 +0,0 @@
from discord.ext import commands
from functions import checks
class TestCog(commands.Cog):
def __init__(self, client):
self.client = client
def cog_check(self, ctx):
"""
Check executed for every command in this cog.
If necessary, create your own check here. A check is just a function
that returns True or False, and takes ctx as an argument. A command will
only be executed when this check returns True, which is why that is the default
implementation for this function.
"""
return True
@commands.command()
async def test(self, ctx):
pass
@test.error
async def test_handler(self, ctx, error):
raise error
def setup(client):
client.add_cog(TestCog(client))

View File

@ -1,128 +0,0 @@
from data import paginatedLeaderboard
import datetime
from decorators import help
import discord
from discord.ext import commands, menus
from enums.help_categories import Category
from functions import checks, timeFormatters
import requests
class Train(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Train", aliases=["Trein"], usage="[Vertrek]* [Bestemming]")
@help.Category(category=Category.School)
async def train(self, ctx, *args):
if not args or len(args) > 2:
await ctx.send("Controleer je argumenten.")
return
destination = args[-1]
departure = args[0] if len(args) > 1 else "Gent Sint-Pieters"
req = requests.get(
"http://api.irail.be/connections/?from={}&to={}&alerts=true&lang=nl&format=json".format(departure,
destination)).json()
if "error" in req:
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Treinen van {} naar {}".format(
self.formatCity(departure), self.formatCity(destination)))
embed.add_field(name="Error", value="Er ging iets fout, probeer het later opnieuw.", inline=False)
await self.sendEmbed(ctx, embed)
return
pages = paginatedLeaderboard.Pages(source=TrainPagination(self.formatConnections(req["connection"]),
self.formatCity(departure),
self.formatCity(destination)),
clear_reactions_after=True)
await pages.start(ctx)
def formatConnections(self, connections):
response = []
for connection in sorted(connections, key=lambda con: con["departure"]["time"]):
conn = {}
if connection["departure"]["canceled"] != "0" or connection["arrival"]["canceled"] != "0":
conn = {"Canceled": "Afgeschaft"}
dep = connection["departure"]
arr = connection["arrival"]
conn["depStation"] = self.formatCity(dep["station"])
conn["depTime"] = self.formatTime(dep["time"])
conn["delay"] = self.formatDelay(dep["delay"])
conn["track"] = dep["platform"]
conn["arrStation"] = self.formatCity(arr["station"])
conn["direction"] = self.formatCity(dep["direction"]["name"])
conn["arrTime"] = self.formatTime(arr["time"])
conn["duration"] = self.formatTime(connection["duration"])
response.append(conn)
return response
def formatTime(self, timestamp):
if int(timestamp) <= 86400:
minutes = int(timestamp) // 60
if minutes < 60:
return str(minutes) + "m"
return "{}h{:02}m".format(minutes // 60, minutes % 60)
else:
return timeFormatters.epochToDate(int(timestamp), "%H:%M")["date"]
def formatDelay(self, seconds):
seconds = int(seconds)
return self.sign(seconds) + self.formatTime(abs(seconds)) if seconds != 0 else ""
def sign(self, number):
return "-" if int(number) < 0 else "+"
def formatCity(self, city):
city = city[0].upper() + city[1:]
arr = []
for i, letter in enumerate(city):
if (i > 0 and (city[i - 1] == " " or city[i - 1] == "-")) or i == 0:
arr.append(letter.upper())
else:
arr.append(letter.lower())
return "".join(arr)
async def sendEmbed(self, ctx, embed):
if checks.allowedChannels(ctx):
await ctx.send(embed=embed)
else:
await ctx.author.send(embed=embed)
class TrainPagination(menus.ListPageSource):
def __init__(self, data, departure, destination):
super().__init__(data, per_page=3)
self.departure = departure
self.destination = destination
async def format_page(self, menu: menus.MenuPages, entries):
offset = menu.current_page * self.per_page
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Treinen van {} naar {}".format(self.departure, self.destination))
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
for i, connection in enumerate(entries, start=offset):
afgeschaft = "Canceled" in connection
embed.add_field(name="Van", value=str(connection["depStation"]), inline=True)
embed.add_field(name="Om", value=str(connection["depTime"]), inline=True)
embed.add_field(name="Spoor", value=str(connection["track"]), inline=True)
embed.add_field(name="Richting", value=str(connection["direction"]), inline=True)
embed.add_field(name="Aankomst", value=(str(connection["arrTime"])
if not afgeschaft else "**AFGESCHAFT**"), inline=True)
embed.add_field(name="Vertraging", value=str(connection["delay"]) if connection["delay"] != "" else "0",
inline=True)
# White space
if i - offset < 2:
embed.add_field(name="\u200b", value="\u200b", inline=False)
return embed
def setup(client):
client.add_cog(Train(client))

View File

@ -1,114 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions.stringFormatters import titleCase as tc
from googletrans import Translator, LANGUAGES
import re
class Translate(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Translate", aliases=["Tl", "Trans"], usage="[Tekst] [Van]* [Naar]*")
@help.Category(Category.Words)
async def translate(self, ctx, query=None, to="nl", fr="auto"):
if query is None:
return await ctx.send("Controleer je argumenten.")
success, query = await self.getQuery(ctx, query)
if not success:
return await ctx.send(query)
translator = Translator()
# From & To were provided, swap them
if fr != "auto":
temp = fr
fr = to
to = temp
try:
translation = translator.translate(query, to, fr)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Didier Translate")
if fr == "auto":
language = translation.extra_data["original-language"]
embed.add_field(name="Gedetecteerde taal", value=tc(LANGUAGES[language]))
embed.add_field(name="Zekerheid", value="{}%".format(translation.extra_data["confidence"] * 100))
embed.add_field(name="Origineel ({})".format(translation.src.upper()), value=query, inline=False)
embed.add_field(name="Vertaling ({})".format(to.upper()), value=translation.text)
await ctx.send(embed=embed)
except ValueError as e:
message = str(e)
if "destination" in message:
return await ctx.send("{} is geen geldige taal.".format(tc(to)))
if "source" in message:
return await ctx.send("{} is geen geldige taal.".format(tc(fr)))
raise e
@commands.command(name="Detect", aliases=["Ld"], usage="[Tekst]")
@help.Category(Category.Words)
async def detect(self, ctx, query=None):
if query is None:
return await ctx.send("Controleer je argumenten.")
success, query = await self.getQuery(ctx, query)
if not success:
return await ctx.send(query)
translator = Translator()
language = translator.detect(query)
confidence = language.confidence * 100
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Language Detection")
embed.add_field(name="Zin", value=query, inline=False)
embed.add_field(name="Gedetecteerde taal", value=tc(LANGUAGES[language.lang]))
embed.add_field(name="Zekerheid", value="{}%".format(confidence))
await ctx.send(embed=embed)
async def getQuery(self, ctx, query):
# Check if it's a link to a message
if re.match(r"^https://discord.com/channels/[0-9A-Za-z@]+/[0-9]+/[0-9]+$", query):
spl = query.split("/")
channel = self.client.get_channel(int(spl[-2]))
if channel is None:
return False, "Ik kan geen kanaal zien met dit id."
message = await channel.fetch_message(spl[-1])
if message is None:
return False, "Ik kan geen bericht zien met dit id."
query = message.content
else:
try:
# An id was passed instead
query = int(query)
message = await ctx.channel.fetch_message(query)
if message is None:
return False, "Ik kan geen bericht zien met dit id."
query = message.content
except ValueError:
pass
if not query:
return False, "Dit is geen geldig bericht."
return True, query
def setup(client):
client.add_cog(Translate(client))

View File

@ -1,67 +0,0 @@
from data import constants
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
class Utils(commands.Cog):
def __init__(self, client):
self.client = client
self.client.locked = False
self.client.lockedUntil = -1
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Marco Polo to check if bot is running & delay
@commands.command(name="Marco")
@help.Category(category=Category.Didier)
async def marco(self, ctx):
await ctx.send("Polo! {}ms".format(round(self.client.latency * 1000)))
async def removeMessage(self, message):
try:
await message.delete()
except discord.Forbidden:
pass
# Send a DM to a user
async def sendDm(self, userid, message: str):
user = self.client.get_user(int(userid))
await user.send(message)
# Send an Embed to a user
async def sendEmbed(self, userid, embed):
await discord.utils.get(self.client.get_all_members(), id=int(userid)).send(embed=embed)
# Returns a member object of a user
def getMember(self, ctx, memberid):
if str(ctx.channel.type) == "private" or ctx.guild.get_member(int(memberid)) is None:
if str(memberid) == str(ctx.author.id):
return ctx.author
COC = self.client.get_guild(int(constants.CallOfCode))
return COC.get_member(int(memberid))
return ctx.guild.get_member(int(memberid))
# Returns a user's display name if he's in this server, else COC
def getDisplayName(self, ctx, memberid):
# Checks if this is a DM, or the user is not in the guild
if str(ctx.channel.type) == "private" or ctx.guild.get_member(int(memberid)) is None:
if str(memberid) == str(ctx.author.id):
return ctx.author.display_name
COC = self.client.get_guild(int(constants.CallOfCode))
member = COC.get_member(int(memberid))
if member is not None:
return member.display_name
return "[Persoon die de server misschien geleaved is | {}]".format(memberid)
mem = ctx.guild.get_member(int(memberid))
return mem.display_name
def setup(client):
client.add_cog(Utils(client))

View File

@ -1,52 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
import requests
class Words(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Adjective", aliases=["Adj", "Adjectives"], usage="[Woord]")
@help.Category(category=Category.Words)
async def adjective(self, ctx, word=None):
await self.getData(ctx, word, "rel_jjb")
@commands.command(name="Synonym", aliases=["Syn", "Synonyms"], usage="[Woord]")
@help.Category(category=Category.Words)
async def synonym(self, ctx, word=None):
await self.getData(ctx, word, "rel_syn")
@commands.command(name="Antonym", aliases=["Ant", "Antonyms", "Opp", "Opposite"], usage="[Woord]")
@help.Category(category=Category.Words)
async def antonym(self, ctx, word=None):
await self.getData(ctx, word, "rel_ant")
@commands.command(name="Rhyme", aliases=["Rhymes"], usage="[Woord]")
@help.Category(category=Category.Words)
async def rhyme(self, ctx, word=None):
await self.getData(ctx, word, "rel_rhy")
# Contacts the API & returns the response, as these commands all do the same anyways
async def getData(self, ctx, word, relation):
if not word:
await ctx.send("Geef een woord op.")
return
res = requests.get("https://api.datamuse.com/words?{}={}".format(relation, word)).json()
# Only show top 20 results
res = res if len(res) <= 15 else res[:15]
# Pull the words out of the dicts
res = [word["word"] for word in res]
await ctx.send(", ".join(res) if len(res) > 0 else "Geen resultaten gevonden.")
def setup(client):
client.add_cog(Words(client))

View File

@ -1,45 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, xp
from functions.database import stats
class Xp(commands.Cog):
def __init__(self, client):
self.client = client
@commands.group(name="Xp", aliases=["Level", "Mc", "Mess", "Messages"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(Category.Other)
async def xp(self, ctx, user: discord.Member = None):
if user is not None and str(ctx.author.id) != constants.myId:
return await ctx.send("Je hebt geen toegang tot dit commando.")
target = user if user is not None else ctx.author
target_stats = stats.getOrAddUser(target.id)
message_count = stats.getTotalMessageCount()
perc = round(int(target_stats[11]) * 100/message_count, 2)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name=target.display_name, icon_url=target.avatar_url)
embed.add_field(name="Aantal Berichten", value="{:,} ({}%)".format(int(target_stats[11]), perc))
embed.add_field(name="Level", value=str(xp.calculate_level(target_stats[12])))
embed.add_field(name="XP", value="{:,}".format(int(target_stats[12])))
embed.set_footer(text="*Sinds Didier 2.0 Launch")
await ctx.send(embed=embed)
@xp.command(name="Leaderboard", aliases=["Lb"], hidden=True)
async def xpleaderboard(self, ctx, *args):
if any(alias in ctx.message.content for alias in ["mc", "mess", "messages"]):
return await self.client.get_cog("Leaderboards").callLeaderboard("Messages", ctx)
await self.client.get_cog("Leaderboards").callLeaderboard("Xp", ctx)
def setup(client):
client.add_cog(Xp(client))

View File

@ -1,67 +0,0 @@
import re
from discord.ext import commands
# Gets the numerical value of a string representation like 1.6k
def abbreviated(rep):
if rep.lower() == "all":
return rep
validMatch = r"^-?[0-9]+\.?[0-9]*[A-Za-z]*$"
numericalValue = r"^-?[0-9]+\.?[0-9]*"
indicator = r"[A-Za-z]*$"
valid = re.match(validMatch, rep)
# Invalid representation
if not valid:
return None
numerical = re.search(numericalValue, rep)
# Invalid number
if float(numerical[0]) == 0.0:
return None
indic = re.search(indicator, rep)
if not indic[0]:
try:
return int(rep)
except ValueError:
# If no indicator was passed, it has to be a whole number
return None
# Invalid indicator
if indic[0] not in exponents() and not any(exp.lower() == indic[0].lower() for exp in exponents()):
return None
if indic[0] in exponents():
try:
return int(float(numerical[0]) * int("1" + ("0" * (exponents().index(indic[0]) + 1) * 3)))
except ValueError:
# Can't be cast to int
return None
for i, exponent in enumerate(exponents()):
if exponent.lower() == indic[0].lower():
try:
return int(float(numerical[0]) * int("1" + ("0" * (i + 1) * 3)))
except ValueError:
# Can't be cast to int
return None
class Abbreviated(commands.Converter):
async def convert(self, ctx, argument):
if argument is None:
return None
converted = abbreviated(argument)
if converted is None:
await ctx.send("Dit is geen geldig getal.")
return None
return converted
def exponents():
return ["K", "M", "B", "t", "q", "Q", "s", "S", "o", "n", "d", "U", "D", "T", "Qt", "Qd", "Sd", "St", "O", "N", "v"]

View File

@ -1,121 +0,0 @@
# Creating Commands
The following document shows you how to create new commands, which kwargs to add, and what they do.
Make sure to properly register the appropriate help decorator to each command. More info on help decorators in `readme.md`.
Do _NOT_ create commands outside of `Cogs`!
### Commands
Commands require a set list of kwargs in order to work correctly, and show up in the help page.
- Name: The name of the command, with only the first letter capitalised.
- Aliases: Optional, a list of all aliases the command can have. Make sure to check if the alias already exists (if not, Discord will throw an error when starting the bot.)
Aliases should only have the first letter capitalised, and be placed in alphabetical order.
- Usage: Optional, only required when the command takes arguments. Show how the command has to be used. Arguments should be placed between \[square brackets\], and optional arguments need an asterisk* after the brackets.
- Hidden: Optional, should only be added when you _don't_ want the command to show up in the help page.
- Ignore_Extra: Optional, should only be used when you don't want the command to be called when too many arguments are passed.
This defaults to `True`, so if you don't care about extra arguments then adding this is obsolete. Setting this to `False` makes sentences like _Didier Bank is a cool command_ not trigger the `Didier Bank` command.
##### Checks
Further, checks can be added to commands to make sure the function only executes in certain situations. There are already a few [built-in checks](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html?highlight=checks#checks) to do commonly-used things, but you can easily create your own functions to do this.
New checks should be added to `/functions/checks.py`. Checks are just a basic function that returns `True` or `False`, and takes `ctx` as a parameter.
The example below shows how to create a command that only takes 1 required argument, 1 optional argument, and does not show up in the help page. The `check` makes sure the command does not work in DM's.
```python
def check(ctx):
return ctx.guild is not None
@commands.command(name="Name", aliases=["N"], usage=["[Arg], [Arg2]*"], hidden=True, ignore_extra=True)
@commands.check(check=check)
async def name(ctx, arg, arg2=None):
pass
```
### Groups
It's possible that you might want to create subcommands for a certain command. At first glance, you might want to do something like this:
```python
@commands.command()
async def parent_command(ctx, arg):
if arg == "sub1":
await sub1()
elif arg == "sub2":
await sub2()
...
async def sub1():
pass
async def sub2():
pass
```
Looking at this you might think that there must be a better way to do this, and there is. The only situation where this type of code will be accepted, is when there are too many possible subcommands to the point where it's not really doable to create all of them (think help & faq categories).
Discord.py has implemented [Command Groups](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html?highlight=group#discord.ext.commands.Group) for this purpose. Groups allow you to register commands to other commands in order to create subcommands (or even subgroups!).
Groups _do_ have a few extra kwargs on top of the usual `Command Kwargs` mentioned above:
- case_insensitive: Indicates whether or not subcommands should be case-insensitive (think `lb dinks` and `lb DINKS`). You are *required* to set this parameter to `True` so Didier will always work case-insensitively.
- invoke_without_command: Indicates whether or not the group command should only execute if no subcommand was found. In most cases, this will be `True`.
The example below shows how to create a group so that `Didier A B D` will execute `b("D")`, but `Didier A C` will execute `a("C")`.
```python
@commands.group(name="A", usage="[Args]", case_insensitive=True, invoke_without_command=True)
async def a(ctx, args):
print(args)
# Prints "C"
@a.command(name="B")
async def b(ctx, args):
print(args)
# Prints "D"
```
### Unpacking Groups
It's possible that you want to create a group to organise subcommands, but want the subcommands to be listed in the category instead of under the group (or when the Cog and Group have the same name). An example is Didier's `Random` module.
When creating a group without doing anything, the help page will look like this:
Categories:
Currency
...
Random (category)
...
Help Random (category):
Random (group)
Help Random (group):
Random Choice
...
This requires an unnecessary step to get to the commands (as there's only 1 result), and it would be nicer to have all subcommands list in the category instead. Seeing as the Cog & Group both have the same name, it also feels weird to do this (and would be near impossible to code in a user-friendly way). When the Cog has the same name, you expect all the commands to be right there immediately.
The `Help Decorator` has an optional argument that can do this for you. When registering a group to a category, you can add `unpack=True` in order to accomplish this.
```python
@commands.group(name="Random")
@help.Category(Category.Random, unpack=True)
async def random(self, ctx):
pass
```
This way, the help page will look like this:
Random (category):
Random Choice
...
### Testing & Messing Around
In case you want to quickly test 1 line of code, or test a quick idea, you'd have to create an entire Cog just to use once. With the Cog Template this is not _that_ bad, but it's still a bit cumbersome.
For this purpose, there is a Cog called `testCog.py`, with 1 command (`Test()`). Slap your code in there, and use `Didier Test` in Discord. Just remember to never commit these changes (right click in the file manager -> `Git` -> `Rollback`).

View File

@ -1,68 +0,0 @@
from enum import Enum
myId = "171671190631481345"
didierId = "680510935164911730"
coolerDidierId = "728361496874057812"
botIDs = [
"155149108183695360",
"234395307759108106",
"239631525350604801",
"408785106942164992",
"679679176466235399",
"680510935164911730",
"706148003949314069",
"728361496874057812"
]
BugReports = "762668401960812554"
CallOfCode = "626699611192688641"
CoCGeneral = "626699611813314561"
DeZandbak = "728361030404538488"
ErrorLogs = "762668505455132722"
FeatureRequests = "762668473313787964"
mods = {
626699611192688641: [384457911377854467, 171671190631481345],
728361030404538488: [171671190631481345],
689200072268841106: [332948151226990614, 171671190631481345, 280025474485321729, 237239312385703951],
710515840566427721: [171671190631481345, 140186024075722752, 296197359539585024]
}
allowedChannels = {
"bot-games": 634327239361691658,
"bot-commands": 629034637955694602,
"bot-testing": 679701786189103106,
"shitposting": 676713433567199232,
"didier-testings": 689202224437395573,
"freegames": 705745297908826113,
"bot-commandsCOCGI": 714170653124722738,
"generalZandbak": 728361031008780422,
"freegamesZandbak": 728545397127118898,
"spelenZandbak": 769248992957038612
}
creationDate = 1582243200
holidayAPIKey = "af4e1ebe-465d-4b93-a828-b95df18e6424"
prefixes = ["big d", "didier"]
faq_channels = {
727876753523081216: "ad2",
727876779481497600: "comnet",
727876797458284584: "funcprog",
727876819264733244: "statprob",
727876836587208714: "sysprog",
676713433567199232: "didier"
}
class Live(Enum):
CallOfCode = "626699611192688641"
DidierPosting = "728361031008780422"
General = "626699611813314561"
class Zandbak(Enum):
CallOfCode = "728361030404538488"
DidierPosting = "728361031008780422"
General = "728361031008780422"

View File

@ -1,31 +0,0 @@
import discord
from discord.ext import menus
# https://github.com/Rapptz/discord-ext-menus
class Source(menus.ListPageSource):
def __init__(self, data, name, colour=discord.Colour.blue()):
super().__init__(data, per_page=10)
self.name = name
self.colour = colour
async def format_page(self, menu: menus.MenuPages, entries):
offset = menu.current_page * self.per_page
description = ""
for i, v in enumerate(entries, start=offset):
# Check if the person's name has to be highlighted
if v.startswith("**") and v.endswith("**"):
description += "**"
v = v[2:]
description += "{}: {}\n".format(i + 1, v)
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.name)
embed.description = description
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
return embed
class Pages(menus.MenuPages):
def __init__(self, source, clear_reactions_after, timeout=30.0):
super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after)

View File

@ -1,24 +0,0 @@
from functions import les
from functions.database import remind
class Reminders:
def __init__(self):
rows = remind.getAllRows()
self._nightlyUsers = [int(user[0]) for user in rows if user[1]]
self._nightlyMessages = ["Dagelijkse herinnering om Didier Nightly te doen.", "Vrees niet, Nightly-streak-liefhebber! 't Zenne kik, Didier, me ne reminder!"]
self.nightly = {"users": self._nightlyUsers, "messages": self._nightlyMessages, "weekends": True, "disabled": False}
self._les = [int(user[0]) for user in rows if user[2]]
self._lesMessages = ["Lessenrooster voor vandaag:"]
self.les = {"users": self._les, "messages": self._lesMessages, "embed": self.lesEmbed(), "weekends": False, "disabled": True}
self.categories = [self.nightly, self.les]
def lesEmbed(self):
day, dayDatetime, semester, year = les.parseArgs([])[1:]
schedule = les.getSchedule(semester, year)
return les.createEmbed(day, dayDatetime, semester, year, schedule)

View File

@ -1,28 +0,0 @@
import discord
from discord.ext import menus
# https://github.com/Rapptz/discord-ext-menus
class Source(menus.ListPageSource):
def __init__(self, data):
super().__init__(data, per_page=10)
self.name = "Didier Store"
self.colour = discord.Colour.blue()
async def format_page(self, menu: menus.MenuPages, entries):
offset = menu.current_page * self.per_page
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.name)
embed.description = "Heb je een idee voor een item? DM DJ STIJN met je idee!"
embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages()))
for i, v in enumerate(entries, start=offset):
embed.add_field(name="#{} - {}".format(v[0], v[1]), value="{:,} Didier Dinks".format(v[2]))
return embed
class Pages(menus.MenuPages):
def __init__(self, source, clear_reactions_after, timeout=30.0):
super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after)

View File

@ -0,0 +1,45 @@
import datetime
from datetime import date
from typing import Optional
from sqlalchemy import extract, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from database.crud import users
from database.schemas import Birthday, User
__all__ = ["add_birthday", "get_birthday_for_user", "get_birthdays_on_day"]
async def add_birthday(session: AsyncSession, user_id: int, birthday: date):
"""Add a user's birthday into the database
If already present, overwrites the existing one
"""
user = await users.get_or_add_user(session, user_id, options=[selectinload(User.birthday)])
if user.birthday is not None:
bd = user.birthday
await session.refresh(bd)
bd.birthday = birthday
else:
bd = Birthday(user_id=user_id, birthday=birthday)
session.add(bd)
await session.commit()
async def get_birthday_for_user(session: AsyncSession, user_id: int) -> Optional[Birthday]:
"""Find a user's birthday"""
statement = select(Birthday).where(Birthday.user_id == user_id)
return (await session.execute(statement)).scalar_one_or_none()
async def get_birthdays_on_day(session: AsyncSession, day: datetime.date) -> list[Birthday]:
"""Get all birthdays that happen on a given day"""
days = extract("day", Birthday.birthday)
months = extract("month", Birthday.birthday)
statement = select(Birthday).where((days == day.day) & (months == day.month))
return list((await session.execute(statement)).scalars().all())

View File

@ -0,0 +1,73 @@
from typing import Optional
import sqlalchemy.exc
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud.users import get_or_add_user
from database.exceptions import (
DuplicateInsertException,
Forbidden,
ForbiddenNameException,
NoResultFoundException,
)
from database.schemas import Bookmark
__all__ = ["create_bookmark", "get_bookmarks", "get_bookmark_by_name"]
async def create_bookmark(session: AsyncSession, user_id: int, label: str, jump_url: str) -> Bookmark:
"""Create a new bookmark to a message"""
# Don't allow bookmarks with names of subcommands
if label.lower() in ["create", "delete", "ls", "list", "Rm", "search"]:
raise ForbiddenNameException
await get_or_add_user(session, user_id)
try:
bookmark = Bookmark(label=label, jump_url=jump_url, user_id=user_id)
session.add(bookmark)
await session.commit()
await session.refresh(bookmark)
except sqlalchemy.exc.IntegrityError as e:
raise DuplicateInsertException from e
return bookmark
async def delete_bookmark_by_id(session: AsyncSession, user_id: int, bookmark_id: int):
"""Find a bookmark by its id & delete it
This fails if you don't own this bookmark
"""
select_statement = select(Bookmark).where(Bookmark.bookmark_id == bookmark_id)
bookmark = (await session.execute(select_statement)).scalar_one_or_none()
# No bookmark with this id
if bookmark is None:
raise NoResultFoundException
# You don't own this bookmark
if bookmark.user_id != user_id:
raise Forbidden
# Delete it
delete_statement = delete(Bookmark).where(Bookmark.bookmark_id == bookmark_id)
await session.execute(delete_statement)
await session.commit()
async def get_bookmarks(session: AsyncSession, user_id: int, *, query: Optional[str] = None) -> list[Bookmark]:
"""Get all a user's bookmarks"""
statement = select(Bookmark).where(Bookmark.user_id == user_id)
if query is not None:
statement = statement.where(Bookmark.label.ilike(f"%{query.lower()}%"))
return list((await session.execute(statement)).scalars().all())
async def get_bookmark_by_name(session: AsyncSession, user_id: int, query: str) -> Optional[Bookmark]:
"""Try to find a bookmark by its name"""
statement = select(Bookmark).where(Bookmark.user_id == user_id).where(func.lower(Bookmark.label) == query.lower())
return (await session.execute(statement)).scalar_one_or_none()

View File

@ -0,0 +1,41 @@
from datetime import datetime
from typing import Optional, Union
from discord import app_commands
from discord.ext import commands
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud.users import get_or_add_user
from database.schemas import CommandStats
__all__ = ["register_command_invocation"]
CommandT = Union[commands.Command, app_commands.Command, app_commands.ContextMenu]
async def register_command_invocation(
session: AsyncSession, ctx: commands.Context, command: Optional[CommandT], timestamp: datetime
):
"""Create an entry for a command invocation"""
if command is None:
return
await get_or_add_user(session, ctx.author.id)
# Check the type of invocation
context_menu = isinstance(command, app_commands.ContextMenu)
# (This is a bit uglier but it accounts for hybrid commands)
slash = isinstance(command, app_commands.Command) or (ctx.interaction is not None and not context_menu)
stats = CommandStats(
command=command.qualified_name.lower(),
timestamp=timestamp,
user_id=ctx.author.id,
slash=slash,
context_menu=context_menu,
)
session.add(stats)
await session.commit()

View File

@ -0,0 +1,172 @@
from datetime import date
from typing import Union
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud import users
from database.exceptions import currency as exceptions
from database.schemas import Bank, NightlyData
from database.utils.math.currency import (
capacity_upgrade_price,
interest_upgrade_price,
rob_upgrade_price,
)
__all__ = [
"add_dinks",
"claim_nightly",
"gamble_dinks",
"get_bank",
"get_nightly_data",
"invest",
"upgrade_capacity",
"upgrade_interest",
"upgrade_rob",
"withdraw",
"NIGHTLY_AMOUNT",
]
NIGHTLY_AMOUNT = 420
async def get_bank(session: AsyncSession, user_id: int) -> Bank:
"""Get a user's bank info"""
user = await users.get_or_add_user(session, user_id)
return user.bank
async def get_nightly_data(session: AsyncSession, user_id: int) -> NightlyData:
"""Get a user's nightly info"""
user = await users.get_or_add_user(session, user_id)
return user.nightly_data
async def invest(session: AsyncSession, user_id: int, amount: Union[str, int]) -> int:
"""Invest some of your Dinks"""
bank = await get_bank(session, user_id)
if amount == "all":
amount = bank.dinks
# Don't allow investing more dinks than you own
amount = min(bank.dinks, int(amount))
bank.dinks -= amount
bank.invested += amount
session.add(bank)
await session.commit()
return amount
async def withdraw(session: AsyncSession, user_id: int, amount: Union[str, int]) -> int:
"""Withdraw your invested Dinks"""
bank = await get_bank(session, user_id)
if amount == "all":
amount = bank.invested
# Don't allow withdrawing more dinks than you own
amount = min(bank.invested, int(amount))
bank.dinks += amount
bank.invested -= amount
await session.commit()
return amount
async def add_dinks(session: AsyncSession, user_id: int, amount: int):
"""Increase the Dinks counter for a user"""
bank = await get_bank(session, user_id)
bank.dinks += amount
session.add(bank)
await session.commit()
async def claim_nightly(session: AsyncSession, user_id: int):
"""Claim daily Dinks"""
nightly_data = await get_nightly_data(session, user_id)
now = date.today()
if nightly_data.last_nightly is not None and nightly_data.last_nightly == now:
raise exceptions.DoubleNightly
bank = await get_bank(session, user_id)
bank.dinks += NIGHTLY_AMOUNT
nightly_data.last_nightly = now
session.add(bank)
session.add(nightly_data)
await session.commit()
async def upgrade_capacity(session: AsyncSession, user_id: int) -> int:
"""Upgrade capacity level"""
bank = await get_bank(session, user_id)
upgrade_price = capacity_upgrade_price(bank.capacity_level)
# Can't afford this upgrade
if upgrade_price > bank.dinks:
raise exceptions.NotEnoughDinks
bank.dinks -= upgrade_price
bank.capacity_level += 1
session.add(bank)
await session.commit()
return bank.capacity_level
async def upgrade_interest(session: AsyncSession, user_id: int) -> int:
"""Upgrade interest level"""
bank = await get_bank(session, user_id)
upgrade_price = interest_upgrade_price(bank.interest_level)
# Can't afford this upgrade
if upgrade_price > bank.dinks:
raise exceptions.NotEnoughDinks
bank.dinks -= upgrade_price
bank.interest_level += 1
session.add(bank)
await session.commit()
return bank.interest_level
async def upgrade_rob(session: AsyncSession, user_id: int) -> int:
"""Upgrade rob level"""
bank = await get_bank(session, user_id)
upgrade_price = rob_upgrade_price(bank.rob_level)
# Can't afford this upgrade
if upgrade_price > bank.dinks:
raise exceptions.NotEnoughDinks
bank.dinks -= upgrade_price
bank.rob_level += 1
session.add(bank)
await session.commit()
return bank.rob_level
async def gamble_dinks(
session: AsyncSession, user_id: int, amount: Union[str, int], payout_factor: int, won: bool
) -> int:
"""Gamble some of your Dinks"""
bank = await get_bank(session, user_id)
if amount == "all":
amount = bank.dinks
amount = min(int(amount), bank.dinks)
sign = 1 if won else -1
factor = (payout_factor - 1) if won else 1
await add_dinks(session, user_id, sign * amount * factor)
return amount * factor

View File

@ -0,0 +1,105 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions.constraints import DuplicateInsertException
from database.exceptions.not_found import NoResultFoundException
from database.schemas import CustomCommand, CustomCommandAlias
__all__ = [
"clean_name",
"create_alias",
"create_command",
"edit_command",
"get_all_commands",
"get_command",
"get_command_by_alias",
"get_command_by_name",
]
def clean_name(name: str) -> str:
"""Convert a name to lowercase & remove spaces to allow easier matching"""
return name.lower().replace(" ", "")
async def create_command(session: AsyncSession, name: str, response: str) -> CustomCommand:
"""Create a new custom command"""
# Check if command or alias already exists
command = await get_command(session, name)
if command is not None:
raise DuplicateInsertException
command = CustomCommand(name=name, indexed_name=clean_name(name), response=response)
session.add(command)
await session.commit()
return command
async def create_alias(session: AsyncSession, command: str, alias: str) -> CustomCommandAlias:
"""Create an alias for a command"""
# Check if the command exists
command_instance = await get_command(session, command)
if command_instance is None:
raise NoResultFoundException
# Check if the alias exists (either as an alias or as a name)
if await get_command(session, alias) is not None:
raise DuplicateInsertException
alias_instance = CustomCommandAlias(alias=alias, indexed_alias=clean_name(alias), command=command_instance)
session.add(alias_instance)
await session.commit()
return alias_instance
async def get_all_commands(session: AsyncSession) -> list[CustomCommand]:
"""Get a list of all commands"""
statement = select(CustomCommand)
return list((await session.execute(statement)).scalars().all())
async def get_command(session: AsyncSession, message: str) -> Optional[CustomCommand]:
"""Try to get a command out of a message"""
# Search lowercase & without spaces
message = clean_name(message)
return (await get_command_by_name(session, message)) or (await get_command_by_alias(session, message))
async def get_command_by_name(session: AsyncSession, message: str) -> Optional[CustomCommand]:
"""Try to get a command by its name"""
statement = select(CustomCommand).where(CustomCommand.indexed_name == message)
return (await session.execute(statement)).scalar_one_or_none()
async def get_command_by_alias(session: AsyncSession, message: str) -> Optional[CustomCommand]:
"""Try to get a command by its alias"""
statement = select(CustomCommandAlias).where(CustomCommandAlias.indexed_alias == message)
alias = (await session.execute(statement)).scalar_one_or_none()
if alias is None:
return None
return alias.command
async def edit_command(
session: AsyncSession, original_name: str, new_name: Optional[str] = None, new_response: Optional[str] = None
) -> CustomCommand:
"""Edit an existing command"""
# Check if the command exists
command = await get_command(session, original_name)
if command is None:
raise NoResultFoundException
if new_name is not None:
command.name = new_name
if new_response is not None:
command.response = new_response
session.add(command)
await session.commit()
return command

View File

@ -0,0 +1,26 @@
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions.not_found import NoResultFoundException
from database.schemas import DadJoke
__all__ = ["add_dad_joke", "get_random_dad_joke"]
async def add_dad_joke(session: AsyncSession, joke: str) -> DadJoke:
"""Add a new dad joke to the database"""
dad_joke = DadJoke(joke=joke)
session.add(dad_joke)
await session.commit()
return dad_joke
async def get_random_dad_joke(session: AsyncSession) -> DadJoke: # pragma: no cover # randomness is untestable
"""Return a random database entry"""
statement = select(DadJoke).order_by(func.random())
row = (await session.execute(statement)).first()
if row is None:
raise NoResultFoundException
return row[0]

View File

@ -0,0 +1,41 @@
import datetime
from typing import Optional
from zoneinfo import ZoneInfo
from dateutil.parser import parse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from database.schemas import Deadline, UforaCourse
__all__ = ["add_deadline", "get_deadlines"]
async def add_deadline(session: AsyncSession, course_id: int, name: str, date_str: str):
"""Add a new deadline"""
date_dt = parse(date_str, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels"))
# If we only have a day, assume it's the end of the day
if date_dt.hour == date_dt.minute == date_dt.second == 0:
date_dt.replace(hour=23, minute=59, second=59)
deadline = Deadline(course_id=course_id, name=name, deadline=date_dt)
session.add(deadline)
await session.commit()
async def get_deadlines(
session: AsyncSession, *, after: Optional[datetime.date] = None, course: Optional[UforaCourse] = None
) -> list[Deadline]:
"""Get a list of all upcoming deadlines"""
statement = select(Deadline)
if after is not None:
statement = statement.where(Deadline.deadline > after)
if course is not None:
statement = statement.where(Deadline.course_id == course.course_id)
statement = statement.options(selectinload(Deadline.course))
return list((await session.execute(statement)).scalars().all())

View File

@ -0,0 +1,12 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import EasterEgg
__all__ = ["get_all_easter_eggs"]
async def get_all_easter_eggs(session: AsyncSession) -> list[EasterEgg]:
"""Return a list of all easter eggs"""
statement = select(EasterEgg)
return list((await session.execute(statement)).scalars().all())

View File

@ -0,0 +1,50 @@
import datetime
from typing import Optional
from zoneinfo import ZoneInfo
from dateutil.parser import parse
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import Event
__all__ = ["add_event", "delete_event_by_id", "get_event_by_id", "get_events", "get_next_event"]
async def add_event(
session: AsyncSession, *, name: str, description: Optional[str], date_str: str, channel_id: int
) -> Event:
"""Create a new event"""
date_dt = parse(date_str, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels"))
event = Event(name=name, description=description, timestamp=date_dt, notification_channel=channel_id)
session.add(event)
await session.commit()
await session.refresh(event)
return event
async def delete_event_by_id(session: AsyncSession, event_id: int):
"""Delete an event by its id"""
statement = delete(Event).where(Event.event_id == event_id)
await session.execute(statement)
await session.commit()
async def get_event_by_id(session: AsyncSession, event_id: int) -> Optional[Event]:
"""Get an event by its id"""
statement = select(Event).where(Event.event_id == event_id)
return (await session.execute(statement)).scalar_one_or_none()
async def get_events(session: AsyncSession, *, now: datetime.datetime) -> list[Event]:
"""Get a list of all upcoming events"""
statement = select(Event).where(Event.timestamp > now)
return list((await session.execute(statement)).scalars().all())
async def get_next_event(session: AsyncSession, *, now: datetime.datetime) -> Optional[Event]:
"""Get the first upcoming event"""
statement = select(Event).where(Event.timestamp > now).order_by(Event.timestamp)
return (await session.execute(statement)).scalars().first()

View File

@ -0,0 +1,20 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import FreeGame
__all__ = ["add_free_games", "filter_present_games"]
async def add_free_games(session: AsyncSession, game_ids: list[int]):
"""Bulk-add a list of IDs into the database"""
games = [FreeGame(free_game_id=game_id) for game_id in game_ids]
session.add_all(games)
await session.commit()
async def filter_present_games(session: AsyncSession, game_ids: list[int]) -> list[int]:
"""Filter a list of game IDs down to the ones that aren't in the database yet"""
statement = select(FreeGame.free_game_id).where(FreeGame.free_game_id.in_(game_ids))
matches: list[int] = list((await session.execute(statement)).scalars().all())
return list(set(game_ids).difference(matches))

View File

@ -0,0 +1,51 @@
from typing import Optional
import sqlalchemy.exc
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions import (
DuplicateInsertException,
Forbidden,
NoResultFoundException,
)
from database.schemas import GitHubLink
__all__ = ["add_github_link", "delete_github_link_by_id", "get_github_links"]
async def add_github_link(session: AsyncSession, user_id: int, url: str) -> GitHubLink:
"""Add a new GitHub link into the database"""
try:
gh_link = GitHubLink(user_id=user_id, url=url)
session.add(gh_link)
await session.commit()
await session.refresh(gh_link)
except sqlalchemy.exc.IntegrityError:
raise DuplicateInsertException
return gh_link
async def delete_github_link_by_id(session: AsyncSession, user_id: int, link_id: int):
"""Remove an existing link from the database
You can only remove links owned by you
"""
select_statement = select(GitHubLink).where(GitHubLink.github_link_id == link_id)
gh_link: Optional[GitHubLink] = (await session.execute(select_statement)).scalar_one_or_none()
if gh_link is None:
raise NoResultFoundException
if gh_link.user_id != user_id:
raise Forbidden
delete_statement = delete(GitHubLink).where(GitHubLink.github_link_id == gh_link.github_link_id)
await session.execute(delete_statement)
await session.commit()
async def get_github_links(session: AsyncSession, user_id: int) -> list[GitHubLink]:
"""Get a user's GitHub links"""
statement = select(GitHubLink).where(GitHubLink.user_id == user_id)
return list((await session.execute(statement)).scalars().all())

View File

@ -0,0 +1,45 @@
from typing import Optional
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions import NoResultFoundException
from database.schemas import Link
__all__ = ["add_link", "edit_link", "get_all_links", "get_link_by_name"]
async def get_all_links(session: AsyncSession) -> list[Link]:
"""Get a list of all links"""
statement = select(Link)
return list((await session.execute(statement)).scalars().all())
async def add_link(session: AsyncSession, name: str, url: str) -> Link:
"""Add a new link into the database"""
if name.islower():
name = name.capitalize()
instance = Link(name=name, url=url)
session.add(instance)
await session.commit()
return instance
async def get_link_by_name(session: AsyncSession, name: str) -> Optional[Link]:
"""Get a link by its name"""
statement = select(Link).where(func.lower(Link.name) == name.lower())
return (await session.execute(statement)).scalar_one_or_none()
async def edit_link(session: AsyncSession, name: str, new_url: str):
"""Edit an existing link"""
link: Optional[Link] = await get_link_by_name(session, name)
if link is None:
raise NoResultFoundException
link.url = new_url
session.add(link)
await session.commit()

View File

@ -0,0 +1,35 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import MemeTemplate
__all__ = ["add_meme", "get_all_memes", "get_meme_by_name"]
async def add_meme(session: AsyncSession, name: str, template_id: int, field_count: int) -> Optional[MemeTemplate]:
"""Add a new meme into the database"""
try:
meme = MemeTemplate(name=name, template_id=template_id, field_count=field_count)
session.add(meme)
await session.commit()
return meme
except IntegrityError:
return None
async def get_all_memes(session: AsyncSession) -> list[MemeTemplate]:
"""Get a list of all memes"""
statement = select(MemeTemplate)
return list((await session.execute(statement)).scalars().all())
async def get_meme_by_name(session: AsyncSession, query: str) -> Optional[MemeTemplate]:
"""Try to find a meme by its name
Returns the first match found by PSQL
"""
statement = select(MemeTemplate).where(MemeTemplate.name.ilike(f"%{query.lower()}%"))
return (await session.execute(statement)).scalar()

View File

@ -0,0 +1,42 @@
from typing import Optional
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud.users import get_or_add_user
from database.enums import ReminderCategory
from database.schemas import Reminder
__all__ = ["get_all_reminders_for_category", "toggle_reminder"]
async def get_all_reminders_for_category(session: AsyncSession, category: ReminderCategory) -> list[Reminder]:
"""Get a list of all Reminders for a given category"""
statement = select(Reminder).where(Reminder.category == category)
return list((await session.execute(statement)).scalars().all())
async def toggle_reminder(session: AsyncSession, user_id: int, category: ReminderCategory) -> bool:
"""Switch a category on/off
Returns the new value for the category
"""
await get_or_add_user(session, user_id)
select_statement = select(Reminder).where(Reminder.user_id == user_id).where(Reminder.category == category)
reminder: Optional[Reminder] = (await session.execute(select_statement)).scalar_one_or_none()
# No reminder set yet
if reminder is None:
reminder = Reminder(user_id=user_id, category=category)
session.add(reminder)
await session.commit()
return True
# Reminder found -> delete it
delete_statement = delete(Reminder).where(Reminder.reminder_id == reminder.reminder_id)
await session.execute(delete_statement)
await session.commit()
return False

View File

@ -0,0 +1,32 @@
import datetime
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.enums import TaskType
from database.schemas import Task
from database.utils.datetime import LOCAL_TIMEZONE
__all__ = ["get_task_by_enum", "set_last_task_execution_time"]
async def get_task_by_enum(session: AsyncSession, task: TaskType) -> Optional[Task]:
"""Get a task by its enum value, if it exists
Returns None if the task does not exist
"""
statement = select(Task).where(Task.task == task)
return (await session.execute(statement)).scalar_one_or_none()
async def set_last_task_execution_time(session: AsyncSession, task: TaskType):
"""Set the last time a specific task was executed"""
_task = await get_task_by_enum(session, task)
if _task is None:
_task = Task(task=task)
_task.previous_run = datetime.datetime.now(tz=LOCAL_TIMEZONE)
session.add(_task)
await session.commit()

View File

@ -0,0 +1,38 @@
import datetime
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import UforaAnnouncement, UforaCourse
__all__ = ["create_new_announcement", "get_courses_with_announcements", "remove_old_announcements"]
async def get_courses_with_announcements(session: AsyncSession) -> list[UforaCourse]:
"""Get all courses where announcements are enabled"""
statement = select(UforaCourse).where(UforaCourse.log_announcements)
return list((await session.execute(statement)).scalars().all())
async def create_new_announcement(
session: AsyncSession, announcement_id: int, course: UforaCourse, publication_date: datetime.datetime
) -> UforaAnnouncement:
"""Add a new announcement to the database"""
new_announcement = UforaAnnouncement(
announcement_id=announcement_id, course=course, publication_date=publication_date
)
session.add(new_announcement)
await session.commit()
return new_announcement
async def remove_old_announcements(session: AsyncSession):
"""Delete all announcements that are > 8 days old
The RSS feed only goes back 7 days, so all of these old announcements never have to
be checked again when checking if an announcement is fresh or not.
"""
limit = datetime.datetime.utcnow() - datetime.timedelta(days=8)
statement = delete(UforaAnnouncement).where(UforaAnnouncement.publication_date < limit)
await session.execute(statement)
await session.commit()

View File

@ -0,0 +1,38 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import UforaCourse, UforaCourseAlias
__all__ = ["get_all_courses", "get_course_by_code", "get_course_by_name"]
async def get_all_courses(session: AsyncSession) -> list[UforaCourse]:
"""Get a list of all courses in the database"""
statement = select(UforaCourse)
return list((await session.execute(statement)).scalars().all())
async def get_course_by_code(session: AsyncSession, code: str) -> Optional[UforaCourse]:
"""Try to find a course by its code"""
statement = select(UforaCourse).where(UforaCourse.code == code)
return (await session.execute(statement)).scalar_one_or_none()
async def get_course_by_name(session: AsyncSession, query: str) -> Optional[UforaCourse]:
"""Try to find a course by its name
This checks for regular name first, and then aliases
"""
# Search case-insensitively
query = query.lower()
course_statement = select(UforaCourse).where(UforaCourse.name.ilike(f"%{query}%"))
course_result = (await session.execute(course_statement)).scalars().first()
if course_result:
return course_result
alias_statement = select(UforaCourseAlias).where(UforaCourseAlias.alias.ilike(f"%{query}%"))
alias_result = (await session.execute(alias_statement)).scalars().first()
return alias_result.course if alias_result else None

View File

@ -0,0 +1,47 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import Bank, NightlyData, User
__all__ = [
"get_or_add_user",
]
async def get_or_add_user(session: AsyncSession, user_id: int, *, options: Optional[list] = None) -> User:
"""Get a user's profile
If it doesn't exist yet, create it (along with all linked datastructures)
"""
if options is None:
options = []
statement = select(User).where(User.user_id == user_id).options(*options)
user: Optional[User] = (await session.execute(statement)).scalar_one_or_none()
# User exists
if user is not None:
return user
# Create new user
user = User(user_id=user_id)
session.add(user)
await session.commit()
# Add bank & nightly info
bank = Bank(user_id=user_id)
nightly_data = NightlyData(user_id=user_id)
user.bank = bank
user.nightly_data = nightly_data
session.add(bank)
session.add(nightly_data)
session.add(user)
await session.commit()
await session.refresh(user)
return user

24
database/engine.py 100644
View File

@ -0,0 +1,24 @@
from urllib.parse import quote_plus
from sqlalchemy.engine import URL
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
import settings
encoded_postgres_password = quote_plus(settings.POSTGRES_PASS)
# PostgreSQL engine
postgres_engine = create_async_engine(
URL.create(
drivername="postgresql+asyncpg",
username=settings.POSTGRES_USER,
password=encoded_postgres_password,
host=settings.POSTGRES_HOST,
port=settings.POSTGRES_PORT,
database=settings.POSTGRES_DB,
),
pool_pre_ping=True,
future=True,
)
DBSession = async_sessionmaker(autocommit=False, autoflush=False, bind=postgres_engine, expire_on_commit=False)

17
database/enums.py 100644
View File

@ -0,0 +1,17 @@
import enum
__all__ = ["ReminderCategory", "TaskType"]
class ReminderCategory(enum.IntEnum):
"""Enum for reminder categories"""
LES = enum.auto()
class TaskType(enum.IntEnum):
"""Enum for the different types of tasks"""
BIRTHDAYS = enum.auto()
SCHEDULES = enum.auto()
UFORA_ANNOUNCEMENTS = enum.auto()

View File

@ -0,0 +1,13 @@
from .constraints import DuplicateInsertException, ForbiddenNameException
from .currency import DoubleNightly, NotEnoughDinks
from .forbidden import Forbidden
from .not_found import NoResultFoundException
__all__ = [
"DuplicateInsertException",
"ForbiddenNameException",
"Forbidden",
"DoubleNightly",
"NotEnoughDinks",
"NoResultFoundException",
]

View File

@ -0,0 +1,9 @@
__all__ = ["DuplicateInsertException", "ForbiddenNameException"]
class DuplicateInsertException(Exception):
"""Exception raised when a value already exists"""
class ForbiddenNameException(Exception):
"""Exception raised when trying to insert something with a name that isn't allowed"""

View File

@ -0,0 +1,9 @@
__all__ = ["DoubleNightly", "NotEnoughDinks"]
class DoubleNightly(Exception):
"""Exception raised when claiming nightlies multiple times per day"""
class NotEnoughDinks(Exception):
"""Exception raised when trying to do something you don't have the Dinks for"""

View File

@ -0,0 +1,5 @@
__all__ = ["Forbidden"]
class Forbidden(Exception):
"""Exception raised when trying to access a resource that isn't yours"""

View File

@ -0,0 +1,5 @@
__all__ = ["NoResultFoundException"]
class NoResultFoundException(Exception):
"""Exception raised when nothing was found"""

View File

@ -0,0 +1,53 @@
import logging
from sqlalchemy.orm import Session
from alembic import command, script
from alembic.config import Config
from alembic.runtime import migration
from database.engine import postgres_engine
__config_path__ = "alembic.ini"
__migrations_path__ = "alembic/"
cfg = Config(__config_path__)
cfg.set_main_option("script_location", __migrations_path__)
__all__ = ["ensure_latest_migration", "migrate"]
async def ensure_latest_migration():
"""Make sure we are currently on the latest revision, otherwise raise an exception"""
alembic_script = script.ScriptDirectory.from_config(cfg)
async with postgres_engine.begin() as connection:
current_revision = await connection.run_sync(
lambda sync_connection: migration.MigrationContext.configure(sync_connection).get_current_revision()
)
alembic_head = alembic_script.get_current_head()
if current_revision != alembic_head:
error_message = (
f"Pending migrations (current revision is {current_revision}, while head is at {alembic_head})"
)
logging.error(error_message)
raise RuntimeError(error_message)
def __execute_upgrade(connection: Session):
cfg.attributes["connection"] = connection
command.upgrade(cfg, "head")
def __execute_downgrade(connection: Session):
cfg.attributes["connection"] = connection
command.downgrade(cfg, "base")
async def migrate(up: bool):
"""Migrate the database upwards or downwards"""
async with postgres_engine.begin() as connection:
await connection.run_sync(__execute_upgrade if up else __execute_downgrade)

336
database/schemas.py 100644
View File

@ -0,0 +1,336 @@
from __future__ import annotations
from datetime import date, datetime
from typing import List, Optional
from sqlalchemy import BigInteger, ForeignKey, UniqueConstraint
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.types import DateTime
from database import enums
__all__ = [
"Base",
"Bank",
"Birthday",
"Bookmark",
"CommandStats",
"CustomCommand",
"CustomCommandAlias",
"DadJoke",
"Deadline",
"EasterEgg",
"Event",
"FreeGame",
"GitHubLink",
"Link",
"MemeTemplate",
"NightlyData",
"Reminder",
"Task",
"UforaAnnouncement",
"UforaCourse",
"UforaCourseAlias",
"User",
]
class Base(DeclarativeBase):
"""Required base class for all tables"""
# Make all DateTimes timezone-aware
type_annotation_map = {datetime: DateTime(timezone=True)}
class Bank(Base):
"""A user's currency information"""
__tablename__ = "bank"
bank_id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
dinks: Mapped[int] = mapped_column(BigInteger, server_default="0", nullable=False)
invested: Mapped[int] = mapped_column(BigInteger, server_default="0", nullable=False)
# Interest rate
interest_level: Mapped[int] = mapped_column(server_default="1", nullable=False)
# Maximum amount that can be stored in the bank
capacity_level: Mapped[int] = mapped_column(server_default="1", nullable=False)
# Maximum amount that can be robbed
rob_level: Mapped[int] = mapped_column(server_default="1", nullable=False)
user: Mapped[User] = relationship(uselist=False, back_populates="bank", lazy="selectin")
class Birthday(Base):
"""A user's birthday"""
__tablename__ = "birthdays"
birthday_id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
birthday: Mapped[date] = mapped_column(nullable=False)
user: Mapped[User] = relationship(uselist=False, back_populates="birthday", lazy="selectin")
class Bookmark(Base):
"""A bookmark to a given message"""
__tablename__ = "bookmarks"
__table_args__ = (UniqueConstraint("user_id", "label"),)
bookmark_id: Mapped[int] = mapped_column(primary_key=True)
label: Mapped[str] = mapped_column(nullable=False)
jump_url: Mapped[str] = mapped_column(nullable=False)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
user: Mapped[User] = relationship(back_populates="bookmarks", uselist=False, lazy="selectin")
class CommandStats(Base):
"""Metrics on how often commands are used"""
__tablename__ = "command_stats"
command_stats_id: Mapped[int] = mapped_column(primary_key=True)
command: Mapped[str] = mapped_column(nullable=False)
timestamp: Mapped[datetime] = mapped_column(nullable=False)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
slash: Mapped[bool] = mapped_column(nullable=False)
context_menu: Mapped[bool] = mapped_column(nullable=False)
user: Mapped[User] = relationship(back_populates="command_stats", uselist=False, lazy="selectin")
class CustomCommand(Base):
"""Custom commands to fill the hole Dyno couldn't"""
__tablename__ = "custom_commands"
command_id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
indexed_name: Mapped[str] = mapped_column(nullable=False, index=True)
response: Mapped[str] = mapped_column(nullable=False)
aliases: Mapped[List[CustomCommandAlias]] = relationship(
back_populates="command", uselist=True, cascade="all, delete-orphan", lazy="selectin"
)
class CustomCommandAlias(Base):
"""Aliases for custom commands"""
__tablename__ = "custom_command_aliases"
alias_id: Mapped[int] = mapped_column(primary_key=True)
alias: Mapped[str] = mapped_column(nullable=False, unique=True)
indexed_alias: Mapped[str] = mapped_column(nullable=False, index=True)
command_id: Mapped[int] = mapped_column(ForeignKey("custom_commands.command_id"))
command: Mapped[CustomCommand] = relationship(back_populates="aliases", uselist=False, lazy="selectin")
class DadJoke(Base):
"""When I finally understood asymptotic notation, it was a big "oh" moment"""
__tablename__ = "dad_jokes"
dad_joke_id: Mapped[int] = mapped_column(primary_key=True)
joke: Mapped[str] = mapped_column(nullable=False)
class Deadline(Base):
"""A deadline for a university project"""
__tablename__ = "deadlines"
deadline_id: Mapped[int] = mapped_column(primary_key=True)
course_id: Mapped[int] = mapped_column(ForeignKey("ufora_courses.course_id"))
name: Mapped[str] = mapped_column(nullable=False)
deadline: Mapped[datetime] = mapped_column(nullable=False)
course: Mapped[UforaCourse] = relationship(back_populates="deadlines", uselist=False, lazy="selectin")
class EasterEgg(Base):
"""An easter egg response"""
__tablename__ = "easter_eggs"
easter_egg_id: Mapped[int] = mapped_column(primary_key=True)
match: Mapped[str] = mapped_column(nullable=False)
response: Mapped[str] = mapped_column(nullable=False)
exact: Mapped[bool] = mapped_column(nullable=False, server_default="1")
startswith: Mapped[bool] = mapped_column(nullable=False, server_default="1")
class Event(Base):
"""A scheduled event"""
__tablename__ = "events"
event_id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
description: Mapped[Optional[str]] = mapped_column(nullable=True)
notification_channel: Mapped[int] = mapped_column(BigInteger, nullable=False)
timestamp: Mapped[datetime] = mapped_column(nullable=False)
class FreeGame(Base):
"""A temporarily free game"""
__tablename__ = "free_games"
free_game_id: Mapped[int] = mapped_column(primary_key=True)
class GitHubLink(Base):
"""A user's GitHub link"""
__tablename__ = "github_links"
github_link_id: Mapped[int] = mapped_column(primary_key=True)
url: Mapped[str] = mapped_column(nullable=False, unique=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
user: Mapped[User] = relationship(back_populates="github_links", uselist=False, lazy="selectin")
class Link(Base):
"""Useful links that go useful places"""
__tablename__ = "links"
link_id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
url: Mapped[str] = mapped_column(nullable=False)
class MemeTemplate(Base):
"""A meme template for the Imgflip API"""
__tablename__ = "meme"
meme_id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
template_id: Mapped[int] = mapped_column(nullable=False, unique=True)
field_count: Mapped[int] = mapped_column(nullable=False)
class NightlyData(Base):
"""Data for a user's Nightly stats"""
__tablename__ = "nightly_data"
nightly_id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
last_nightly: Mapped[Optional[date]] = mapped_column(nullable=True)
count: Mapped[int] = mapped_column(server_default="0", nullable=False)
user: Mapped[User] = relationship(back_populates="nightly_data", uselist=False, lazy="selectin")
class Reminder(Base):
"""Something that a user should be reminded of"""
__tablename__ = "reminders"
reminder_id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.user_id"))
category: Mapped[enums.ReminderCategory] = mapped_column(nullable=False)
user: Mapped[User] = relationship(back_populates="reminders", uselist=False, lazy="selectin")
class Task(Base):
"""A Didier task"""
__tablename__ = "tasks"
task_id: Mapped[int] = mapped_column(primary_key=True)
task: Mapped[enums.TaskType] = mapped_column(nullable=False, unique=True)
previous_run: Mapped[datetime] = mapped_column(nullable=True)
class UforaCourse(Base):
"""A course on Ufora"""
__tablename__ = "ufora_courses"
course_id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
code: Mapped[str] = mapped_column(nullable=False, unique=True)
year: Mapped[int] = mapped_column(nullable=False)
compulsory: Mapped[bool] = mapped_column(server_default="1", nullable=False)
role_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, unique=False)
overarching_role_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, unique=False)
# This is not the greatest fix, but there can only ever be two, so it will do the job
alternative_overarching_role_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, unique=False)
log_announcements: Mapped[bool] = mapped_column(server_default="0", nullable=False)
announcements: Mapped[List[UforaAnnouncement]] = relationship(
back_populates="course", cascade="all, delete-orphan", lazy="selectin"
)
aliases: Mapped[List[UforaCourseAlias]] = relationship(
back_populates="course", cascade="all, delete-orphan", lazy="selectin"
)
deadlines: Mapped[List[Deadline]] = relationship(
back_populates="course", cascade="all, delete-orphan", lazy="selectin"
)
class UforaCourseAlias(Base):
"""An alias for a course on Ufora that we use to refer to them"""
__tablename__ = "ufora_course_aliases"
alias_id: Mapped[int] = mapped_column(primary_key=True)
alias: Mapped[str] = mapped_column(nullable=False, unique=True)
course_id: Mapped[int] = mapped_column(ForeignKey("ufora_courses.course_id"))
course: Mapped[UforaCourse] = relationship(back_populates="aliases", uselist=False, lazy="selectin")
class UforaAnnouncement(Base):
"""An announcement sent on Ufora"""
__tablename__ = "ufora_announcements"
announcement_id: Mapped[int] = mapped_column(primary_key=True)
course_id: Mapped[int] = mapped_column(ForeignKey("ufora_courses.course_id"))
publication_date: Mapped[date] = mapped_column()
course: Mapped[UforaCourse] = relationship(back_populates="announcements", uselist=False, lazy="selectin")
class User(Base):
"""A Didier user"""
__tablename__ = "users"
user_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
bank: Mapped[Bank] = relationship(
back_populates="user", uselist=False, lazy="selectin", cascade="all, delete-orphan"
)
birthday: Mapped[Optional[Birthday]] = relationship(
back_populates="user", uselist=False, lazy="selectin", cascade="all, delete-orphan"
)
bookmarks: Mapped[List[Bookmark]] = relationship(
back_populates="user", uselist=True, lazy="selectin", cascade="all, delete-orphan"
)
command_stats: Mapped[List[CommandStats]] = relationship(
back_populates="user", uselist=True, lazy="selectin", cascade="all, delete-orphan"
)
github_links: Mapped[List[GitHubLink]] = relationship(
back_populates="user", uselist=True, lazy="selectin", cascade="all, delete-orphan"
)
nightly_data: Mapped[NightlyData] = relationship(
back_populates="user", uselist=False, lazy="selectin", cascade="all, delete-orphan"
)
reminders: Mapped[List[Reminder]] = relationship(
back_populates="user", uselist=True, lazy="selectin", cascade="all, delete-orphan"
)

View File

@ -0,0 +1,23 @@
from sqlalchemy.ext.asyncio import AsyncSession
from database.engine import DBSession
from database.schemas import UforaCourse
__all__ = ["main"]
async def main():
"""Example script: add a Ufora course"""
session: AsyncSession
async with DBSession() as session:
modsim = UforaCourse(
course_id=439235,
code="C003786",
name="Modelleren en Simuleren",
year=3,
compulsory=False,
role_id=785577582561067028,
)
session.add_all([modsim])
await session.commit()

View File

@ -0,0 +1,69 @@
from sqlalchemy.ext.asyncio import AsyncSession
from database.engine import DBSession
from database.schemas import EasterEgg
__all__ = ["main"]
async def main():
"""Add the initial easter egg responses"""
session: AsyncSession
async with DBSession() as session:
# https://www.youtube.com/watch?v=Vd6hVYkkq88
do_not_cite_deep_magic = EasterEgg(
match=r"((don'?t)|(do not)) cite the deep magic to me,? witch",
response="_I was there when it was written_",
exact=True,
)
# https://www.youtube.com/watch?v=LrHTR22pIhw
dormammu = EasterEgg(match=r"dormammu", response="_I've come to bargain_", exact=True)
# https://youtu.be/rEq1Z0bjdwc?t=7
hello_there = EasterEgg(match=r"hello there", response="_General Kenobi_", exact=True)
# https://www.youtube.com/watch?v=_WZCvQ5J3pk
hey = EasterEgg(
match=r"hey,? ?(?:you)?",
response="_You're finally awake!_",
exact=True,
)
# https://www.youtube.com/watch?v=2z5ZDC1eQEA
is_this_the_kk = EasterEgg(
match=r"is (this|dis) (.*)", response="No, this is Patrick.", exact=False, startswith=True
)
# https://youtu.be/d6uckPRKvSg?t=4
its_over_anakin = EasterEgg(
match=r"it'?s over ", response="_I have the high ground_", exact=False, startswith=True
)
# https://www.youtube.com/watch?v=Vx5prDjKAcw
perfectly_balanced = EasterEgg(match=r"perfectly balanced", response="_As all things should be_", exact=True)
# ( ͡◉ ͜ʖ ͡◉)
sixty_nine = EasterEgg(match=r"(^69$)|(^69 )|( 69 )|( 69$)", response="_Nice_", exact=False, startswith=False)
# https://youtu.be/7mbLzkNFDs8?t=19
what_did_it_cost = EasterEgg(match=r"what did it cost\??", response="_Everything_", exact=True)
# https://youtu.be/EJfYh-JVbJA?t=10
you_cant_defeat_me = EasterEgg(match=r"you can'?t defeat me", response="_I know, but he can_", exact=False)
session.add_all(
[
do_not_cite_deep_magic,
dormammu,
hello_there,
hey,
is_this_the_kk,
its_over_anakin,
perfectly_balanced,
sixty_nine,
what_did_it_cost,
you_cant_defeat_me,
]
)
await session.commit()

View File

@ -0,0 +1,599 @@
from sqlalchemy.ext.asyncio import AsyncSession
from database.engine import DBSession
from database.schemas import UforaCourse, UforaCourseAlias
__all__ = ["main"]
async def main():
"""Add the Ufora courses for the 2022-2023 academic year
Course id's are only used to fetch announcements, and I can only fetch announcements of courses I enroll in,
so other courses can use an auto-generated id
This will never clash as there will never be 650k regular courses
"""
session: AsyncSession
async with DBSession() as session:
"""3rd Bachelor"""
artificiele_intelligentie = UforaCourse(
code="C003756",
name="Artificiële Intelligentie",
year=3,
compulsory=True,
role_id=891743671022673920,
overarching_role_id=891743208248324196,
)
algoritmen_datastructuren_3 = UforaCourse(
code="C003782",
name="Algoritmen en Datastructuren 3",
year=3,
compulsory=True,
role_id=891743791466307654,
overarching_role_id=891743208248324196,
)
automaten_berekenbaarheid_complexiteit = UforaCourse(
code="C003785",
name="Automaten, Berekenbaarheid en Complexiteit",
year=3,
compulsory=True,
role_id=891744082404200539,
overarching_role_id=891743208248324196,
)
besturingssystemen = UforaCourse(
code="E019010",
name="Besturingssystemen",
year=3,
compulsory=True,
role_id=891743898291032114,
overarching_role_id=891743208248324196,
)
computationele_biologie = UforaCourse(
code="C003789",
name="Computationele Biologie",
year=3,
compulsory=True,
role_id=891744050988847135,
overarching_role_id=891743208248324196,
)
logisch_programmeren = UforaCourse(
code="C003783",
name="Logisch Programmeren",
year=3,
compulsory=True,
role_id=891743966482034800,
overarching_role_id=891743208248324196,
)
software_engineering_lab_2 = UforaCourse(
code="C003784",
name="Software Engineering Lab 2",
year=3,
compulsory=True,
role_id=891744007300980737,
overarching_role_id=891743208248324196,
)
modelleren_en_simuleren = UforaCourse(
course_id=636139,
code="C003786",
name="Modelleren en Simuleren",
year=3,
compulsory=True,
role_id=1024573730418069565,
overarching_role_id=891744461405687808,
log_announcements=True,
)
informatiebeveiliging = UforaCourse(
code="E019400",
name="Informatiebeveiliging",
year=3,
compulsory=True,
role_id=1023333190678626314,
overarching_role_id=891744461405687808,
alternative_overarching_role_id=1023278462733127710,
)
parallelle_computersystemen = UforaCourse(
code="E034140",
name="Parallelle Computersystemen",
year=3,
compulsory=True,
role_id=1023300295918358691,
overarching_role_id=891744461405687808,
alternative_overarching_role_id=1023278462733127710,
)
inleiding_tot_elektrotechniek = UforaCourse(
code="C003806",
name="Inleiding tot de Elektrotechniek",
year=3,
compulsory=True,
overarching_role_id=891744390035415111,
)
inleiding_tot_telecommunicatie = UforaCourse(
code="C003787",
name="Inleiding tot de Telecommunicatie",
year=3,
compulsory=True,
overarching_role_id=891744390035415111,
)
wiskundige_modellering = UforaCourse(
code="C003788",
name="Wiskundige Modellering in de Ingenieurswetenschappen",
year=3,
compulsory=True,
overarching_role_id=891744390035415111,
)
session.add_all(
[
artificiele_intelligentie,
algoritmen_datastructuren_3,
automaten_berekenbaarheid_complexiteit,
besturingssystemen,
computationele_biologie,
logisch_programmeren,
software_engineering_lab_2,
modelleren_en_simuleren,
informatiebeveiliging,
parallelle_computersystemen,
inleiding_tot_elektrotechniek,
inleiding_tot_telecommunicatie,
wiskundige_modellering,
]
)
await session.commit()
"""Aliases"""
ai = UforaCourseAlias(course_id=artificiele_intelligentie.course_id, alias="AI")
ad3 = UforaCourseAlias(course_id=algoritmen_datastructuren_3.course_id, alias="AD3")
abc = UforaCourseAlias(course_id=automaten_berekenbaarheid_complexiteit.course_id, alias="ABC")
bs = UforaCourseAlias(course_id=besturingssystemen.course_id, alias="BS")
compbio = UforaCourseAlias(course_id=computationele_biologie.course_id, alias="Compbio")
logprog = UforaCourseAlias(course_id=logisch_programmeren.course_id, alias="LogProg")
prolog = UforaCourseAlias(course_id=logisch_programmeren.course_id, alias="Prolog")
sel2 = UforaCourseAlias(course_id=software_engineering_lab_2.course_id, alias="SEL2")
selab2 = UforaCourseAlias(course_id=software_engineering_lab_2.course_id, alias="SELab2")
modsim = UforaCourseAlias(course_id=modelleren_en_simuleren.course_id, alias="ModSim")
infosec = UforaCourseAlias(course_id=informatiebeveiliging.course_id, alias="InfoSec")
information_security = UforaCourseAlias(course_id=informatiebeveiliging.course_id, alias="Information Security")
pcs = UforaCourseAlias(course_id=parallelle_computersystemen.course_id, alias="PCS")
parallel_computer_systems = UforaCourseAlias(
course_id=parallelle_computersystemen.course_id, alias="Parallel Computer Systems"
)
elektro = UforaCourseAlias(course_id=inleiding_tot_elektrotechniek.course_id, alias="Elektro")
elektrotechniek = UforaCourseAlias(course_id=inleiding_tot_elektrotechniek.course_id, alias="Elektrotechniek")
telecom = UforaCourseAlias(course_id=inleiding_tot_telecommunicatie.course_id, alias="Telecom")
wimo = UforaCourseAlias(course_id=wiskundige_modellering.course_id, alias="WiMo")
session.add_all(
[
ai,
ad3,
abc,
bs,
compbio,
logprog,
prolog,
sel2,
selab2,
modsim,
infosec,
information_security,
pcs,
parallel_computer_systems,
elektro,
elektrotechniek,
telecom,
wimo,
]
)
await session.commit()
"""1st Master CS"""
fundamenten_van_programmeertalen = UforaCourse(
course_id=633639,
code="C003241",
name="Fundamenten van Programmeertalen",
year=4,
compulsory=True,
role_id=1023298665416228876,
overarching_role_id=1023293447387496570,
)
machine_learning = UforaCourse(
course_id=630807,
code="C003758",
name="Machine Learning (CS)",
year=4,
compulsory=True,
role_id=1023294041825235087,
overarching_role_id=1023293447387496570,
)
parallelle_en_gedistribueerde = UforaCourse(
course_id=633583,
code="E017930",
name="Parallelle en Gedistribueerde Softwaresystemen",
year=4,
compulsory=True,
role_id=1023293978273136700,
overarching_role_id=1023293447387496570,
alternative_overarching_role_id=1023278462733127710,
)
discrete_algoritmen = UforaCourse(
course_id=633675,
code="C003349",
name="Discrete Algoritmen",
year=4,
compulsory=True,
role_id=1023299229277487145,
overarching_role_id=1023293447387496570,
)
software_engineering_lab_3 = UforaCourse(
course_id=631370,
code="C004072",
name="Software Engineering Lab 3",
year=4,
compulsory=True,
role_id=1023299234008678550,
overarching_role_id=1023293447387496570,
)
compilers = UforaCourse(
course_id=633663,
code="E018520",
name="Compilers",
year=4,
compulsory=True,
role_id=1023299237003399249,
overarching_role_id=1023293447387496570,
)
datavisualisatie = UforaCourse(
course_id=630803,
code="C004041",
name="Datavisualisatie",
year=4,
compulsory=True,
role_id=1023299239243161671,
overarching_role_id=1023293447387496570,
)
recht_van_intellectuele_eigendom = UforaCourse(
course_id=633696,
code="C000957",
name="Recht van de Intellectuele Eigendom",
year=4,
compulsory=True,
role_id=1023299241457745930,
overarching_role_id=1023293447387496570,
)
"""2nd Master CS"""
computergrafiek = UforaCourse(
code="C004073",
name="Computergrafiek",
year=5,
compulsory=True,
role_id=1023303199609860268,
overarching_role_id=1023302736210567208,
)
big_data_science = UforaCourse(
code="C004074",
name="Big Data Science",
year=5,
compulsory=True,
role_id=1023303190046851153,
overarching_role_id=1023302736210567208,
)
bedrijfsstage = UforaCourse(
code="C004075",
name="Bedrijfsstage",
year=5,
compulsory=True,
role_id=1023303201807679598,
overarching_role_id=1023302736210567208,
)
masterproef = UforaCourse(
code="C002309",
name="Masterproef",
year=5,
compulsory=True,
role_id=1023319264851144754,
overarching_role_id=1023302736210567208,
alternative_overarching_role_id=1023300434800164914,
)
"""1st Master CSE"""
design_of_multimedia_applications = UforaCourse(
code="E017920",
name="Design of Multimedia Applications",
year=4,
compulsory=True,
role_id=1023300418635317259,
overarching_role_id=1023278462733127710,
)
research_project = UforaCourse(
code="E031710",
name="Research Project",
year=4,
compulsory=True,
role_id=1023300421776855160,
overarching_role_id=1023278462733127710,
)
design_project = UforaCourse(
code="E033710",
name="Design Project",
year=4,
compulsory=True,
role_id=1023300424561852537,
overarching_role_id=1023278462733127710,
)
mobile_and_broadband_access_networks = UforaCourse(
code="E012320",
name="Mobile and Broadband Access Networks",
year=4,
compulsory=True,
role_id=1023300427246223471,
overarching_role_id=1023278462733127710,
)
information_theory = UforaCourse(
code="E003600",
name="Information Theory",
year=4,
compulsory=True,
role_id=1023300429469204480,
overarching_role_id=1023278462733127710,
)
queueing_analysis_and_simulation = UforaCourse(
code="E011322",
name="Queueing Analysis and Simulation",
year=4,
compulsory=True,
role_id=1023300431696371793,
overarching_role_id=1023278462733127710,
)
session.add_all(
[
fundamenten_van_programmeertalen,
machine_learning,
parallelle_en_gedistribueerde,
discrete_algoritmen,
software_engineering_lab_3,
compilers,
datavisualisatie,
recht_van_intellectuele_eigendom,
computergrafiek,
big_data_science,
bedrijfsstage,
masterproef,
design_of_multimedia_applications,
research_project,
design_project,
mobile_and_broadband_access_networks,
information_theory,
queueing_analysis_and_simulation,
]
)
await session.commit()
"""Master aliases"""
fundamenten = UforaCourseAlias(course_id=fundamenten_van_programmeertalen.course_id, alias="Fundamenten")
ml = UforaCourseAlias(course_id=machine_learning.course_id, alias="ML")
pds = UforaCourseAlias(course_id=parallelle_en_gedistribueerde.course_id, alias="PDS")
parallel_and_distributed = UforaCourseAlias(
course_id=parallelle_en_gedistribueerde.course_id, alias="Parallel and Distributed Software Systems"
)
da = UforaCourseAlias(course_id=discrete_algoritmen.course_id, alias="DA")
discalg = UforaCourseAlias(course_id=discrete_algoritmen.course_id, alias="DiscAlg")
sel3 = UforaCourseAlias(course_id=software_engineering_lab_3.course_id, alias="SEL3")
selab3 = UforaCourseAlias(course_id=software_engineering_lab_3.course_id, alias="SELab3")
dv = UforaCourseAlias(course_id=datavisualisatie.course_id, alias="DV")
datavis = UforaCourseAlias(course_id=datavisualisatie.course_id, alias="DataVis")
recht = UforaCourseAlias(course_id=recht_van_intellectuele_eigendom.course_id, alias="Recht")
computer_graphics = UforaCourseAlias(course_id=computergrafiek.course_id, alias="Computer Graphics")
stage = UforaCourseAlias(course_id=bedrijfsstage.course_id, alias="Stage")
thesis = UforaCourseAlias(course_id=masterproef.course_id, alias="Thesis")
session.add_all(
[
fundamenten,
ml,
pds,
parallel_and_distributed,
da,
discalg,
sel3,
selab3,
dv,
datavis,
recht,
computer_graphics,
stage,
thesis,
]
)
await session.commit()
"""Elective master's courses"""
aanbevelingssystemen = UforaCourse(
course_id=635444,
code="E018230",
name="Aanbevelingssystemen",
year=6,
compulsory=False,
role_id=1023303206572404817,
)
algoritmische_grafentheorie = UforaCourse(
code="C000145", name="Algoritmische Grafentheorie", year=6, compulsory=False, role_id=1023304281094373436
)
artificial_intelligence = UforaCourse(
code="E016330", name="Artificial Intelligence", year=6, compulsory=False, role_id=1023304874789703741
)
berekenbaarheid_en_complexiteit = UforaCourse(
code="C000627",
name="Berekenbaarheid en Complexiteit",
year=6,
compulsory=False,
role_id=1023304692861784064,
)
capita_selecta_bio = UforaCourse(
code="C004122",
name="Capita Selecta in Bioinformatics",
year=6,
compulsory=False,
role_id=1023305177727504444,
)
causal_machine_learning = UforaCourse(
code="C004413", name="Causal Machine Learning", year=6, compulsory=False, role_id=1023304690491985961
)
computational_challenges = UforaCourse(
code="C003711",
name="Computational Challenges in Bioinformatics",
year=6,
compulsory=False,
role_id=1023304283413811411,
)
computeralgebra = UforaCourse(
code="C001026", name="Computeralgebra", year=6, compulsory=False, role_id=1023304697928495164
)
computervisie = UforaCourse(
code="E736020", name="Computervisie", year=6, compulsory=False, role_id=1023304274945511575
)
context_and_nuance = UforaCourse(
code="A005503",
name="Context and Nuance: A Critical Reflection on Current Topics",
year=6,
compulsory=False,
role_id=1023304898252648470,
)
deep_learning = UforaCourse(
code="F000918", name="Deep Learning", year=6, compulsory=False, role_id=1023304893672464474
)
economie = UforaCourse(code="F000758", name="Economie", year=6, compulsory=False, role_id=1023305174506291290)
geavanceerde_databanken = UforaCourse(
code="E018441", name="Geavanceerde Databanken", year=6, compulsory=False, role_id=1023304259913134130
)
gevorderde_numerieke_methoden = UforaCourse(
code="C004011", name="Gevorderde Numerieke Methoden", year=6, compulsory=False, role_id=1023304278410002482
)
gevorderd_wetenschappelijk_engels = UforaCourse(
code="A003107",
name="Gevorderd wetenschappelijk Engels",
year=6,
compulsory=False,
role_id=1023305170987257898,
)
internet_of_things = UforaCourse(
code="E019170", name="Internet of Things", year=6, compulsory=False, role_id=1023304695416111224
)
medical_imaging = UforaCourse(
code="E010371", name="Medical Imaging", year=6, compulsory=False, role_id=1023304900555317399
)
robotica = UforaCourse(
course_id=634368, code="E019370", name="Robotica", year=6, compulsory=False, role_id=1023303205360246904
)
software_hacking = UforaCourse(
course_id=635436,
code="E017941",
name="Softwarehacking en -Protectie",
year=6,
compulsory=False,
role_id=1023303203913211905,
)
spraakverwerking = UforaCourse(
code="E010220", name="Spraakverwerking", year=6, compulsory=False, role_id=1023304686704533576
)
sustainable_computing = UforaCourse(
code="E034500", name="Sustainable Computing", year=6, compulsory=False, role_id=1023304895421481081
)
session.add_all(
[
aanbevelingssystemen,
algoritmische_grafentheorie,
artificial_intelligence,
berekenbaarheid_en_complexiteit,
capita_selecta_bio,
causal_machine_learning,
computational_challenges,
computeralgebra,
computervisie,
context_and_nuance,
deep_learning,
economie,
geavanceerde_databanken,
gevorderde_numerieke_methoden,
gevorderd_wetenschappelijk_engels,
internet_of_things,
medical_imaging,
robotica,
software_hacking,
spraakverwerking,
sustainable_computing,
]
)
await session.commit()
recommender_systems = UforaCourseAlias(course_id=aanbevelingssystemen.course_id, alias="Recommender Systems")
cv = UforaCourseAlias(course_id=computervisie.course_id, alias="CV")
comp_vis = UforaCourseAlias(course_id=computervisie.course_id, alias="CompVis")
computer_vision = UforaCourseAlias(course_id=computervisie.course_id, alias="Computer Vision")
context = UforaCourseAlias(course_id=context_and_nuance.course_id, alias="Context")
eco = UforaCourseAlias(course_id=economie.course_id, alias="Eco")
advanced_databases = UforaCourseAlias(course_id=geavanceerde_databanken.course_id, alias="Advanced Databases")
advanced_academic_english = UforaCourseAlias(
course_id=gevorderd_wetenschappelijk_engels.course_id, alias="Advanced Academic English"
)
iot = UforaCourseAlias(course_id=internet_of_things.course_id, alias="IoT")
robotics = UforaCourseAlias(course_id=robotica.course_id, alias="Robotics")
software_hacking_en = UforaCourseAlias(
course_id=software_hacking.course_id, alias="Software Hacking and Protection"
)
speech_processing = UforaCourseAlias(course_id=spraakverwerking.course_id, alias="Speech Processing")
session.add_all(
[
recommender_systems,
cv,
comp_vis,
computer_vision,
context,
eco,
advanced_databases,
advanced_academic_english,
iot,
robotics,
software_hacking_en,
speech_processing,
]
)
await session.commit()

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