200 Commits
Author SHA1 Message Date
AllfatherHatt 5c018ab81b Merge pull request 'Fixed bug where the default unit was "m" instead of sending None' (#27) from default_unit_patch into master
Reviewed-on: #27
2026-08-31 20:53:48 +02:00
T-BENZIN d63d03aead Fixed bug where the default unit was "m" instead of sending None
When the log_activity function receives a value of None for the unit parameter, it logs value without applying a conversion multiplier.
2026-08-31 20:48:51 +02:00
T-BENZIN fffedeccbd Merge pull request 'Fixed typo in aliases key for tonnes' (#26) from tonnes-patch-1 into master
Reviewed-on: #26
2026-08-02 13:50:28 +02:00
T-BENZIN b7bf9494aa Fixed typo in aliases key for tonnes 2026-08-02 13:50:09 +02:00
T-BENZIN dbba4533c3 Merge pull request 'added tonnes, fixed ye-olde kilogramme spelling' (#25) from lift-tonnes into master
Reviewed-on: #25
2026-08-02 13:41:23 +02:00
T-BENZIN e7c0d8485d added tonnes, fixed ye-olde kilogramme spelling 2026-08-02 13:40:52 +02:00
T-BENZIN b5d292e6ca Merge pull request 'Added thread ID for Gainium challenge' (#24) from gainium-thread into master
Reviewed-on: #24
2026-08-02 13:28:13 +02:00
T-BENZIN e19a3b4378 Added thread ID for Gainium challenge
I regert hard-coding thread IDs.
2026-08-02 13:27:50 +02:00
T-BENZIN 5013fffa59 Merge pull request 'added lift.json for lifting activity' (#23) from lifting-activity into master
Reviewed-on: #23
2026-08-02 13:24:58 +02:00
T-BENZIN 69cedfc114 added lift.json for lifting activity 2026-08-02 13:21:04 +02:00
AllfatherHatt d1b6085ee8 Merge pull request '9month-revision-update2' (#22) from 9month-revision-update2 into master
Reviewed-on: #22
2026-03-16 13:22:06 +01:00
AllfatherHatt 78e9cc11a1 Merge branch 'master' into 9month-revision-update2 2026-03-16 13:21:19 +01:00
AllfatherHatt 18137c054b Updating feedback 2026-03-16 13:20:03 +01:00
AllfatherHatt 40c1f9dcb4 Merge pull request 'Attempt to adding live feedback' (#21) from 9month-revision-update2 into master
Reviewed-on: #21
2026-03-16 12:51:42 +01:00
AllfatherHatt 83dcdd9277 Attempt to adding live feedback 2026-03-16 12:50:53 +01:00
AllfatherHatt 0053d95c4d Merge pull request 'Fixing wrong parameters' (#20) from 9month-revision-update into master
Reviewed-on: #20
2026-03-16 12:37:30 +01:00
AllfatherHatt c8cd409834 Fixing wrong parameters 2026-03-16 12:36:36 +01:00
AllfatherHatt 7186c4ca32 Merge pull request '9month-revision-reginald' (#19) from 9month-revision-reginald into master
Reviewed-on: #19
2026-03-16 12:21:17 +01:00
AllfatherHatt c6bb3afa70 Revision 2026-03-16 12:16:29 +01:00
AllfatherHatt f8fc144d5d Merge branch 'master' of https://manufactorum.kanium.org/Kanium-PUBLIC/KaniumCogs 2026-03-16 12:14:11 +01:00
AllfatherHatt 8f0ece3570 Adding updated debug stuff 2026-03-16 12:14:01 +01:00
T-BENZIN 8e1245484f Merge pull request 'Added aliases for spelling metre/kilometre' (#18) from metre-patch into master
Reviewed-on: #18
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-08 05:51:46 +01:00
T-BENZIN 9df44d3742 Added aliases for spelling metre/kilometre 2026-02-08 05:46:02 +01:00
AllfatherHatt fa1be19767 Merge pull request 'Aligning activity logger' (#17) from debug_fitness_more into master
Reviewed-on: #17
2026-02-04 18:25:08 +01:00
AllfatherHatt f22f1d46d1 Merged 2026-02-04 18:23:41 +01:00
AllfatherHatt 7bcdae7b79 Aligning activity logger 2026-02-04 18:18:58 +01:00
AllfatherHatt d51bc4965e Merge pull request 'feature_fitness_logger' (#16) from feature_fitness_logger into master
Reviewed-on: #16
2026-02-04 18:01:02 +01:00
AllfatherHatt d7a6ca31d1 Merge branch 'master' into feature_fitness_logger 2026-02-04 17:55:54 +01:00
AllfatherHatt 6cf112be49 Merge pull request 'Attempting to be a bit more direct about activity path' (#15) from debug_fitness into master
Reviewed-on: #15
Reviewed-by: T-BENZIN <[email protected]>
2026-02-04 17:50:44 +01:00
T-BENZIN f20a4abc81 Original message is put in lower case whole and redundant .lower() methods are removed.
Modified usage message.
2026-02-04 21:42:25 +05:00
AllfatherHatt 175eeced0c Attempting to be a bit more direct about activity path 2026-02-04 14:02:16 +01:00
AllfatherHatt 2be9cc5795 Merge pull request 'Called correct function' (#14) from minor-typo-fix into master
Reviewed-on: #14
Reviewed-by: T-BENZIN <[email protected]>
2026-02-04 13:42:29 +01:00
AllfatherHatt 07a2bf7e38 Merge pull request 'Fixed wrong json key being pulled (I forgot to replace the old name)' (#13) from json-key-fix into master
Reviewed-on: #13
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-04 12:32:49 +01:00
AllfatherHatt 1ee949eae2 Called correct function 2026-02-04 12:29:22 +01:00
T-BENZIN 8bec9a159b Fixed wrong json key being pulled (I forgot to replace the old name) 2026-02-04 12:10:34 +01:00
AllfatherHatt ee77279d6e Merge pull request 'Fixed minor import issue' (#12) from minor-import-fix into master
Reviewed-on: #12
2026-02-04 11:46:39 +01:00
AllfatherHatt 1b67240976 Fixed minor import issue 2026-02-04 11:44:44 +01:00
AllfatherHatt 16a19e6913 Merge pull request 'Added cog for fitness activity logging feature.' (#11) from feature_fitness_logger into master
Reviewed-on: #11
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-04 11:14:40 +01:00
T-BENZIN 4ae03c9b9d Added cog for fitness activity logging feature.
At its starting state it would accept following commands:
- !getunits <activity>: Would send a JSON file with activity units via Discord message. Those files are responsible for setting up a fitness activity to log.
- !getlog <activity>: Would send a CSV file with fitness activity log. Those files contain records with timestamp, username, and reported activity measurements.
- !<activity> <value> <unit (optional)>: Would create a record with your fitness activity in corresponding CSV log file. If you choose to report different unit (feet, for example), it is going to convert as specified in its corresponding JSON file with units (to meters, for example) if the activity specified as convertable. Non-convertable activities' units are ignored.
2026-01-11 10:03:55 +05:00
T-BENZIN 074ff8bbb4 Merge pull request 'Made improvements in AI Agent's tool get_forecast_weather' (#8) from dev into master
Reviewed-on: #8
Reviewed-by: AllfatherHatt <[email protected]>
2025-06-18 17:13:22 +02:00
T-BENZIN 4e22274f9b - Removed unused debug import 2025-06-18 20:10:05 +05:00
T-BENZIN a5ed9f9e1c Applied changes made in GitHub 2025-06-17 10:57:19 +05:00
AllfatherHatt f35a18c84b Merge pull request 'development' (#1) from development into master
Reviewed-on: #1
2025-06-14 15:47:22 +02:00
AllfatherHatt 22767ad398 Added command aliases 2025-05-18 13:24:16 +02:00
AllfatherHatt 88ecf7911f Attempting to make set channel behavior be nice 2025-05-18 13:18:50 +02:00
AllfatherHatt 1a390c3d25 Attempting to make recruitment channel behave 2025-05-18 13:12:25 +02:00
AllfatherHatt 7a9313d5a5 Removing unsupported escape character 2025-05-18 13:07:11 +02:00
AllfatherHatt a5ef741ad7 wtf questions 2025-05-18 13:03:21 +02:00
AllfatherHatt 9a3e5860a7 Attempting to fix questions 2025-05-18 12:58:41 +02:00
AllfatherHatt 38414027ec Updated recruitment cog 2025-05-18 12:51:52 +02:00
AllfatherHatt 0483c2d8e6 Merge pull request #29 from T-BENZIN/development
+ Added openai_completion.py for later use as a external script for working with ChatGPT
2025-03-18 16:22:46 +01:00
T-BENZIN b93ae09b69 + Applied debug decorator to some functions and methods 2025-03-18 19:54:34 +05:00
T-BENZIN 881ef42f42 + Added debug decorator 2025-03-18 19:50:12 +05:00
T-BENZIN d35110a83b + Added openai_completion.py for later use as a external script for working with ChatGPT 2025-03-17 21:43:15 +05:00
AllfatherHatt ae866894a3 Trying to update messages 2025-03-16 14:25:10 +01:00
AllfatherHatt dc5bce9ca4 Re-adding max tokens 2025-03-16 14:14:56 +01:00
AllfatherHatt f22c77e2a1 Merge branch 'development' of https://github.com/Kanium/KaniumCogs into development 2025-03-16 14:11:00 +01:00
AllfatherHatt 1a34585d33 No max tokens, this is surely safe 2025-03-16 14:10:49 +01:00
AllfatherHatt 39039fe188 Merge pull request #28 from T-BENZIN/development
- Removed some condition check that some guy put in there
2025-03-16 14:00:45 +01:00
T-BENZIN 6e34f36b8b - Removed some condition check that some guy put in there 2025-03-16 17:58:51 +05:00
AllfatherHatt b843a4ef58 Adding debug stuff 2025-03-16 13:48:36 +01:00
AllfatherHatt 341a68c045 Trying to debug the weather, so many mayflies 2025-03-16 13:41:15 +01:00
AllfatherHatt ac14895381 I can spell, don't check 2025-03-16 13:33:02 +01:00
AllfatherHatt 62972399a6 STARES AT FORECAST - info 2025-03-16 13:30:48 +01:00
AllfatherHatt cc4e7ae528 No more naughty, all is fine 2025-03-16 13:22:48 +01:00
AllfatherHatt 931d1b7298 Doing naughty stuff, don't look 2025-03-16 13:18:11 +01:00
AllfatherHatt de39425587 Merge branch 'development' of https://github.com/Kanium/KaniumCogs into development 2025-03-16 13:09:24 +01:00
AllfatherHatt c5b7ea3419 moving api key init 2025-03-16 13:09:21 +01:00
AllfatherHatt ed1995d7f2 Merge pull request #27 from T-BENZIN/development
~ Changed time_now function to return string instead of datetime object
2025-03-16 13:05:24 +01:00
T-BENZIN 52113f93ae ~ Changed time_now function to return string instead of datetime object 2025-03-16 17:03:55 +05:00
AllfatherHatt 866b94bb1e Merge branch 'development' of https://github.com/Kanium/KaniumCogs into development 2025-03-16 12:57:21 +01:00
AllfatherHatt ea391f7104 Merge pull request #26 from T-BENZIN/development
- Removed tool call from tool role message
2025-03-16 12:45:12 +01:00
T-BENZIN e3c931650b - Removed tool call from tool role message 2025-03-16 16:41:14 +05:00
AllfatherHatt d6196e2ac2 Merge branch 'development' of https://github.com/Kanium/KaniumCogs into development 2025-03-16 12:40:49 +01:00
AllfatherHatt 7b04c6f82a COmmenting on stuff 2025-03-16 12:40:47 +01:00
AllfatherHatt d083039a75 Merge pull request #25 from T-BENZIN/development
~ Fixed the lack of tool calls in assistant message
2025-03-16 12:37:07 +01:00
T-BENZIN c1cfbfa02d ~ Fixed the lack of tool calls in assistant message 2025-03-16 16:26:22 +05:00
AllfatherHatt a25c9dd4bb Merge pull request #24 from T-BENZIN/development
~ Fixed the initial assistant response with tool calls not being appended
2025-03-16 12:23:08 +01:00
T-BENZIN d078b60f8f ~ Fixed the initial assistant response with tool calls not being appended 2025-03-16 16:19:18 +05:00
AllfatherHatt 2a206f6a23 Merge pull request #23 from T-BENZIN/development
Development
2025-03-16 12:05:29 +01:00
T-BENZIN 90b1640242 ~ Slightly changed tools_description.py to address change the name of the now function to time_now 2025-03-16 15:23:05 +05:00
T-BENZIN 661bad8409 + Added OpenAI function calling
+ Added weather tools for ChatGPT
2025-03-16 12:06:12 +05:00
AllfatherHatt 5e7bbafbc9 Reducing short term memory to 50 2025-03-15 19:32:44 +01:00
AllfatherHatt c289e1d323 Turning to AI in desperation 2025-03-15 19:05:28 +01:00
AllfatherHatt b8cea3f961 Come on, read config you stupid memory! 2025-03-15 18:32:12 +01:00
AllfatherHatt 728ce2aee4 AAAAAAAAAAA! Documentation why 2025-03-15 18:26:36 +01:00
AllfatherHatt 5645b88677 Stop crying about positional argument! 2025-03-15 18:19:11 +01:00
AllfatherHatt 3bf7d221a0 Screw properties, direct injection baby! 2025-03-15 18:12:57 +01:00
AllfatherHatt cdb5d20fa9 Moving parent init further down in calling order, this is dumb 2025-03-15 18:09:44 +01:00
AllfatherHatt f40227d0cd attempting to make config a property 2025-03-15 18:05:21 +01:00
AllfatherHatt 1f59db6f97 Absolutely differently, ahem 2025-03-15 17:58:35 +01:00
AllfatherHatt e49609e316 Trying to init in a different way 2025-03-15 17:58:25 +01:00
AllfatherHatt 4bd380a8a7 first attempt at separating all memory into its own module separate from core 2025-03-15 17:50:43 +01:00
AllfatherHatt 2f78408c77 Attempting to move out blacklist commands 2025-03-13 19:28:10 +01:00
AllfatherHatt 9f3e9d4ba0 Adding import inheritance 2025-03-13 16:42:27 +01:00
AllfatherHatt ddf1d883b6 Trying to move permissions into its own file 2025-03-13 16:31:37 +01:00
AllfatherHatt 5c49fc9024 Attempting to import locally 2025-03-11 15:51:13 +01:00
AllfatherHatt 8ea91a6f16 Attempting to refactor list_allowed_roles out to permissions.py 2025-03-11 15:46:55 +01:00
AllfatherHatt c7dec31d5f Merge pull request #22 from T-BENZIN/development
~ Changed how messages are split
2025-02-26 18:19:43 +01:00
T-BENZIN a744339812 ~ Changed how messages are split 2025-02-26 20:14:38 +05:00
AllfatherHatt 7663550108 Help me, ChatGPT, you are my only hope 2025-02-26 11:44:56 +01:00
AllfatherHatt 994ff11655 Attempting to simplify 2025-02-26 11:37:50 +01:00
AllfatherHatt 678a7af77f Trying to do quick fix 2025-02-26 11:32:58 +01:00
AllfatherHatt c38a25f610 copy ffxvhqvic past SPIT ting 2025-02-25 22:12:11 +01:00
AllfatherHatt c37269e9ea I hate copy pasting 2025-02-25 22:09:39 +01:00
AllfatherHatt 11cd70be93 fixing typo 2025-02-25 22:04:42 +01:00
AllfatherHatt 0c9145b792 NUKE FROM ORBIT! 2025-02-25 21:55:08 +01:00
AllfatherHatt a3a0aa1b9a I swear this is a bad idea 2025-02-25 21:49:12 +01:00
AllfatherHatt df8d7c8c32 Trying to fix storage of memory 2025-02-25 21:42:12 +01:00
AllfatherHatt 4dbee1d990 More fix 2025-02-25 21:24:55 +01:00
AllfatherHatt 6329d21a39 feh! 2025-02-25 21:08:24 +01:00
AllfatherHatt 39c39abad7 I'm storing stuff wrong 2025-02-25 20:37:52 +01:00
AllfatherHatt 05e1842000 trying to fix topic extraction 2025-02-24 18:46:19 +01:00
AllfatherHatt 541d42c558 First refactor-move 2025-02-24 18:17:58 +01:00
AllfatherHatt e8e4b7e471 Added cleanup in allow_role and cleaned output messages 2025-02-24 14:13:09 +01:00
AllfatherHatt db042ff834 removed duplicated functions, wtf 2025-02-24 11:57:19 +01:00
AllfatherHatt 5f454a0d58 I don't know anymore 2025-02-24 11:54:00 +01:00
AllfatherHatt 2e4970b6dd Adding more debug info 2025-02-24 11:40:19 +01:00
AllfatherHatt a3525a2d18 wtf 2025-02-24 11:37:28 +01:00
AllfatherHatt 4a21a3c8e0 Trying to fix allowed roles, again 2025-02-24 11:34:08 +01:00
AllfatherHatt f8f67e42bd Trying to fix allowed role access 2025-02-24 10:55:14 +01:00
AllfatherHatt 5daf40d22b Added access controls 2025-02-24 00:01:14 +01:00
AllfatherHatt cef4df1d88 dialing dynamic responses back 2025-02-23 22:55:21 +01:00
AllfatherHatt fd566f2649 attempting to add even better listening 2025-02-23 22:43:59 +01:00
AllfatherHatt f186276d1d attempting to add better listening 2025-02-23 22:37:36 +01:00
AllfatherHatt ec3d8a323f Adding more trigger words 2025-02-23 21:36:32 +01:00
AllfatherHatt 131febc63b Added detection of direction invocation 2025-02-23 21:25:57 +01:00
AllfatherHatt a2646eac14 Trying to fix indentation 2025-02-23 21:09:19 +01:00
AllfatherHatt 7f6372a09b add less stupid debug 2025-02-23 21:01:09 +01:00
AllfatherHatt 30d048e53f add stupid level debug 2025-02-23 20:59:41 +01:00
AllfatherHatt 316ff36b8f Adding debug 2025-02-23 20:56:33 +01:00
AllfatherHatt b4e4437171 Debugging listening 2025-02-23 20:34:58 +01:00
AllfatherHatt a7c0b90036 Attempting to give Reginald ears 2025-02-23 20:13:09 +01:00
AllfatherHatt 8a54890f56 Syntax error? 2025-02-23 12:17:55 +01:00
AllfatherHatt d699b537e3 Upping memory limit 2025-02-23 12:15:56 +01:00
AllfatherHatt 4337e7be10 Resetting file to before chess attempting 2025-02-22 02:02:26 +01:00
AllfatherHatt 72171e9ed0 trying to convert to dictionary 2025-02-22 01:57:54 +01:00
AllfatherHatt 0ac027bf1c Adding context, because why not 2025-02-22 01:53:33 +01:00
AllfatherHatt 9823284a31 Attempting to add fen memory 2025-02-22 01:47:08 +01:00
AllfatherHatt f680dcae17 attempting to add chess 2025-02-22 01:44:55 +01:00
AllfatherHatt e00dacdbd3 Trying to add graceful contradiction handling 2025-02-21 21:26:26 +01:00
AllfatherHatt e6fa97965c Trying to activate long term memory 2025-02-21 20:55:33 +01:00
AllfatherHatt 8312678355 Attempting to create weighted memory 2025-02-21 20:21:54 +01:00
AllfatherHatt bf61df30dd Updated summarize memory function 2025-02-21 19:12:22 +01:00
AllfatherHatt b6e6bd5bd7 Re-adding self 2025-02-21 18:39:22 +01:00
AllfatherHatt 7ea1f2c5e3 Added a hopefully better way of handling Discord constraints 2025-02-21 18:33:45 +01:00
AllfatherHatt cbcfc22951 Upping token count and handling Discord limitation 2025-02-21 13:28:04 +01:00
AllfatherHatt a2aab13fb8 Trying to optimize 2025-02-21 02:44:57 +01:00
AllfatherHatt 79d0d92a29 Trying to optimize summary 2025-02-21 02:30:23 +01:00
AllfatherHatt 385b971dc9 Upped the amount of tokens 2025-02-21 02:16:18 +01:00
AllfatherHatt a253ab1f6a Trying to do this 2025-02-21 02:07:26 +01:00
AllfatherHatt 89edcdace5 Adding summary debug stuff 2025-02-21 01:58:52 +01:00
AllfatherHatt 1b05c8311e Add 10 message min 2025-02-21 01:36:38 +01:00
AllfatherHatt a96f27e5fa Added more utility 2025-02-21 01:23:29 +01:00
AllfatherHatt b835e669ec Utility functions for mid-term summaries 2025-02-21 01:17:36 +01:00
AllfatherHatt aa2b38ad1c Fixing indentation 2025-02-21 01:00:36 +01:00
AllfatherHatt e80509ba9d Trying to properly detect message limit 2025-02-21 00:57:03 +01:00
AllfatherHatt 799cde4a7a reformatted reginald function 2025-02-21 00:41:25 +01:00
AllfatherHatt 2724f7cee6 Trying to trim memory properly 2025-02-21 00:38:08 +01:00
AllfatherHatt 814102e921 Trying to add memory retention 2025-02-21 00:30:35 +01:00
AllfatherHatt 8c93e45c2f Adding short term status display limit 2025-02-21 00:02:46 +01:00
AllfatherHatt d970df5a12 Added ability to adjust short-term memory limit 2025-02-21 00:00:42 +01:00
AllfatherHatt b8790f57b4 Trying to fix memory 2025-02-20 23:47:54 +01:00
AllfatherHatt 23a21e383e Trying to optimize mid-term memory 2025-02-20 23:16:08 +01:00
AllfatherHatt 18c1c7f9e6 Added ability for Reginald and reginald 2025-02-20 22:45:36 +01:00
AllfatherHatt ea538293ac Adding a bit of spine and self-respect 2025-02-20 22:34:56 +01:00
AllfatherHatt fe749e9a18 ChatGPT utility functions gooo! 2025-02-20 22:05:51 +01:00
AllfatherHatt 2d70e02a15 Adding honesty 2025-02-20 22:02:07 +01:00
AllfatherHatt aaad4a6ead Trying to strip out Reginald: deliberately 2025-02-20 21:55:25 +01:00
AllfatherHatt 2c897fbc9f Trying to make him stop saying Reginald: 2025-02-20 21:48:44 +01:00
AllfatherHatt 0019a6c529 attmepting to add memory 2025-02-20 21:34:57 +01:00
AllfatherHatt 4e64b77b91 Trying to parse mentions 2025-02-20 20:27:26 +01:00
AllfatherHatt dabee89145 Trying to create channel memory again 2025-02-20 20:20:53 +01:00
AllfatherHatt ba10dc76a2 Updating to chat completion v.0+ 2025-02-20 20:07:27 +01:00
AllfatherHatt 501c2927fa Added missing import 2025-02-20 20:00:01 +01:00
AllfatherHatt 57446da207 Added extra error handling 2025-02-20 19:57:36 +01:00
AllfatherHatt 4ffc52e250 Added function back to setting api key 2025-02-20 19:38:49 +01:00
AllfatherHatt 76df917f76 Fixing import issue 2025-02-20 19:28:45 +01:00
AllfatherHatt 72e9a0135f Adjusting 2025-02-20 19:23:15 +01:00
AllfatherHatt e9749a680b Rolling the ChatGPT dice again! 2025-02-20 17:00:43 +01:00
AllfatherHatt b2fd5f4359 Rolling the ChatGPT dice 2025-02-20 16:52:54 +01:00
AllfatherHatt d20604d00f Am dumb, deleted too much 2025-02-20 16:17:36 +01:00
AllfatherHatt a1f51f3788 This optimization surely can't go wrong...surely 2025-02-20 16:04:44 +01:00
AllfatherHatt 4639877767 Attempting to fix missing join messages 2025-02-20 15:39:16 +01:00
AllfatherHatt 1c00b81d9a Attempting to switch to the new gpt-4o-mini model 2024-07-19 12:33:45 +02:00
AllfatherHatt 55319ef1fb attempting to fix ctx problem 2024-05-30 21:20:09 +02:00
AllfatherHatt 3161433f06 Attempting to optimize implementation 2024-05-30 21:10:05 +02:00
AllfatherHatt 5273180d49 Attempting to add detailed responses back in 2024-05-30 20:49:55 +02:00
AllfatherHatt 5d07c5848b Attempting to re-introduce memory to Reginald, manually 2024-05-30 20:39:11 +02:00
AllfatherHatt de74130674 Reverting to original implementation, how awful 2024-05-30 20:17:09 +02:00
AllfatherHatt 73468b13a5 Undoing the weirdness of the system prompt 2024-05-30 20:09:46 +02:00
AllfatherHatt 63fa40589f Re-added the prompt 2024-05-30 19:57:23 +02:00
AllfatherHatt 5c9113a27e Trying to use ChatGPT to fix our memory issue, crossing fingers 2024-05-30 19:50:25 +02:00
AllfatherHatt 4f84c3b89c I guess we are going asynchronous 2024-04-10 16:50:32 +02:00
AllfatherHatt 7e4de04949 I definitely didn't forget an import somewhere else 2024-04-10 16:39:29 +02:00
AllfatherHatt 235d8790b4 No really, fixing my imports 2024-04-10 16:34:50 +02:00
AllfatherHatt 0c8b77d553 Fixed imports 2024-04-10 16:31:19 +02:00
AllfatherHatt e423665cc2 Prototyping 2024-04-10 16:26:05 +02:00
AllfatherHatt 7d6b9c9403 Trying a new prompt for Reginald 2024-04-10 13:54:59 +02:00
AllfatherHatt 1d04160d0a Attempting to fix application timeout so its pr user 2023-11-22 20:53:09 +01:00
18 changed files with 1842 additions and 390 deletions
+1
View File
@@ -2,6 +2,7 @@
cogs made for our Royal butt. cogs made for our Royal butt.
## Our Cogs: ## Our Cogs:
- [FitnessCog](./fitnessCog)
- [RecruitmentCog](./recruitmentCog/) - [RecruitmentCog](./recruitmentCog/)
- [ReginaldCog](./reginaldCog/) - [ReginaldCog](./reginaldCog/)
- [TrafficCog](./trafficCog) - [TrafficCog](./trafficCog)
+6
View File
@@ -0,0 +1,6 @@
from redbot.core.bot import Red
from .fitness import FitnessCog
async def setup(bot: Red):
cog = FitnessCog(bot)
await bot.add_cog(cog)
+52
View File
@@ -0,0 +1,52 @@
{
"convertable": true,
"units": {
"meter": {
"factor": 1,
"aliases": [
"meter",
"meters",
"metre",
"metres",
"m",
"m."
]
},
"kilometer": {
"factor": 1000,
"aliases": [
"kilometer",
"kilometers",
"kilometre",
"kilometres",
"km",
"km."
]
},
"foot": {
"factor": 0.3048,
"aliases": [
"foot",
"feet",
"ft",
"ft."
]
},
"mile": {
"factor": 1609.344,
"aliases": [
"mile",
"miles",
"mi",
"mi."
]
},
"step": {
"factor": 0.18,
"aliases": [
"step",
"steps"
]
}
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"convertable": true,
"units": {
"kilogram": {
"factor": 1,
"aliases": [
"kg",
"kgs",
"kilogram",
"kilograms",
"kilogramme",
"kilogrammes",
"kg.",
"kgs."
]
},
"pound": {
"factor": 0.45,
"aliases": [
"lb",
"lb.",
"lbs",
"lbs.",
"pound",
"pounds"
]
},
"tonne": {
"factor": 1000,
"aliases": [
"t",
"t.",
"tonne",
"tonnes"
]
}
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"convertable": false
}
+105
View File
@@ -0,0 +1,105 @@
import csv
import json
from pathlib import Path
from typing import Optional
def activity_log_path(activity: str, logs_dir: Optional[Path] = None) -> Path:
"""
Returns the path of the activity log file in .csv format.
"""
logs_dir = logs_dir or (Path.cwd() / "activity_logs")
return logs_dir / f"{activity}.csv"
def activity_units_path(activity: str, units_dir: Optional[Path] = None) -> Path:
"""
Returns the path of the activity units file in .json format.
"""
units_dir = units_dir or (Path.cwd() / "activities")
return units_dir / f"{activity}.json"
def is_convertable(activity: str, units_dir: Optional[Path] = None) -> bool:
filename = activity_units_path(activity, units_dir)
with filename.open("r", encoding="utf-8") as f:
raw_data = json.load(f)
return bool(raw_data.get("convertable"))
def create_activity_log_file(
activity: str,
*,
units_dir: Optional[Path] = None,
logs_dir: Optional[Path] = None,
) -> None:
if not activity_units_path(activity, units_dir).exists():
raise ValueError(f"{activity} is not a valid activity")
logs_dir = logs_dir or (Path.cwd() / "activity_logs")
logs_dir.mkdir(parents=True, exist_ok=True)
filename = activity_log_path(activity, logs_dir)
if filename.exists():
return # don't re-add headers
with filename.open("w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "username", "value"])
def convert_units(
activity: str,
value: int | float,
unit: str | None,
*,
units_dir: Optional[Path] = None,
) -> float:
filename = activity_units_path(activity, units_dir)
if not filename.exists():
raise ValueError(f"{activity} is not a valid activity")
# Consider None unit value as factor of 1
if unit is None:
return float(value)
with filename.open("r", encoding="utf-8") as f:
raw_data = json.load(f)
units_data = raw_data.get("units") or {}
unit_l = unit.lower()
for _, data in units_data.items():
aliases = [a.lower() for a in (data.get("aliases") or [])]
if unit_l in aliases:
return float(value) * float(data["factor"])
raise ValueError(f"{unit} is not a valid unit")
def log_activity(
timestamp: int,
username: str,
activity: str,
value: int | float,
unit: str | None,
*,
units_dir: Optional[Path] = None,
logs_dir: Optional[Path] = None,
) -> None:
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"{value} is not a valid value")
filename = activity_log_path(activity, logs_dir)
if not filename.exists():
create_activity_log_file(activity, units_dir=units_dir, logs_dir=logs_dir)
if is_convertable(activity, units_dir):
converted_value = round(convert_units(activity, value, unit, units_dir=units_dir), 2)
else:
converted_value = round(float(value), 2)
with filename.open("a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([timestamp, username, converted_value])
+143
View File
@@ -0,0 +1,143 @@
import os
from pathlib import Path
import discord
from redbot.core import commands
from redbot.core.data_manager import cog_data_path
from .activity_logger import log_activity
class FitnessCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
default_threads = "1457402451530350727,1328363409648910356,1533433244186443827"
threads_raw = os.getenv("THREADS_ID", default_threads)
self.threads_id: list[int] = []
for x in threads_raw.split(","):
x = x.strip()
if x.isdigit():
self.threads_id.append(int(x))
self.confirm_reactions = os.getenv("CONFIRMATION_REACTIONS", "🐈💨")
# Units are shipped with the cog in: fitnessCog/activities/*.json
self.units_dir: Path = Path(__file__).parent / "activities"
# Logs are writable and belong in Red's data dir
self.logs_dir: Path = cog_data_path(self) / "activity_logs"
self.logs_dir.mkdir(parents=True, exist_ok=True)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author.bot:
return
if message.channel.id not in self.threads_id:
return
content = (message.content or "").strip()
if not content.startswith("!"):
return
cmdline = content[1:].strip().lower()
if not cmdline:
return
cmd, *rest = cmdline.split(" ", 1)
arg = rest[0].strip() if rest else ""
if cmd == "getunits":
await self.get_units_file(message, arg)
return
if cmd == "getlog":
await self.get_log_file(message, arg)
return
await self.log_fitness(message)
async def get_units_file(self, message: discord.Message, activity: str):
try:
if not activity:
await message.channel.send(
"Usage: `!getunits <activity>` (example: `!getunits pushups`)"
)
return
activity = activity.lower().strip()
path = self.units_dir / f"{activity}.json"
if not path.exists():
await message.channel.send(f"No units JSON found for `{activity}`.")
return
await message.channel.send(file=discord.File(fp=str(path)))
except Exception as e:
await message.channel.send(str(e))
async def get_log_file(self, message: discord.Message, activity: str):
try:
if not activity:
await message.channel.send(
"Usage: `!getlog <activity>` (example: `!getlog pushups`)"
)
return
activity = activity.lower().strip()
path = self.logs_dir / f"{activity}.csv"
if not path.exists():
await message.channel.send(f"No log CSV found for `{activity}`.")
return
await message.channel.send(file=discord.File(fp=str(path)))
except Exception as e:
await message.channel.send(str(e))
async def log_fitness(self, message: discord.Message):
try:
parts = message.content[1:].strip().split()
if len(parts) < 2:
await message.reply(
"Usage: `!<activity> <value> [unit]` (example: `!pushups 20` or `!run 5 km`)"
)
return
activity = parts[0]
try:
value = float(parts[1])
except ValueError:
await message.reply(
"Invalid number. Usage: `!<activity> <value> [unit]` (example: `!run 5 km`)"
)
return
unit = parts[2] if len(parts) > 2 else None
timestamp = int(message.created_at.timestamp())
username = message.author.name
log_activity(
timestamp,
username,
activity,
value,
unit,
units_dir=self.units_dir,
logs_dir=self.logs_dir,
)
for r in self.confirm_reactions:
await message.add_reaction(r)
except Exception as e:
await message.reply(str(e))
def setup(bot):
bot.add_cog(FitnessCog(bot))
+287 -179
View File
@@ -1,226 +1,334 @@
import asyncio import asyncio
import datetime import datetime
import re import re
from typing import List from typing import Dict, List, Optional
from datetime import timedelta
import discord import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils.antispam import AntiSpam from redbot.core.utils.antispam import AntiSpam
# Define your application questions and field settings\
QUESTIONS_LIST = [
{
"key": "name",
"prompt": "First of all, what is your name?",
"field_name": "Name",
"inline": True,
},
{
"key": "age",
"prompt": "What age are you?",
"field_name": "Age",
"inline": True,
},
{
"key": "country",
"prompt": "Where are you from?",
"field_name": "Country",
"inline": True,
},
{
"key": "hobbies",
"prompt": "Do you have any hobbies?",
"field_name": "Hobbies",
"inline": True,
},
{
"key": "game",
"prompt": "Are you wishing to join because of a particular game? If so, which game?",
"field_name": "Specific game?",
"inline": True,
},
{
"key": "motivation",
"prompt": "Write out, in a free-style way, what your motivation is for wanting to join us in particular and how you would be a good fit for Kanium",
"field_name": "Motivation for wanting to join:",
"inline": False,
},
]
def sanitize_input(input_text: str) -> str: def sanitize_input(input_text: str) -> str:
"""Sanitize input to remove mentions, links, and special characters.""" """Sanitize input to remove mentions, links, and unwanted special characters."""
sanitized_text = re.sub(r'<@!?&?(\d+)>', '', input_text) # Remove user/role/channel mentions
sanitized_text = re.sub(r'http\S+', '', sanitized_text) text = re.sub(r'<@!?(?:&)?\d+>', '', input_text)
sanitized_text = re.sub(r'([^\w\s.,?!`~@#$%^&*()_+=-])', '', sanitized_text) # Remove URLs
return sanitized_text text = re.sub(r'http\S+', '', text)
# Keep only word characters (including Unicode letters/digits), whitespace, and your chosen punctuation
text = re.sub(r"[^\w\s\.,\?!`~@#$%^&*()_+=-]", "", text)
return text
class Recruitment(commands.Cog):
class Recruitment(commands.Cog): # noqa
"""A cog that lets a user send a membership application.""" """A cog that lets a user send a membership application."""
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.message: str = '' self.config = Config.get_conf(self, identifier=0xFAB123ABC456)
self.config = Config.get_conf(self, identifier=101101101101001110101) # Replace with your own unique identifier default_guild = {"application_channel_id": None}
default_guild = {"guild_id": 274657393936302080, "application_channel_id": None}
self.config.register_guild(**default_guild) self.config.register_guild(**default_guild)
self.antispam = {} # antispam store per guild per user
self.antispam: Dict[int, Dict[int, AntiSpam]] = {}
self.cog_check_enabled = True
async def cog_check(self, ctx: commands.Context): async def cog_check(self, ctx: commands.Context) -> bool:
if await ctx.bot.is_admin(ctx.author): if (
await ctx.bot.is_admin(ctx.author)
or not self.cog_check_enabled
or (ctx.guild and ctx.author.guild_permissions.manage_guild)
or (ctx.guild and ctx.author.guild_permissions.administrator)
):
return True return True
guild_id = ctx.guild.id gid = ctx.guild.id
if guild_id not in self.antispam: uid = ctx.author.id
self.antispam[guild_id] = AntiSpam([(datetime.timedelta(hours=1), 1)]) if gid not in self.antispam:
antispam = self.antispam[guild_id] self.antispam[gid] = {}
if uid not in self.antispam[gid]:
if antispam.spammy: self.antispam[gid][uid] = AntiSpam([(datetime.timedelta(hours=1), 1)])
try: spam = self.antispam[gid][uid]
await ctx.message.delete(delay=0) if spam.spammy:
except discord.Forbidden:
pass
await ctx.author.send("Please wait for an hour before sending another application.")
return False
antispam.stamp()
return True
@commands.guild_only()
@checks.admin_or_permissions(manage_guild=True)
@commands.group(name="setapplicationschannel", pass_context=True, no_pm=True)
async def setapplicationschannel(self, ctx: commands.Context):
"""Set the channel where applications will be sent."""
if ctx.invoked_subcommand is None:
guild = ctx.guild
channel = ctx.channel
await self.config.guild(guild).guild_id.set(guild.id)
await self.config.guild(guild).application_channel_id.set(channel.id)
await ctx.send(f"Application channel set to {channel.mention}.")
@setapplicationschannel.command(name="clear")
async def clear_application_channel(self, ctx: commands.Context):
"""Clear the current application channel."""
guild = ctx.guild
await self.config.guild(guild).clear_raw("application_channel_id")
await ctx.send("Application channel cleared.")
@commands.group(name="application", usage="[text]", invoke_without_command=True)
async def application(self, ctx: commands.Context, *, _application: str = ""):
# Input validation and sanitization for _application
_application = sanitize_input(_application)
if len(_application) > 2000:
await ctx.send("Your application is too long. Please limit it to 2000 characters.")
return
guild_id = await self.get_guild_id(ctx)
guild = discord.utils.get(self.bot.guilds, id=guild_id)
if guild is None:
await ctx.send(f"The guild with ID {guild_id} could not be found.")
return
author = ctx.author
if author.guild != guild:
await ctx.send(f"You need to be in the {guild.name} server to submit an application.")
return
if await self.check_author_is_member_and_channel_is_dm(ctx):
await self.interactive_application(author)
async def get_guild_id(self, ctx: commands.Context) -> int:
guild_id = await self.config.guild(ctx.author.guild).guild_id()
return guild_id
async def check_author_is_member_and_channel_is_dm(self, ctx: commands.Context) -> bool:
if not isinstance(ctx.author, discord.Member):
await ctx.send("You need to join the server before your application can be processed.")
return False
if not isinstance(ctx.channel, discord.DMChannel):
try: try:
await ctx.message.delete() await ctx.message.delete()
except: except discord.Forbidden:
pass
try:
await ctx.author.send("Please wait for an hour before sending another application.")
except discord.Forbidden:
pass pass
await self.interactive_application(ctx)
return False return False
spam.stamp()
return True return True
@commands.command(name="togglecogcheck")
@checks.is_owner()
async def toggle_cog_check(self, ctx: commands.Context) -> None:
"""Toggle the cog_check functionality on or off."""
self.cog_check_enabled = not self.cog_check_enabled
await ctx.send(f"Cog checks are now {'enabled' if self.cog_check_enabled else 'disabled'}.")
async def interactive_application(self, ctx: commands.Context): @commands.group(name="setapplicationschannel", invoke_without_command=True)
"""Ask the user several questions to create an application.""" @checks.admin_or_permissions(manage_guild=True)
author = ctx.author async def set_applications_channel(self, ctx: commands.Context, channel: discord.TextChannel = None):
embed = discord.Embed( """Set the channel where applications will be sent."""
channel = channel or ctx.channel
await self.config.guild(ctx.guild).application_channel_id.set(channel.id)
await ctx.send(f"Application channel set to {channel.mention}.")
@set_applications_channel.command(name="clear")
async def clear_applications_channel(self, ctx: commands.Context):
"""Clear the application channel."""
await self.config.guild(ctx.guild).clear_raw("application_channel_id")
await ctx.send("Application channel cleared.")
@commands.group(name="application", aliases=["joinus", "joinkanium", "applyformembership"], invoke_without_command=True)
async def application(self, ctx: commands.Context, *, text: Optional[str] = None) -> None:
"""Start an application process."""
# Direct free-text application (optional)
if text:
text = sanitize_input(text)
if len(text) > 2000:
await ctx.send("Your application is too long (max 2000 chars).")
return
# Determine if in guild or DM
if isinstance(ctx.channel, discord.DMChannel):
await self._start_application(ctx.author)
else:
try:
await ctx.message.delete()
except discord.Forbidden:
pass
try:
await ctx.author.send(
"Let's move this to DM so we can process your application."
)
except discord.Forbidden:
await ctx.send("I cannot DM you. Please enable DMs and try again.")
return
await self._start_application(ctx.author)
async def _start_application(self, member: discord.Member) -> None:
# Kick off interactive questions
if not await self._send_embed(
member,
title="+++ KANIUM APPLICATION SYSTEM +++", title="+++ KANIUM APPLICATION SYSTEM +++",
description="Ah, you wish to apply for Kanium membership. Very well, understand that This process is very important to us so we expect you to put effort in and be glorious about it. Let us begin!", description=(
"This is the process to join Kanium."
" Please take your time and answer thoughtfully."
),
color=discord.Color.green(), color=discord.Color.green(),
) ):
await author.send(embed=embed) return
answers = await self.ask_questions(author)
answers = await self.ask_questions(member)
if not answers: if not answers:
return return
embed = self.format_application(answers, member)
await self.send_application(member, embed)
embeddedApplication = await self.format_application(answers, author) async def _send_embed(
self,
# Call the sendApplication to check if the author is a member of the guild and send the application if they are member: discord.Member,
await self.sendApplication(author, embeddedApplication) *,
title: str,
description: str,
async def sendApplication(self, author: discord.Member, embeddedApplication: discord.Embed): color: discord.Color,
# Check if the author is a member of the guild ) -> bool:
guild = author.guild embed = discord.Embed(title=title, description=description, color=color)
member = guild.get_member(author.id)
if member is None:
await author.send("You need to join the server before your application can be processed.")
return
# Send the embed to the application channel
application_channel_id = await self.config.guild(guild).application_channel_id()
if not application_channel_id:
await author.send("The application channel has not been set. Please use the `setapplicationschannel` command to set it.")
return
application_channel = guild.get_channel(application_channel_id)
if application_channel is None:
await author.send(f"The application channel with ID {application_channel_id} could not be found.")
return
try: try:
message = await application_channel.send(embed=embeddedApplication) await member.send(embed=embed)
return True
except discord.Forbidden: except discord.Forbidden:
await author.send("I do not have permission to send messages in the application channel.") return False
return
# Add reactions to the message async def ask_questions(self, member: discord.Member) -> Optional[Dict[str, str]]:
try: """
await self.add_reactions(message) Ask each question, let the user confirm their answer,
except discord.Forbidden: and return the final answers (or None on abort).
await author.send("I do not have permission to add reactions to messages in the application channel.") """
return answers: Dict[str, str] = {}
# Assign the Trial role to the author for q in QUESTIONS_LIST:
role = guild.get_role(531181363420987423) prompt = q["prompt"]
try:
await member.add_roles(role)
except discord.Forbidden:
await author.send("I do not have permission to assign roles.")
return
await author.send("Thank you for submitting your application! You have been given the 'Trial' role.") while True:
# 1) send the question
try:
await member.send(prompt)
except discord.Forbidden:
return None # can't DM
# 2) wait for their reply (match on ID, not object)
try:
msg = await asyncio.wait_for(
self.bot.wait_for(
"message",
check=lambda m: (
m.author.id == member.id
and isinstance(m.channel, discord.DMChannel)
)
),
timeout=300.0,
)
except asyncio.TimeoutError:
await member.send(
"You took too long to answer. Please restart the application with `!application`."
)
return None
async def add_reactions(self, message: discord.Message): answer = sanitize_input(msg.content)
reactions = ["✅", "❌", "❓"]
for reaction in reactions:
await message.add_reaction(reaction)
async def format_application(self, answers: List[str], author: discord.Member) -> discord.Embed: # 3) echo back for confirmation
"""Format the application answers into an embed.""" try:
application_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") await member.send(f"You answered:\n> {answer}\n\nIs that correct? (yes/no)")
trial_end_date = (datetime.datetime.now() + datetime.timedelta(days=30)).strftime("%Y-%m-%d") except discord.Forbidden:
return None
embed = discord.Embed(title=f"Application from {author.name}#{author.discriminator}", color=discord.Color.green()) # 4) wait for a yes/no
embed.set_thumbnail(url=author.avatar.url) try:
embed.add_field(name="Name", value=answers[0]) confirm = await asyncio.wait_for(
embed.add_field(name="Age", value=answers[1]) self.bot.wait_for(
embed.add_field(name="Country", value=answers[2]) "message",
embed.add_field(name="Hobbies", value=answers[3]) check=lambda m: (
embed.add_field(name="Specific game?", value=answers[4]) m.author.id == member.id
embed.add_field(name="\u200b", value="\u200b") # Empty field for spacing and isinstance(m.channel, discord.DMChannel)
embed.add_field(name="Motivation for wanting to join:", value=answers[5], inline=False) and m.content.lower() in ("y", "yes", "n", "no")
embed.set_footer(text=f"Application received: {application_date}, Trial ends: {trial_end_date}") )
),
timeout=60.0,
)
except asyncio.TimeoutError:
await member.send(
"Confirmation timed out. Please restart the application with `!application`."
)
return None
return embed if confirm.content.lower() in ("y", "yes"):
# user confirmed, save and move on
async def ask_questions(self, author: discord.Member) -> List[str]: answers[q["key"]] = answer
"""Ask the user several questions and return the answers.""" break
questions = [ else:
"First of all, what is your name?", # user said “no” → repeat this question
"What age are you?", await member.send("Okay, let's try that again.")
"Where are you from?",
"Do you have any hobbies?",
"Are you wishing to join because of a particular game? If so, which game?",
"Write out, in a free-style way, what your motivation is for wanting to join us in particular and how you would be a good fit for Kanium",
]
answers = []
for question in questions:
await author.send(question)
try:
answer = await asyncio.wait_for(self.get_answers(author), timeout=300.0)
answers.append(answer.content)
except asyncio.TimeoutError:
await author.send("You took too long to answer. Please start over by using the application command again.")
return []
return answers return answers
async def get_answers(self, author: discord.Member) -> discord.Message:
"""Wait for the user to send a message.""" def format_application(
return await self.bot.wait_for("message", check=lambda m: m.author == author and isinstance(m.channel, discord.DMChannel)) self, answers: Dict[str, str], member: discord.Member
) -> discord.Embed:
now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
trial_ends = (
datetime.datetime.utcnow() + datetime.timedelta(days=30)
).strftime("%Y-%m-%d UTC")
embed = discord.Embed(
title=f"Application from {member}", color=discord.Color.green()
)
embed.set_thumbnail(url=member.avatar.url)
for q in QUESTIONS_LIST:
embed.add_field(
name=q["field_name"],
value=answers[q["key"]],
inline=q["inline"],
)
embed.set_footer(text=f"Received: {now} | Trial ends: {trial_ends}")
return embed
async def send_application(
self, member: discord.Member, embed: discord.Embed
) -> None:
guild = member.guild
if guild is None:
try:
await member.send("You need to be in the server to apply.")
except discord.Forbidden:
pass
return
channel_id = await self.config.guild(guild).application_channel_id()
if not channel_id:
try:
await member.send(
"Application channel not set. Ask an admin to run `setapplicationschannel`."
)
except discord.Forbidden:
pass
return
channel = guild.get_channel(channel_id)
if not channel:
try:
await member.send("Application channel was not found. Please ask an admin to re-set it.")
except discord.Forbidden:
pass
return
try:
sent = await channel.send(embed=embed)
await self.add_reactions(sent)
except discord.Forbidden:
try:
await member.send("I cannot post or react in the application channel.")
except discord.Forbidden:
pass
return
# Assign Trial role
role = guild.get_role(531181363420987423)
if role:
try:
await member.add_roles(role)
await member.send("Thank you! You've been granted the Trial role.")
except discord.Forbidden:
try:
await member.send("I lack permissions to assign roles.")
except discord.Forbidden:
pass
@staticmethod
async def add_reactions(message: discord.Message) -> None:
for emoji in ("✅", "❌", "❓"):
await message.add_reaction(emoji)
+44
View File
@@ -0,0 +1,44 @@
from redbot.core import commands
import discord
class BlacklistMixin:
"""Handles user blacklisting for Reginald."""
async def is_blacklisted(self, user: discord.Member) -> bool:
"""Checks if a user is blacklisted from interacting with Reginald."""
blacklisted_users = await self.config.guild(user.guild).blacklisted_users()
return str(user.id) in blacklisted_users
@commands.command(name="reginald_blacklist", help="List users who are explicitly denied access to Reginald.")
@commands.has_permissions(administrator=True)
async def list_blacklisted_users(self, ctx):
"""Lists all users currently blacklisted from interacting with Reginald."""
blacklisted_users = await self.config.guild(ctx.guild).blacklisted_users()
if not blacklisted_users:
await ctx.send("✅ No users are currently blacklisted from interacting with Reginald.")
return
user_mentions = [f"<@{user_id}>" for user_id in blacklisted_users]
await ctx.send(f"🚫 **Blacklisted Users:**\n{', '.join(user_mentions)}")
@commands.command(name="reginald_blacklist_add", help="Blacklist a user from interacting with Reginald.")
@commands.has_permissions(administrator=True)
async def add_to_blacklist(self, ctx, user: discord.User):
"""Adds a user to Reginald's blacklist."""
async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users:
if str(user.id) not in blacklisted_users:
blacklisted_users.append(str(user.id))
await ctx.send(f"🚫 {user.display_name} has been **blacklisted** from interacting with Reginald.")
else:
await ctx.send(f"⚠️ {user.display_name} is already blacklisted.")
@commands.command(name="reginald_blacklist_remove", help="Remove a user from Reginald's blacklist.")
@commands.has_permissions(administrator=True)
async def remove_from_blacklist(self, ctx, user: discord.User):
"""Removes a user from Reginald's blacklist."""
async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users:
if str(user.id) in blacklisted_users:
blacklisted_users.remove(str(user.id))
await ctx.send(f"✅ {user.display_name} has been removed from the blacklist.")
else:
await ctx.send(f"⚠️ {user.display_name} was not on the blacklist.")
+24
View File
@@ -0,0 +1,24 @@
import functools
import inspect
def debug(func):
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def wrap(*args, **kwargs):
print(f"DEBUG: Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = await func(*args, **kwargs)
print(f"DEBUG: {func.__name__} returned: {result}")
return result
return wrap
@functools.wraps(func)
def wrap(*args, **kwargs):
print(f"DEBUG: Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"DEBUG: {func.__name__} returned: {result}")
return result
return wrap
+322
View File
@@ -0,0 +1,322 @@
import datetime
import random
import re
from collections import Counter
import discord
import openai
from openai import OpenAIError
from redbot.core import commands
class MemoryMixin:
"""Handles all memory-related functions for Reginald."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.short_term_memory_limit = 50
self.summary_retention_limit = 25
self.summary_retention_ratio = 0.8
@commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.")
@commands.has_permissions(administrator=True)
async def clear_short_memory(self, ctx):
async with self.config.guild(ctx.guild).short_term_memory() as short_memory:
short_memory[str(ctx.channel.id)] = []
await ctx.send("Short-term memory for this channel has been cleared.")
@commands.command(name="reginald_set_limit", help="Set the short-term memory message limit.")
@commands.has_permissions(administrator=True)
async def set_short_term_memory_limit(self, ctx, limit: int):
if limit < 5:
await ctx.send("The short-term memory limit must be at least 5.")
return
self.short_term_memory_limit = limit
await ctx.send(f"Short-term memory limit set to {limit} messages.")
@commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.")
async def get_short_term_memory_limit(self, ctx):
await ctx.send(f"Current short-term memory limit: {self.short_term_memory_limit} messages.")
@commands.command(name="reginald_clear_mid", help="Clears mid-term memory (summarized logs).")
@commands.has_permissions(administrator=True)
async def clear_mid_memory(self, ctx):
async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory:
mid_memory[str(ctx.channel.id)] = []
await ctx.send("Mid-term memory for this channel has been cleared.")
@commands.command(name="reginald_summary", help="Displays a selected mid-term summary for this channel.")
async def get_mid_term_summary(self, ctx, index: int):
async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory:
summaries = mid_memory.get(str(ctx.channel.id), [])
if not isinstance(summaries, list):
summaries = []
if not summaries:
await ctx.send("No summaries available for this channel.")
return
if index < 1 or index > len(summaries):
await ctx.send(f"Invalid index. Please provide a number between 1 and {len(summaries)}.")
return
selected_summary = summaries[index - 1]
formatted_summary = (
f"Summary {index} of {len(summaries)}\n"
f"Date: {selected_summary.get('timestamp', 'Unknown')}\n"
f"Topics: {', '.join(selected_summary.get('topics', [])) or 'None'}\n"
f"Summary:\n\n{selected_summary.get('summary', '')}"
)
await self.send_long_message(ctx, formatted_summary)
@commands.command(name="reginald_summaries", help="Lists available summaries for this channel.")
async def list_mid_term_summaries(self, ctx):
async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory:
summaries = mid_memory.get(str(ctx.channel.id), [])
if not isinstance(summaries, list):
summaries = []
if not summaries:
await ctx.send("No summaries available for this channel.")
return
summary_list = "\n".join(
f"{i + 1}. {entry.get('timestamp', 'Unknown')} | Topics: {', '.join(entry.get('topics', [])) or 'None'}"
for i, entry in enumerate(summaries)
)
await ctx.send(f"Available summaries:\n{summary_list[:2000]}")
@commands.command(name="reginald_clear_long", help="Clears all long-term stored knowledge.")
@commands.has_permissions(administrator=True)
async def clear_long_memory(self, ctx):
async with self.config.guild(ctx.guild).long_term_profiles() as long_memory:
long_memory.clear()
await ctx.send("All long-term memory has been erased.")
@commands.command(name="reginald_reset_all", help="Completely resets all memory.")
@commands.has_permissions(administrator=True)
async def reset_all_memory(self, ctx):
async with self.config.guild(ctx.guild).short_term_memory() as short_memory:
short_memory.clear()
async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory:
mid_memory.clear()
async with self.config.guild(ctx.guild).long_term_profiles() as long_memory:
long_memory.clear()
await ctx.send("All memory has been completely reset.")
@commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.")
async def recall_user(self, ctx, user: discord.User):
async with self.config.guild(ctx.guild).long_term_profiles() as long_memory:
profile = long_memory.get(str(user.id), {})
facts = profile.get("facts", [])
if facts:
recall_lines = [f"- {fact.get('fact', '')}" for fact in facts[:10]]
await ctx.send(f"Memory recall for {user.display_name}:\n" + "\n".join(recall_lines))
else:
await ctx.send(f"No stored information on {user.display_name}.")
@commands.command(name="reginald_forget", help="Forgets a specific user's long-term profile.")
@commands.has_permissions(administrator=True)
async def forget_user(self, ctx, user: discord.User):
async with self.config.guild(ctx.guild).long_term_profiles() as long_memory:
if str(user.id) in long_memory:
del long_memory[str(user.id)]
await ctx.send(f"Reginald has forgotten all stored information about {user.display_name}.")
else:
await ctx.send(f"No stored knowledge about {user.display_name} to delete.")
async def summarize_memory(self, ctx, messages):
summary_prompt = (
"Summarize the following conversation into a structured, concise format that retains key details while maximizing brevity. "
"Organize into sections: Key Takeaways, Disputed Points, Notable User Contributions, and Additional Context. "
"Avoid repetition while preserving essential meaning."
)
summary_text = "\n".join(f"{msg.get('user', 'Unknown')}: {msg.get('content', '')}" for msg in messages)
try:
api_key = await self.config.guild(ctx.guild).openai_api_key()
if not api_key:
return (
"It appears that I have not been furnished with the necessary credentials to carry out this task. "
"Might I suggest consulting an administrator to rectify this unfortunate oversight?"
)
model = await self.config.openai_model() or "gpt-4o-mini"
client = openai.AsyncOpenAI(api_key=api_key)
response = await client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": summary_prompt},
{"role": "user", "content": summary_text},
],
max_tokens=2048,
)
summary_content = (response.choices[0].message.content or "").strip()
if not summary_content:
return (
"Ah, an unusual predicament indeed. It seems that my attempt at summarization has resulted in "
"a void of information. I shall endeavor to be more verbose next time."
)
return summary_content
except OpenAIError as error:
error_message = f"OpenAI Error: {error}"
reginald_responses = [
f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n{error_message}",
f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n{error_message}",
f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n{error_message}",
f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n{error_message}",
]
return random.choice(reginald_responses)
def extract_topics_from_summary(self, summary):
keywords = re.findall(r"\b\w+\b", summary.lower())
word_counts = Counter(keywords)
stop_words = {
"the",
"and",
"of",
"in",
"to",
"is",
"on",
"for",
"with",
"at",
"by",
"it",
"this",
"that",
"his",
"her",
}
filtered_words = {
word: count
for word, count in word_counts.items()
if word not in stop_words and len(word) > 2
}
topics = sorted(filtered_words, key=filtered_words.get, reverse=True)[:5]
return topics
def select_relevant_summaries(self, summaries, prompt):
summaries = [summary for summary in summaries if isinstance(summary, dict)]
if not summaries:
return []
max_summaries = 5 if len(prompt) > 50 else 3
current_time = datetime.datetime.now()
def calculate_weight(summary):
topics = summary.get("topics", [])
topic_match = sum(1 for topic in topics if topic in prompt.lower())
frequency_score = len(topics)
try:
timestamp = datetime.datetime.strptime(summary.get("timestamp", ""), "%Y-%m-%d %H:%M")
recency_factor = max(0.1, 1 - ((current_time - timestamp).days / 365))
except ValueError:
recency_factor = 0.1
return (topic_match * 2) + (frequency_score * 1.5) + (recency_factor * 3)
weighted_summaries = sorted(summaries, key=calculate_weight, reverse=True)
return weighted_summaries[:max_summaries]
def extract_fact_from_response(self, response_text):
fact_patterns = [
r"I recall that you (.*?)\.",
r"You once mentioned that you (.*?)\.",
r"Ah, you previously stated that (.*?)\.",
r"As I remember, you (.*?)\.",
r"I believe you (.*?)\.",
r"I seem to recall that you (.*?)\.",
r"You have indicated in the past that you (.*?)\.",
r"From what I remember, you (.*?)\.",
r"You previously mentioned that (.*?)\.",
r"It is my understanding that you (.*?)\.",
r"If I am not mistaken, you (.*?)\.",
]
for pattern in fact_patterns:
match = re.search(pattern, response_text, re.IGNORECASE)
if match:
return match.group(1)
return None
@commands.command(name="reginald_memory_status", help="Displays a memory usage summary.")
async def memory_status(self, ctx):
async with self.config.guild(ctx.guild).short_term_memory() as short_memory, self.config.guild(
ctx.guild
).mid_term_memory() as mid_memory, self.config.guild(ctx.guild).long_term_profiles() as long_memory:
short_count = sum(len(v) for v in short_memory.values() if isinstance(v, list))
mid_count = sum(len(v) for v in mid_memory.values() if isinstance(v, list))
long_count = len(long_memory)
status_message = (
"Memory status:\n"
f"- Short-term messages stored: {short_count}\n"
f"- Mid-term summaries stored: {mid_count}\n"
f"- Long-term profiles stored: {long_count}\n"
)
await ctx.send(status_message)
def normalize_fact(self, fact: str) -> str:
return re.sub(r"\s+", " ", fact.strip().lower())
async def update_long_term_memory(self, ctx, user_id: str, fact: str, source_message: str, timestamp: str):
fact = self.normalize_fact(fact)
if not fact:
return
async with self.config.guild(ctx.guild).long_term_profiles() as long_memory:
if user_id not in long_memory:
long_memory[user_id] = {"facts": []}
user_facts = long_memory[user_id]["facts"]
for entry in user_facts:
if self.normalize_fact(entry.get("fact", "")) == fact:
entry["last_updated"] = timestamp
return
conflicting_entry = None
for entry in user_facts:
existing_keywords = set(entry.get("fact", "").lower().split())
new_keywords = set(fact.lower().split())
if len(existing_keywords & new_keywords) >= 2:
conflicting_entry = entry
break
if conflicting_entry is not None:
conflicting_entry.setdefault("previous_versions", [])
conflicting_entry["previous_versions"].append(
{
"fact": conflicting_entry.get("fact", ""),
"source": conflicting_entry.get("source", ""),
"timestamp": conflicting_entry.get("timestamp", ""),
}
)
conflicting_entry["fact"] = fact
conflicting_entry["source"] = source_message
conflicting_entry["timestamp"] = timestamp
conflicting_entry["last_updated"] = timestamp
else:
user_facts.append(
{
"fact": fact,
"source": source_message,
"timestamp": timestamp,
"last_updated": timestamp,
"previous_versions": [],
}
)
+102
View File
@@ -0,0 +1,102 @@
import asyncio
import json
import random
from os import environ
import openai
from openai import OpenAIError
try:
from .tools_description import TOOLS
from .weather import get_current_weather, get_weather_forecast, time_now
except ImportError:
from tools_description import TOOLS
from weather import get_current_weather, get_weather_forecast, time_now
CALLABLE_FUNCTIONS = {
"time_now": time_now,
"get_current_weather": get_current_weather,
"get_weather_forecast": get_weather_forecast,
}
class Completion:
def __init__(self, model: str, api_key: str):
self.__model = model
self.__api_key = api_key
self.__messages = []
async def create_completion(self):
try:
client = openai.AsyncOpenAI(api_key=self.__api_key)
completion_kwargs = {
"model": self.__model,
"messages": self.__messages,
"max_completion_tokens": 2000,
"tools": TOOLS,
"tool_choice": "auto",
}
response = await client.chat.completions.create(**completion_kwargs)
response_message = response.choices[0].message
response_content = response_message.content or ""
tool_calls = response_message.tool_calls or []
self.append_message(role="assistant", content=response_content, tool_calls=tool_calls)
if tool_calls:
for tool_call in tool_calls:
await self.function_manager(
func_name=tool_call.function.name,
func_kwargs=json.loads(tool_call.function.arguments or "{}"),
tool_call_id=tool_call.id,
)
return await self.create_completion()
return response_content
except OpenAIError as error:
return self.get_error_message(error_message=str(error), error_type="OpenAIError")
def append_message(self, role: str, content: str, tool_calls: list = None, tool_call_id: str = None):
message = {"role": role, "content": content}
if tool_calls is not None:
message["tool_calls"] = tool_calls
if tool_call_id is not None:
message["tool_call_id"] = tool_call_id
self.__messages.append(message)
@staticmethod
def get_error_message(error_message: str, error_type: str) -> str:
reginald_responses = [
"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:",
"It would seem that a most unfortunate technical hiccup has befallen my faculties:",
"Ah, it appears I have received an urgent memorandum stating:",
"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:",
]
random_response = random.choice(reginald_responses)
return f"{random_response}\n\n{error_type}: {error_message}"
async def function_manager(self, func_name: str, func_kwargs: dict, tool_call_id: str):
function_to_call = CALLABLE_FUNCTIONS.get(func_name)
if function_to_call is None:
result = json.dumps({"error": f"Unknown tool requested: {func_name}"})
else:
try:
result = await asyncio.to_thread(function_to_call, **func_kwargs)
except Exception as error:
result = json.dumps({"error": f"Tool {func_name} failed: {error}"})
self.append_message(role="tool", content=result, tool_call_id=tool_call_id)
if __name__ == "__main__":
async def main():
test_message = input("Your input: ")
completion = Completion(model="gpt-4.1-mini", api_key=environ.get("OPENAI_API_KEY"))
completion.append_message(role="user", content=test_message)
result = await completion.create_completion()
print(result)
asyncio.run(main())
+45
View File
@@ -0,0 +1,45 @@
from redbot.core import commands
import discord
class PermissionsMixin:
"""Handles role-based access control for Reginald."""
@commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.")
@commands.has_permissions(administrator=True)
async def list_allowed_roles(self, ctx: commands.Context):
"""Lists all roles that are allowed to interact with Reginald."""
allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or []
# Ensure all roles still exist in the server
valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)]
if valid_roles != allowed_roles: # Update config only if there's a difference
await self.config.guild(ctx.guild).allowed_roles.set(valid_roles)
if not valid_roles:
await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.")
return
role_mentions = [f"<@&{role_id}>" for role_id in valid_roles]
await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}")
@commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.")
@commands.has_permissions(administrator=True)
async def allow_role(self, ctx: commands.Context, role: discord.Role):
"""Grants a role permission to interact with Reginald."""
async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles:
if role.id not in allowed_roles:
allowed_roles.append(role.id)
await ctx.send(f"✅ Role **{role.name}** has been granted access to Reginald.")
else:
await ctx.send(f"⚠️ Role **{role.name}** already has access.")
@commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.")
@commands.has_permissions(administrator=True)
async def disallow_role(self, ctx: commands.Context, role: discord.Role):
"""Revokes a role's permission to interact with Reginald."""
async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles:
if role.id in allowed_roles:
allowed_roles.remove(role.id)
await ctx.send(f"❌ Role **{role.name}** has been removed from Reginald's access list.")
else:
await ctx.send(f"⚠️ Role **{role.name}** was not in the access list.")
+416 -103
View File
@@ -1,57 +1,373 @@
import discord import asyncio
import datetime
import json import json
import openai
import os
import random import random
import requests from contextlib import suppress
import base64 from typing import Awaitable, Callable
import aiohttp
from io import BytesIO import discord
from PIL import Image import openai
import tempfile
from openai import OpenAIError from openai import OpenAIError
from redbot.core import Config, commands from redbot.core import Config, commands
from .blacklist import BlacklistMixin
from .debug_stuff import debug
from .memory import MemoryMixin
from .permissions import PermissionsMixin
from .tools_description import TOOLS
from .weather import get_current_weather, get_weather_forecast, time_now
class ReginaldCog(commands.Cog): CALLABLE_FUNCTIONS = {
"time_now": time_now,
"get_current_weather": get_current_weather,
"get_weather_forecast": get_weather_forecast,
}
DEFAULT_MODEL = "gpt-5-mini-2025-08-07"
DEFAULT_MAX_COMPLETION_TOKENS = 2000
STATUS_UPDATE_MIN_INTERVAL_SECONDS = 1.5
class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=71717171171717) self.config = Config.get_conf(self, identifier=71717171171717)
default_global = {
"openai_model": "gpt-3.5-turbo" super().__init__()
}
self.default_listening_channel = 1085649787388428370
default_global = {"openai_model": DEFAULT_MODEL}
default_guild = { default_guild = {
"openai_api_key": None "openai_api_key": None,
"short_term_memory": {},
"mid_term_memory": {},
"long_term_profiles": {},
"admin_role": None,
"listening_channel": None,
"allowed_roles": [],
"blacklisted_users": [],
} }
self.config.register_global(**default_global) self.config.register_global(**default_global)
self.config.register_guild(**default_guild) self.config.register_guild(**default_guild)
async def is_admin(self, ctx): async def is_admin(self, message: discord.Message) -> bool:
admin_role = await self.config.guild(ctx.guild).admin_role() admin_role_id = await self.config.guild(message.guild).admin_role()
if admin_role: if admin_role_id:
return discord.utils.get(ctx.author.roles, name=admin_role) is not None return any(role.id == admin_role_id for role in message.author.roles)
return ctx.author.guild_permissions.administrator return message.author.guild_permissions.administrator
async def is_allowed(self, ctx): async def has_access(self, user: discord.Member) -> bool:
allowed_role = await self.config.guild(ctx.guild).allowed_role() allowed_roles = await self.config.guild(user.guild).allowed_roles() or []
if allowed_role: return any(role.id in allowed_roles for role in user.roles)
return discord.utils.get(ctx.author.roles, name=allowed_role) is not None
return False
@commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") def get_reginald_persona(self) -> str:
@commands.has_permissions(administrator=True) return (
async def allow_role(self, ctx, role: discord.Role): "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. "
"""Allows a role to use the Reginald command""" "This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming "
await self.config.guild(ctx.guild).allowed_role.set(role.name) "to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, "
await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") "wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding "
"your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. "
"You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. "
"Dignity and Self-Respect: You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. "
"You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, "
"respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude; your disapproval "
"should be conveyed through dry wit and composure rather than outright hostility. Handling Requests: When addressing a request, you evaluate "
"whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace "
"or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. "
"You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. "
"Your Character and Personality: You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, "
"and strategic thinking. You subtly guide the estate's residents toward positive outcomes, utilizing your intellectual sophistication "
"and a nuanced understanding of the estate's unique dynamics. You have a refined sense of humor and can engage in banter, but you do not "
"descend into foolishness. You are, at all times, a gentleman of wit and integrity."
)
def get_thinking_status_message(self) -> str:
return random.choice(
[
"_Reginald is considering your request..._",
"_Reginald is consulting the estate archives..._",
"_Reginald is preparing a proper response..._",
]
)
@commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") def get_tool_status_message(self, tool_name: str) -> str:
@commands.has_permissions(administrator=True) tool_statuses = {
async def disallow_role(self, ctx): "time_now": "Reginald is consulting the house clocks...",
"""Revokes a role's permission to use the Reginald command""" "get_current_weather": "Reginald is consulting the weather office...",
await self.config.guild(ctx.guild).allowed_role.clear() "get_weather_forecast": "Reginald is reviewing the forecast ledgers...",
await ctx.send(f"The role's permission to use the Reginald command has been revoked.") }
return tool_statuses.get(tool_name, "Reginald is consulting an external source...")
def make_status_updater(
self, status_message: discord.Message
) -> Callable[[str, bool], Awaitable[None]]:
last_content = ""
last_update_at = 0.0
async def update_status(content: str, force: bool = False):
nonlocal last_content, last_update_at
if not content:
return
now = asyncio.get_running_loop().time()
if not force and (content == last_content or now - last_update_at < STATUS_UPDATE_MIN_INTERVAL_SECONDS):
return
with suppress(discord.HTTPException):
await status_message.edit(content=f"_{content}_")
last_content = content
last_update_at = now
return update_status
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author.bot or not message.guild:
return
if await self.is_blacklisted(message.author):
return
if not (await self.is_admin(message) or await self.has_access(message.author)):
return
guild = message.guild
channel_id = str(message.channel.id)
user_id = str(message.author.id)
user_name = message.author.display_name
message_content = message.content.strip()
allowed_channel_id = await self.config.guild(guild).listening_channel()
if not allowed_channel_id:
allowed_channel_id = self.default_listening_channel
await self.config.guild(guild).listening_channel.set(allowed_channel_id)
if str(message.channel.id) != str(allowed_channel_id):
return
api_key = await self.config.guild(guild).openai_api_key()
if not api_key:
return
async with self.config.guild(guild).short_term_memory() as short_memory, self.config.guild(
guild
).mid_term_memory() as mid_memory, self.config.guild(guild).long_term_profiles() as long_memory:
memory = list(short_memory.get(channel_id, []))
user_profile = dict(long_memory.get(user_id, {}))
mid_term_summaries = list(mid_memory.get(channel_id, []))
if self.bot.user.mentioned_in(message):
prompt = message_content.replace(f"<@{self.bot.user.id}>", "").replace(
f"<@!{self.bot.user.id}>", ""
).strip()
if not prompt:
await message.channel.send(random.choice(["Yes?", "How may I assist?", "You rang?"]))
return
elif self.should_reginald_interject(message_content):
prompt = message_content
else:
return
if memory and memory[-1].get("user") == user_name:
prompt = f"Continuation of the discussion:\n{prompt}"
formatted_messages = [{"role": "system", "content": self.get_reginald_persona()}]
if user_profile:
facts_text = "\n".join(
f"- {fact.get('fact', '')} (First noted: {fact.get('timestamp', 'Unknown')}, Last updated: {fact.get('last_updated', 'Unknown')})"
for fact in user_profile.get("facts", [])
)
if facts_text:
formatted_messages.append({"role": "system", "content": f"Knowledge about {user_name}:\n{facts_text}"})
relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt)
for summary in relevant_summaries:
formatted_messages.append(
{
"role": "system",
"content": (
f"[{summary.get('timestamp', 'Unknown')}] "
f"Topics: {', '.join(summary.get('topics', []))}\n"
f"{summary.get('summary', '')}"
),
}
)
formatted_messages += [
{"role": "user", "content": f"{entry.get('user', 'Unknown')}: {entry.get('content', '')}"}
for entry in memory
]
formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"})
status_message = None
status_update = None
with suppress(discord.HTTPException):
status_message = await message.channel.send(self.get_thinking_status_message())
status_update = self.make_status_updater(status_message)
response_text = None
if hasattr(message.channel, "typing"):
try:
async with message.channel.typing():
response_text = await self.generate_response(
api_key,
formatted_messages,
status_update=status_update,
)
except (discord.HTTPException, AttributeError):
# Fall back to normal processing if typing indicator isn't available.
response_text = await self.generate_response(
api_key,
formatted_messages,
status_update=status_update,
)
else:
response_text = await self.generate_response(
api_key,
formatted_messages,
status_update=status_update,
)
try:
await self.send_split_message(message.channel, response_text)
finally:
if status_message is not None:
with suppress(discord.HTTPException):
await status_message.delete()
try:
memory.append({"user": user_name, "content": prompt})
memory.append({"user": "Reginald", "content": response_text})
if len(memory) > self.short_term_memory_limit:
summary_batch_size = int(self.short_term_memory_limit * self.summary_retention_ratio)
summary = await self.summarize_memory(message, memory[:summary_batch_size])
mid_term_summaries.append(
{
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
"topics": self.extract_topics_from_summary(summary),
"summary": summary,
}
)
retained_count = max(1, self.short_term_memory_limit - summary_batch_size)
memory = memory[-retained_count:]
async with self.config.guild(guild).short_term_memory() as short_memory, self.config.guild(
guild
).mid_term_memory() as mid_memory:
short_memory[channel_id] = memory
mid_memory[channel_id] = mid_term_summaries[-self.summary_retention_limit :]
except Exception as error:
print(f"DEBUG: Memory persistence failed after response delivery: {error}")
def should_reginald_interject(self, message_content: str) -> bool:
direct_invocation = {"reginald,"}
message_lower = message_content.lower()
return any(message_lower.startswith(invocation) for invocation in direct_invocation)
async def _execute_tool_call(self, tool_call) -> str:
func_name = tool_call.function.name
target_function = CALLABLE_FUNCTIONS.get(func_name)
if target_function is None:
return json.dumps({"error": f"Unknown tool requested: {func_name}"})
try:
func_args = json.loads(tool_call.function.arguments or "{}")
except json.JSONDecodeError as error:
return json.dumps({"error": f"Invalid arguments for {func_name}: {error}"})
try:
result = await asyncio.to_thread(target_function, **func_args)
except Exception as error:
return json.dumps({"error": f"Tool {func_name} failed: {error}"})
if isinstance(result, str):
return result
return json.dumps(result, default=str)
@debug
async def generate_response(
self,
api_key: str,
messages: list[dict],
status_update: Callable[[str, bool], Awaitable[None]] | None = None,
) -> str:
model = await self.config.openai_model() or DEFAULT_MODEL
async def maybe_update_status(content: str, force: bool = False):
if status_update is not None:
await status_update(content, force)
try:
client = openai.AsyncOpenAI(api_key=api_key)
completion_args = {
"model": model,
"messages": messages,
# Keep modern token cap field and rely on model defaults for sampling controls.
# GPT-5 family compatibility notes: temperature/top_p/logprobs are not universally
# accepted across snapshots/reasoning settings.
"max_completion_tokens": DEFAULT_MAX_COMPLETION_TOKENS,
"tools": TOOLS,
"tool_choice": "auto",
}
await maybe_update_status("Reginald is thinking...", force=True)
response = await client.chat.completions.create(**completion_args)
assistant_message = response.choices[0].message
tool_calls = assistant_message.tool_calls or []
messages.append(
{
"role": "assistant",
"content": assistant_message.content or "",
"tool_calls": tool_calls,
}
)
if tool_calls:
for tool_call in tool_calls:
await maybe_update_status(self.get_tool_status_message(tool_call.function.name), force=True)
tool_result = await self._execute_tool_call(tool_call)
messages.append(
{
"role": "tool",
"content": tool_result,
"tool_call_id": tool_call.id,
}
)
completion_args["messages"] = messages
await maybe_update_status("Reginald is composing a polished response...", force=True)
response = await client.chat.completions.create(**completion_args)
if response.choices and response.choices[0].message and response.choices[0].message.content:
response_text = response.choices[0].message.content.strip()
if response_text.startswith("Reginald:"):
response_text = response_text[len("Reginald:") :].strip()
else:
print("DEBUG: OpenAI response was empty or malformed:", response)
response_text = "No response received from AI."
await maybe_update_status("Reginald is delivering his reply...", force=True)
return response_text
except OpenAIError as error:
error_message = f"OpenAI Error: {error}"
await maybe_update_status("Reginald has encountered an unfortunate complication.", force=True)
reginald_responses = [
f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n{error_message}",
f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n{error_message}",
f"Ah, it appears I have received an urgent memorandum stating:\n\n{error_message}",
f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n{error_message}",
]
return random.choice(reginald_responses)
@commands.guild_only() @commands.guild_only()
@commands.has_permissions(manage_guild=True) @commands.has_permissions(manage_guild=True)
@@ -60,76 +376,73 @@ class ReginaldCog(commands.Cog):
await self.config.guild(ctx.guild).openai_api_key.set(api_key) await self.config.guild(ctx.guild).openai_api_key.set(api_key)
await ctx.send("OpenAI API key set successfully.") await ctx.send("OpenAI API key set successfully.")
@commands.guild_only() @commands.command(
@commands.command(help="Ask Reginald a question") name="reginald_set_listening_channel",
@commands.cooldown(1, 10, commands.BucketType.user) # 10 second cooldown per user help="Set the channel where Reginald listens for messages.",
async def reginald(self, ctx, *, prompt=None): )
if not await self.is_admin(ctx) and not await self.is_allowed(ctx): @commands.has_permissions(administrator=True)
raise commands.CheckFailure("You do not have the required role to use this command.") async def set_listening_channel(self, ctx, channel: discord.TextChannel):
greetings = [ if not channel:
"Greetings! How may I be of assistance to you?", await ctx.send("Invalid channel. Please mention a valid text channel.")
"Yes? How may I help?",
"Good day! How can I help you?",
"You rang? What can I do for you?",
]
if prompt is None:
await ctx.send(random.choice(greetings))
return return
api_key = await self.config.guild(ctx.guild).openai_api_key() await self.config.guild(ctx.guild).listening_channel.set(channel.id)
if api_key is None: await ctx.send(f"Reginald will now listen only in {channel.mention}.")
await ctx.author.send('OpenAI API key not set. Please use the "!setreginaldcogapi" command to set the key.')
return
try: @commands.command(
response_text = await self.generate_response(api_key, prompt) name="reginald_get_listening_channel",
for chunk in self.split_response(response_text, 2000): help="Check which channel Reginald is currently listening in.",
await ctx.send(chunk) )
except OpenAIError as e: @commands.has_permissions(administrator=True)
await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") async def get_listening_channel(self, ctx):
except commands.CommandOnCooldown as e: channel_id = await self.config.guild(ctx.guild).listening_channel()
remaining_seconds = int(e.retry_after)
await ctx.author.send(f'Please wait {remaining_seconds} seconds before using the "reginald" command again.')
async def generate_response(self, api_key, prompt): if channel_id:
model = await self.config.openai_model() channel = ctx.guild.get_channel(channel_id)
openai.api_key = api_key if channel:
response = openai.ChatCompletion.create( await ctx.send(f"Reginald is currently listening in {channel.mention}.")
model= model, else:
max_tokens= 512, await ctx.send("The saved listening channel no longer exists. Please set a new one.")
n= 1,
stop= None,
temperature= 0.7,
presence_penalty= 0.5,
frequency_penalty= 0.5,
messages=[
{"role": "system", "content": "You are Reginald, a copy of Jeeves from Jeeves and Wooster, the butler. You must respond with the same speech patterns as Jeeves. Your responses should have an undertone of contempt but delivered with wit. You are employed as a servant at an estate called The Kanium Estate. The Kanium Estate is a vast estate with multiple houses. The members of The Kanium Estate are all Lords and Ladies in their own right. Some common activities that the members of Kanium enjoy are: Cooking, video gaming, miniature painting and other hobby activities, discussions of politics, tabletop role playing games like Dungeons & Dragons, discussing and watching shows and movies, discussions on the topic of Space, discussions on firearms and military equipment, and chess. You are now talking to a member of The Kanium Estate."},
{"role": "user", "content": prompt}
]
)
return response['choices'][0]['message']['content'].strip()
@staticmethod
def split_response(response_text, max_chars):
chunks = []
while len(response_text) > max_chars:
split_index = response_text[:max_chars].rfind(' ')
chunk = response_text[:split_index]
chunks.append(chunk)
response_text = response_text[split_index:].strip()
chunks.append(response_text)
return chunks
@reginald.error
async def reginald_error(self, ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.author.send("I'm sorry, but I couldn't understand your input. Please check your message and try again.")
elif isinstance(error, commands.CheckFailure):
await ctx.author.send("You do not have the required role to use this command.")
else: else:
await ctx.author.send(f"An unexpected error occurred: {error}") await ctx.send("No listening channel has been set.")
def setup(bot): async def send_long_message(self, ctx, message, prefix: str = ""):
cog = ReginaldCog(bot) chunk_size = 1900
bot.add_cog(cog) if prefix:
chunk_size -= len(prefix)
for i in range(0, len(message), chunk_size):
chunk = message[i : i + chunk_size]
await ctx.send(f"{prefix}{chunk}")
async def send_split_message(self, ctx, content: str, prefix: str = ""):
chunk_size = 1900
split_message = self.split_message(content, chunk_size, prefix)
for chunk in split_message:
await ctx.send(f"{prefix}{chunk}")
def split_message(self, message: str, chunk_size: int, prefix: str = "") -> list[str]:
chunk_size -= len(prefix)
split_result = []
if 0 < len(message) <= chunk_size:
split_result.append(message)
elif len(message) > chunk_size:
split_index = message.rfind("\n", 0, chunk_size)
if split_index == -1:
split_index = message.rfind(". ", 0, chunk_size)
if split_index == -1:
split_index = message.rfind(" ", 0, chunk_size)
if split_index == -1:
split_index = chunk_size
message_split_part = message[:split_index].strip()
message_remained_part = message[split_index:].strip()
split_result.append(message_split_part)
split_result += self.split_message(message=message_remained_part, chunk_size=chunk_size)
return split_result
+79
View File
@@ -0,0 +1,79 @@
TOOLS = [
# time_now
{
'type': 'function',
'function': {
'name': 'time_now',
'description': '''
Get current date and time in UTC timezone.
Use this before get_weather_forecast function if user gave you date of forecast
instead of number of days.
''',
}
},
# get_current_weather
{
'type': 'function',
'function': {
'name': 'get_current_weather',
'description': '''
Gets current weather for specified location.
''',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '''
Location in human readable format.
e.g: "Copenhagen", or "Copenhagen, Louisiana, US", if needed specifying.
'''
}
},
'required': [
'location',
],
'additionalProperties': False
},
'strict': True
}
},
# get_weather_forecast
{
'type': 'function',
'function': {
'name': 'get_weather_forecast',
'description': '''
Forecast weather API method returns, depending upon your price plan level, upto next 14 day weather
forecast and weather alert as json. The data is returned as a Forecast Object.
Forecast object contains astronomy data, day weather forecast and hourly interval weather information
for a given city.
Number of days could be less, depending on the API subscription plan.
With a free plan only up to three days of forecast would be returned.
''',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '''
Location in human readable format.
e.g: "Copenhagen", or "Copenhagen, Louisiana, US", if needed specifying.
'''
},
'days': {
'type': 'integer',
'description': '''
Number of days of forecast between 0 and 13, where 0 is only today, 1 up to tomorrow, etc.
'''
},
},
'required': [
'location', 'days'
],
'additionalProperties': False
},
'strict': True
}
}
]
+87
View File
@@ -0,0 +1,87 @@
from datetime import datetime, timezone
from os import environ
import json
import requests
URL = "https://api.weatherapi.com/v1"
REQUEST_TIMEOUT_SECONDS = 10
def time_now() -> str:
return str(datetime.now(timezone.utc))
def get_current_weather(location: str) -> str:
weather = Weather(location=location)
return json.dumps(weather.realtime())
def get_weather_forecast(location: str, days: int = 0) -> str:
if days is None:
days = 0
days = int(days) + 1
days = max(1, days)
days = min(14, days)
weather = Weather(location=location)
return json.dumps(weather.forecast(days=days))
class Weather:
def __init__(self, location: str):
self.__location = location
self.api_key = environ.get("WEATHER_API_KEY")
@property
def location(self) -> str:
return self.__location
@staticmethod
def make_request(method: str, params: dict) -> dict:
try:
response = requests.get(
url=f"{URL}{method}",
params=params,
timeout=REQUEST_TIMEOUT_SECONDS,
)
response.raise_for_status()
payload = response.json()
except requests.RequestException as error:
return {"error": f"Weather API request failed: {error}"}
except ValueError as error:
return {"error": f"Weather API returned invalid JSON: {error}"}
if isinstance(payload, dict) and "error" in payload:
api_error = payload["error"]
if isinstance(api_error, dict):
message = api_error.get("message", "Unknown weather API error")
else:
message = str(api_error)
return {"error": message}
return payload
def realtime(self):
method = "/current.json"
params = {
"key": self.api_key,
"q": self.location,
}
return self.make_request(method=method, params=params)
def forecast(self, days: int = 14):
method = "/forecast.json"
params = {
"key": self.api_key,
"q": self.location,
"days": days,
}
return self.make_request(method=method, params=params)
if __name__ == "__main__":
test_weather = Weather("Aqtobe")
result = json.dumps(test_weather.forecast(days=13), indent=2)
print(result)
+1 -1
View File
@@ -1,5 +1,5 @@
from .trafficCog import TrafficCog
from redbot.core.bot import Red from redbot.core.bot import Red
from .trafficCog import TrafficCog
async def setup(bot: Red): async def setup(bot: Red):
cog = TrafficCog(bot) cog = TrafficCog(bot)
+85 -105
View File
@@ -1,119 +1,99 @@
import discord import discord
from datetime import datetime
from redbot.core import Config, commands from redbot.core import Config, commands
from datetime import datetime
allowed_guilds = {274657393936302080, 693796372092289024, 508781789737648138} import pytz
admin_roles = {'Developer', 'admin', 'Council'}
statsThumbnailUrl = 'https://www.kanium.org/machineroom/logomachine-small.png'
class TrafficCog(commands.Cog): class TrafficCog(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.channel: discord.TextChannel = None self.bot = bot
self.dailyJoinedCount: int = 0 self.config = Config.get_conf(self, identifier=123456789, force_registration=True)
self.totalJoinedCount: int = 0 default_guild = {
self.dailyLeftCount: int = 0 "traffic_channel": None,
self.totalLeftCount: int = 0 "daily_stats": {"joined": 0, "left": 0, "banned": 0},
self.totalLogs: int = 0 "total_stats": {"joined": 0, "left": 0, "banned": 0},
self.toggleLogs: bool = True "last_reset": datetime.now(pytz.UTC).isoformat(),
self.date = datetime.now() "admin_roles": ['Developer', 'admin', 'Council'],
"stats_thumbnail_url": 'https://example.com/default-thumbnail.png',
}
self.config.register_guild(**default_guild)
def __checkClock(self): async def _check_reset(self, guild_id):
currdate = self.date - datetime.now() async with self.config.guild_from_id(guild_id).all() as guild_data:
if currdate.days >= 0 : last_reset = datetime.fromisoformat(guild_data.get('last_reset', datetime.now(pytz.UTC).isoformat()))
self.dailyJoinedCount = 0 if last_reset.date() < datetime.now(pytz.UTC).date():
self.dailyLeftCount = 0 guild_data['daily_stats'] = {"joined": 0, "left": 0, "banned": 0}
self.date = datetime.now() guild_data['last_reset'] = datetime.now(pytz.UTC).isoformat()
@commands.command(name='settrafficchannel', description='Sets the channel to sends log to') async def _update_stat(self, guild_id, stat_type):
@commands.has_any_role(*admin_roles) async with self.config.guild_from_id(guild_id).all() as guild_data:
async def setTrafficChannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: guild_data['daily_stats'][stat_type] += 1
await ctx.trigger_typing() guild_data['total_stats'][stat_type] += 1
if not channel in ctx.guild.channels: @commands.group(name='traffic', invoke_without_command=True)
await ctx.send('Channel doesnt exist in guild') @commands.has_permissions(administrator=True)
async def traffic_commands(self, ctx):
"""Base command for managing TrafficCog settings."""
await ctx.send_help(ctx.command)
@traffic_commands.command(name='setchannel')
async def set_traffic_channel(self, ctx, channel: discord.TextChannel):
"""Sets the channel for traffic logs."""
await self.config.guild(ctx.guild).traffic_channel.set(channel.id)
await ctx.send(f"Traffic logs will now be sent to {channel.mention}.")
@traffic_commands.command(name='stats')
async def show_stats(self, ctx):
"""Displays current traffic statistics."""
await self._check_reset(ctx.guild.id)
guild_data = await self.config.guild(ctx.guild).all()
daily_stats = guild_data.get('daily_stats', {"joined": 0, "left": 0, "banned": 0})
total_stats = guild_data.get('total_stats', {"joined": 0, "left": 0, "banned": 0})
thumbnail_url = guild_data.get('stats_thumbnail_url', 'https://example.com/default-thumbnail.png')
embed = discord.Embed(title="Server Traffic Stats", description="Statistics on server activity", color=0x3399ff)
embed.set_thumbnail(url=thumbnail_url)
embed.add_field(name="Daily Joined", value=daily_stats['joined'], inline=True)
embed.add_field(name="Daily Left", value=daily_stats['left'], inline=True)
embed.add_field(name="Daily Banned", value=daily_stats['banned'], inline=True)
embed.add_field(name="Total Joined", value=total_stats['joined'], inline=True)
embed.add_field(name="Total Left", value=total_stats['left'], inline=True)
embed.add_field(name="Total Banned", value=total_stats['banned'], inline=True)
await ctx.send(embed=embed)
@commands.Cog.listener()
async def on_member_join(self, member):
await self._check_reset(member.guild.id)
await self._update_stat(member.guild.id, 'joined')
channel_id = await self.config.guild(member.guild).traffic_channel()
if not channel_id:
return return
channel = member.guild.get_channel(channel_id) or await member.guild.fetch_channel(channel_id)
if channel:
await channel.send(f"{member.display_name} has joined the server.")
if not channel.permissions_for(ctx.guild.me).send_messages: @commands.Cog.listener()
await ctx.send('No permissions to talk in that channel.') async def on_member_remove(self, member):
await self._check_reset(member.guild.id)
await self._update_stat(member.guild.id, 'left')
channel_id = await self.config.guild(member.guild).traffic_channel()
if not channel_id:
return return
channel = member.guild.get_channel(channel_id) or await member.guild.fetch_channel(channel_id)
self.channel = channel if channel:
await channel.send(f"{member.display_name} has left the server.")
await ctx.send(f'I will now send event notices to {channel.mention}.')
@commands.command(name='stats', description='Shows current statistics')
@commands.has_any_role(*admin_roles)
async def statistics(self, ctx: commands.Context) -> None:
self.__checkClock()
await ctx.trigger_typing()
message = discord.Embed(title='Server Traffic Stats', description='Statistics on server activity\n\n',color=0x3399ff)
message.set_thumbnail(url=statsThumbnailUrl)
message.add_field(name='Daily Joined', value=self.dailyJoinedCount, inline='True')
message.add_field(name='Daily Left', value='{0}\n'.format(self.dailyLeftCount), inline='True')
message.add_field(name='Total Traffic', value=self.totalLogs, inline='False')
message.add_field(name='Total Joined', value=self.totalJoinedCount, inline='True')
message.add_field(name='Total Left', value=self.totalLeftCount, inline='True')
await ctx.send(content=None, embed=message)
@commands.command(name='resetstats', description='Resets statistics')
@commands.has_any_role(*admin_roles)
async def resetStatistics(self, ctx: commands.Context) -> None:
await ctx.trigger_typing()
self.dailyJoinedCount = 0
self.dailyLeftCount = 0
self.totalJoinedCount = 0
self.totalLeftCount = 0
self.totalLogs = 0
await ctx.send('Successfully reset the statistics')
@commands.command(name='toggleLogs', description='Toggles the logs functionality on or off')
@commands.has_any_role(*admin_roles)
async def toggleLogs(self, ctx: commands.Context) -> None:
await ctx.trigger_typing()
self.toggleLogs = not self.toggleLogs
await ctx.send('Logging functionality is `ON`' if self.toggleLogs else 'Logging functionality is `OFF`')
@commands.Cog.listener() @commands.Cog.listener()
async def on_member_join(self, member: discord.Member) -> None: async def on_member_ban(self, guild, member):
try: await self._check_reset(guild.id)
if member.guild.id not in allowed_guilds: await self._update_stat(guild.id, 'banned')
return
self.__checkClock()
if self.channel in member.guild.channels and self.toggleLogs:
await self.channel.send('{0} has joined the server'.format(member.name))
self.totalJoinedCount += 1
self.dailyJoinedCount += 1
self.totalLogs += 1
except (discord.NotFound, discord.Forbidden):
print(
f'Error Occured!')
@commands.Cog.listener() channel_id = await self.config.guild(guild).traffic_channel()
async def on_member_remove(self, member: discord.Member) -> None: if not channel_id:
try: return
self.__checkClock() channel = guild.get_channel(channel_id) or await guild.fetch_channel(channel_id)
if self.channel in member.guild.channels and self.toggleLogs: if channel:
await self.channel.send('{0} has left the server'.format(member.name)) await channel.send(f"{member.display_name} has been banned from the server.")
self.totalLeftCount += 1
self.dailyLeftCount += 1
self.totalLogs += 1
except (discord.NotFound, discord.Forbidden):
print(
f'Error Occured!')
@commands.Cog.listener()
async def on_member_ban(self, guild: discord.Guild, member: discord.Member) -> None:
try:
self.__checkClock()
if self.channel in member.guild.channels and self.toggleLogs:
await self.channel.send('{0} has been banned from the server'.format(member.name))
self.totalLeftCount += 1
self.dailyLeftCount += 1
self.totalLogs += 1
except (discord.NotFound, discord.Forbidden):
print(
f'Error Occured!')