Compare commits

..
Author SHA1 Message Date
alwayscompiling 0acfa77492 Change delete_scores to a slash command. 2022-08-08 22:07:12 -04:00
alwayscompiling ffefd08761 Updated presense to account for slash commands. 2022-08-08 21:54:27 -04:00
alwayscompiling a61662429a Remove help command. Doesn't work with slash commands. 2022-08-08 21:51:25 -04:00
alwayscompiling 57244bcbfe Update post highscores command to slash command. 2022-08-08 21:51:06 -04:00
alwayscompiling 3fd9ff46a3 imports cleanup. 2022-08-08 21:36:45 -04:00
alwayscompiling d4ad727f2a Correcting error statement. 2022-08-06 22:58:43 -04:00
alwayscompiling 516ff7feeb Update minimum python version. Nextcord v2.0 requires python 3.8. 2022-08-06 22:26:55 -04:00
alwayscompiling 0eb332aefd Added verify and submission register subcommands. Removed bot argument from Register class. 2022-08-06 22:21:08 -04:00
alwayscompiling ff07787a9a Create register group and highscore slash subcommand. 2022-08-06 20:14:30 -04:00
alwayscompiling b5959ccc33 Change constructor of score. Create new function to add score value from a string. 2022-08-06 17:32:17 -04:00
alwayscompiling b80f160914 Update verification view to use Table instead of Boss. 2022-08-06 16:55:06 -04:00
alwayscompiling 3b866be3ab Corrected typo in submission_state setter. 2022-08-06 16:45:05 -04:00
alwayscompiling e55b22c821 Easier to read submission access. 2022-08-06 16:41:50 -04:00
alwayscompiling f06e6f1e1e Create function to know if a user has a submission. 2022-08-06 16:30:49 -04:00
alwayscompiling 5f3e7361b4 Convert old submission_objects to new class. 2022-08-06 16:30:31 -04:00
alwayscompiling 835611a11e Created submission classes. 2022-08-06 16:10:19 -04:00
alwayscompiling ee9a23ca79 Renamed data folder to classes. 2022-08-06 14:59:58 -04:00
alwayscompiling 448bed17ec Have to pass guild object to save function. 2022-08-04 11:34:51 -04:00
alwayscompiling b4d0f8a15e Save data after posting messages. 2022-08-04 11:22:50 -04:00
alwayscompiling 72a97f6cb7 Correct config creation. 2022-08-04 11:15:27 -04:00
alwayscompiling 8aadf07968 Added asending and time record setters. 2022-08-04 11:06:36 -04:00
alwayscompiling a6f98d11ab Added temporary config handling. 2022-08-04 11:06:18 -04:00
alwayscompiling 4ee0593619 Edit category constructor to allow for empty objects. 2022-08-04 10:38:45 -04:00
alwayscompiling d4db9b51f0 Moved table size to table class. Edited table constructor. 2022-08-04 10:33:34 -04:00
alwayscompiling f67eb75c7b Add warning for posting without tables. 2022-08-04 10:29:14 -04:00
alwayscompiling 6f4322bf6a Added a to_str function for all classes. __str__ function uses it. highscores_data uses the to_str function for saving. 2022-08-03 22:35:27 -04:00
alwayscompiling 66ace947c6 Saving guild after modification. 2022-08-03 22:21:49 -04:00
alwayscompiling 4d9c70bf09 Update saving functions. 2022-08-03 22:16:43 -04:00
alwayscompiling 4c944c8323 pylint line length. 2022-08-03 20:08:32 -04:00
alwayscompiling b65254c2cc Runtime error fixes. 2022-08-03 20:03:48 -04:00
alwayscompiling 5b3c9ef247 Somehow didn't finish guilds function. 2022-08-03 19:32:37 -04:00
alwayscompiling 027da38574 Update utils function args to use new data classes instead of various args. 2022-08-03 19:28:47 -04:00
alwayscompiling 80e4602621 Update submissin cog to new data class. Also clean up import lists. 2022-08-02 21:52:12 -04:00
alwayscompiling 07310c804a Remove score command. It isn't used. 2022-08-02 21:41:13 -04:00
alwayscompiling 4641ce4048 Update register command to use data class. 2022-08-02 21:40:27 -04:00
alwayscompiling 142842a36b Update post highscores to new data class. 2022-08-02 21:36:17 -04:00
alwayscompiling 6371fc4e8c Update message listener to new data classes. 2022-08-02 21:29:57 -04:00
alwayscompiling 3fac5f7a06 Update guild listener command to new data classes. 2022-08-02 21:23:16 -04:00
alwayscompiling 80f3966d39 Update delete scores command. 2022-08-02 21:21:36 -04:00
alwayscompiling 69a439d13a Correct message map creation. 2022-08-02 16:43:15 -04:00
alwayscompiling 251ac2cf47 Update highscores.py to use new data classes. 2022-08-02 16:41:46 -04:00
alwayscompiling 2dc0350213 Idk how the merge messed up, but fixed the bad merge. 2022-08-02 16:36:50 -04:00
alwayscompiling 7e40937cdd Merge branch '25-data-class' of github.com:alwayscompiling/pvm-highscores into 25-data-class 2022-08-02 16:33:35 -04:00
alwayscompiling 761a7df29f Constructor and string function fixes. Ignored new data dir. 2022-08-02 16:30:30 -04:00
alwayscompiling b3a84fc3a8 Add str function for highscores data. 2022-08-02 16:20:56 -04:00
alwayscompiling 262bcd07b9 Update highscores_data to be an overarching class for all data. 2022-08-02 16:16:45 -04:00
alwayscompiling 8ca77d93b3 Update highscores_data to be an overarching class for all data. 2022-08-02 16:13:08 -04:00
alwayscompiling 1b4f0362b5 Renamed runescape example config. 2022-08-01 22:50:01 -04:00
alwayscompiling ce60288892 Add highscore size to guild class. 2022-08-01 22:48:23 -04:00
alwayscompiling 2515b90497 redo guild docstrings. 2022-08-01 22:38:32 -04:00
alwayscompiling 3ce9f8090f Correct from dict wording. 2022-08-01 22:29:30 -04:00
alwayscompiling 68da97e138 Redo table docstrings. 2022-08-01 22:23:38 -04:00
alwayscompiling ef28b97a1b get user score was using wrong variable. 2022-08-01 22:22:16 -04:00
alwayscompiling e43a0a8977 Redo category docstrings. 2022-08-01 22:17:52 -04:00
alwayscompiling 2ee2d3405e Redo score docstrings. 2022-08-01 22:06:54 -04:00
alwayscompiling af204a3156 Correct sort scores value. 2022-08-01 21:56:23 -04:00
alwayscompiling 109b22be62 Add get_table function. 2022-08-01 21:21:57 -04:00
alwayscompiling aa4b06c125 Correct return type of tables. 2022-08-01 21:17:26 -04:00
alwayscompiling 96fce1d661 Clarified to_dict function description. 2022-08-01 21:12:46 -04:00
alwayscompiling 3d471198f5 Created rest of guild functions. 2022-08-01 21:09:03 -04:00
alwayscompiling 05d0653a5b Create guild class. 2022-07-31 22:45:57 -04:00
alwayscompiling 6758f4e90d Changing from_dict to classmethod. 2022-07-31 22:14:53 -04:00
alwayscompiling 6e6b57c49c Create table class. 2022-07-31 12:07:07 -04:00
alwayscompiling b4f72b9528 Renamed scores to scores_map to avoid confusion. 2022-07-31 11:51:50 -04:00
alwayscompiling cd81ee3140 Change score score to be score value. 2022-07-31 10:57:45 -04:00
alwayscompiling 6ddba14908 Change str function to use json string dump. 2022-07-31 10:19:23 -04:00
alwayscompiling 0fc0fc33da Filled out to and from dict functions and a few others. 2022-07-30 23:12:40 -04:00
alwayscompiling 529761b45f Remove optional on score. 2022-07-30 22:43:11 -04:00
alwayscompiling bac64110c1 Add category class. 2022-07-30 22:31:40 -04:00
alwayscompiling d04de253e7 Add str function. 2022-07-30 22:15:58 -04:00
alwayscompiling 8c7a801800 Small description change. 2022-07-30 21:58:10 -04:00
alwayscompiling 01df80d358 Correcting time string. 2022-07-30 21:53:29 -04:00
alwayscompiling 06248cfe6c Using property decorators. 2022-07-30 21:12:36 -04:00
alwayscompiling a3af93d4bc Create Score class. 2022-07-30 17:09:44 -04:00
alwayscompiling 646ee0274b Move highscores_data to new location. 2022-07-30 16:34:33 -04:00
alwayscompiling fe0a74cfc4 Extend submission timeout. Remove submission object on submission timeout. Also Let user know when it isn't their submission. 2022-07-30 16:19:44 -04:00
alwayscompiling 5bed587da9 Fixing configs for elite dungeon bosses. 2022-07-30 10:56:21 -04:00
alwayscompiling f7a40f83a9 Birthday troll. 2022-07-29 22:45:40 -04:00
alwayscompiling a66bc4982f on_guild_join needs 2 parameters. 2022-07-29 22:45:21 -04:00
alwayscompiling 04051eeacc Merge branch '14-score-entries-as-dictsjson-objects' into develop 2022-07-29 16:49:50 -04:00
alwayscompiling f33dc8ce37 Skeleton needs to have dict for category scores. 2022-07-29 16:48:25 -04:00
alwayscompiling 306f983c8e Had to change access to scores. Somewhat weird due to them being dicts and dicts being difficult to access by index. 2022-07-29 16:35:45 -04:00
alwayscompiling 9e9d616814 Change tuple to list. Its stored as a list anyways. 2022-07-29 16:16:16 -04:00
alwayscompiling 5a09beea31 Convert add_score to have scores as a dict. 2022-07-29 15:56:37 -04:00
alwayscompiling 8c2b3fa487 Server count on all presense strings. 2022-07-29 15:10:41 -04:00
alwayscompiling 62e89960f7 Changed presence update functionability to a cog. Randomly chooses a string and updates the presence every 2 minutes. 2022-07-29 15:08:42 -04:00
alwayscompiling e2b0c0ea89 Enforcing python version because ordered dicts in 3.7 is required. 2022-07-29 14:27:10 -04:00
alwayscompiling 3eed30567d Fixed single digit second submissions. Covered bases to ensure time format is consistant. 2022-07-29 14:12:58 -04:00
alwayscompiling 565babd1d3 Correct submission listener usage of HighscoresData. 2022-07-28 11:14:38 -04:00
alwayscompiling 455b51a998 Remove outdated import. 2022-07-28 11:02:47 -04:00
alwayscompiling f136181216 Message map has guild id as an int. Update submission view to use HighscoresData. 2022-07-28 11:01:40 -04:00
alwayscompiling 463ef48052 Move message map creation to highscores.py. Was a circular import. 2022-07-28 10:41:10 -04:00
alwayscompiling 64004329da Add HighscoresData to register command. Add channel setting functions. 2022-07-28 10:25:56 -04:00
alwayscompiling f497928054 Get table names from data. Update post scores to use HighscoresData class. 2022-07-28 10:17:46 -04:00
alwayscompiling 0911ebfa45 Updated guild listener to HighscoresData class. 2022-07-27 22:32:28 -04:00
alwayscompiling beedf946c9 Update delete scores to new HighscoresData class. 2022-07-27 22:27:27 -04:00
alwayscompiling 5b31314f02 Fill out HighscoreData class with access functions for utils.py. 2022-07-27 22:15:06 -04:00
alwayscompiling 4b15fef30c Set highscore_data type. 2022-07-27 21:59:57 -04:00
alwayscompiling e11a86af7b Update highscores.py to use HighscoreData class. 2022-07-27 20:48:04 -04:00
alwayscompiling f4f8f426d0 Move create_message_map to here. 2022-07-27 20:44:35 -04:00
alwayscompiling b8eb84165e Create new class for storing data. 2022-07-27 20:44:07 -04:00
alwayscompiling 273ff757af Correct delete scores description. 2022-07-26 22:44:32 -04:00
alwayscompiling f84e2eef29 Preventing user from having multiple submissions. 2022-07-26 22:17:24 -04:00
alwayscompiling b4eb5ab6d1 Removed deleting of scores pushed off table. 2022-07-26 22:12:12 -04:00
alwayscompiling 1ae6a4a223 Ensure formatting lines up times and right justifies int scores. 2022-07-26 22:09:35 -04:00
alwayscompiling e1a429c42a Trim leading 0s from time scores. 2022-07-26 21:48:31 -04:00
alwayscompiling 790e8ca50f Create delete-score command. 2022-07-26 21:35:56 -04:00
alwayscompiling 7ba43ea4f4 Creating message map entry if guild id isn't there already. 2022-07-26 21:22:12 -04:00
alwayscompiling 39c858af64 Correct regex to ensure start and end of string. 2022-07-23 15:23:16 -04:00
alwayscompiling 093861b2e6 Improve readability with guild id variable. 2022-07-23 14:26:07 -04:00
alwayscompiling 4ef487a8c4 Saving deleted data from left servers. 2022-07-23 14:12:32 -04:00
alwayscompiling 849ebb966a Guild id is a string in message map. 2022-07-23 14:02:47 -04:00
alwayscompiling f1b0e34194 message_map doesn't need guild id as a string. 2022-07-23 00:06:56 -04:00
alwayscompiling 7cdc7d2cef Need empty dict before adding to it. 2022-07-23 00:00:18 -04:00
alwayscompiling db4672dcda Correct spacing. 2022-07-22 23:59:46 -04:00
alwayscompiling fc2b601687 Somehow removed await on fetch message. 2022-07-22 23:44:33 -04:00
alwayscompiling 493d44fa19 Pass guild_data when needed. 2022-07-22 23:41:00 -04:00
alwayscompiling 5d3a1568a2 Guild id is a string in highscores data json. 2022-07-22 23:39:51 -04:00
alwayscompiling be29194a57 Get guild data everywhere highscores_data was used. 2022-07-22 23:33:11 -04:00
alwayscompiling d067da2300 Changed config table name. 2022-07-22 23:02:24 -04:00
alwayscompiling 6d0c10e1cc Create empty dict for each guild during message_map creation. 2022-07-22 22:41:35 -04:00
alwayscompiling 609bfa1277 Update message map and send highscore message for multiple guilds. 2022-07-22 22:36:47 -04:00
alwayscompiling 158e53e799 Add on_guild_remove listener. 2022-07-22 22:09:09 -04:00
alwayscompiling 3cc67a4e9e Corrected skeleton provider. Add cog for guild listener. 2022-07-22 22:05:21 -04:00
alwayscompiling df80eb2952 Move data skeleton creation out of opening highscores data. 2022-07-22 21:52:13 -04:00
alwayscompiling b78eb7b9bf Changed order of functions. 2022-07-22 21:41:45 -04:00
alwayscompiling 65ce143cfc Merge branch '18-remove-boss-message-map' into develop 2022-07-22 21:37:29 -04:00
alwayscompiling 8dbb2a6895 Missed adding tables one place. 2022-07-22 21:34:23 -04:00
alwayscompiling a24fe52182 Message id is no longer a string in the message map. 2022-07-22 21:32:18 -04:00
alwayscompiling d0131b1044 Updated all table data accesses to have table as part of access. 2022-07-22 21:30:46 -04:00
alwayscompiling 307ef86493 Removed old usage of message map from file. 2022-07-22 21:22:17 -04:00
alwayscompiling a1a9fae9fa Generating message map on bot start. 2022-07-22 21:18:01 -04:00
alwayscompiling 56984b3df0 Remove unnecessary intent activation. 2022-07-22 20:59:25 -04:00
alwayscompiling 7a261d56fc Move formatting to when score is retrieved. 2022-07-21 10:31:07 -04:00
alwayscompiling 4ee2280565 Correcting example runescape config. 2022-07-21 10:04:52 -04:00
alwayscompiling a24900a957 Merge branch '15-format-messages-to-look-cleaner' into develop 2022-07-20 22:56:22 -04:00
alwayscompiling ef17d3dc3e Made all scores line up properly. 2022-07-20 22:55:40 -04:00
alwayscompiling d711763c80 Standardize time score format. 2022-07-20 22:35:33 -04:00
alwayscompiling 8318bf741a Remove old TODO. 2022-07-20 22:32:59 -04:00
alwayscompiling 17c1915810 Change location of approve button. Should be on right. 2022-07-20 22:31:39 -04:00
alwayscompiling 674ea94588 Pad username out to max length. 2022-07-20 22:24:50 -04:00
alwayscompiling 090184c1e9 Tracking username length limit in data file. 2022-07-20 22:17:20 -04:00
alwayscompiling 31fa0b50a3 Added regex for time and int submissions. 2022-07-20 22:06:07 -04:00
alwayscompiling 374632bbc6 Removing TODOs. 2022-07-20 20:58:17 -04:00
alwayscompiling 8a36c9dac9 Users cannot move to next step without completing current step. 2022-07-20 20:50:29 -04:00
alwayscompiling 1900e21f72 Updating message listener to work with states. 2022-07-20 20:37:29 -04:00
alwayscompiling 2c85483db5 Buttons now change views, embed has proper steps. Added todos for what is next. 2022-07-19 22:29:06 -04:00
alwayscompiling 63db9fb7d4 Create buttons and enum for submission state navigation. 2022-07-19 21:05:37 -04:00
alwayscompiling 5155744ec5 Made config file more efficient. 2022-07-13 22:02:40 -04:00
alwayscompiling fedc8be778 Adding VerificationView to bot on cog load. 2022-07-13 20:01:21 -04:00
alwayscompiling 125939e5b3 Updating runescape_bosses.json example config to follow new config style. 2022-07-12 21:26:12 -04:00
alwayscompiling 3b410b5f90 Redid configs to allow for unique categories on each table. 2022-07-12 21:00:51 -04:00
alwayscompiling d7bac2048f Changed listener to allow for score or attachment by themselves. 2022-07-12 19:59:21 -04:00
alwayscompiling dd4bee73c6 Approved message now mentions person who approved. 2022-07-12 18:30:25 -04:00
alwayscompiling d21683fb59 Only add best score if its new, only delete worse scores if they exist. 2022-07-12 16:07:20 -04:00
alwayscompiling 44b3d7a80b Merge branch 'develop' into 4-user-score-update 2022-07-12 15:59:42 -04:00
alwayscompiling af5017c95d Changed id of verification views. Solved problem for long lasting buttons. 2022-07-12 15:59:17 -04:00
alwayscompiling 3087c6bc39 Worked out dup check method, just need to debug. 2022-07-12 15:58:39 -04:00
alwayscompiling cf0ef96edc Merge branch 'develop' into 4-user-score-update 2022-07-12 15:22:07 -04:00
alwayscompiling 9ba37c22bb Splitting username on pipe. 2022-07-12 15:18:59 -04:00
alwayscompiling 8eeda14119 Strip leading 0 and : from scores. 2022-07-12 15:15:37 -04:00
alwayscompiling 115e3dd3d0 Shortening the pad size for messages a little. 2022-07-10 21:59:11 -04:00
alwayscompiling 9ef2acf34c Capping user names to 12 characters because runescape names. Will need to revisit user name limit later. 2022-07-10 21:51:19 -04:00
alwayscompiling 82f47ac136 Temporary attempt. Makes assumption that a user will submit a better score. 2022-07-10 21:47:01 -04:00
alwayscompiling 4a15c63e8c Corrected some comments. 2022-07-10 21:11:51 -04:00
alwayscompiling 4c6279b444 Made post command admin only. 2022-07-10 21:08:30 -04:00
alwayscompiling 39f4a108ca Fixing hardmode category searching. 2022-07-10 20:33:39 -04:00
alwayscompiling 83f0bf6498 Slight time score bug fixes. 2022-07-10 20:27:56 -04:00
alwayscompiling 3f5ebdf175 Changed listener check to be more efficient. 2022-07-10 20:16:52 -04:00
alwayscompiling f98c80d200 Updated comments in message listener. 2022-07-10 20:09:27 -04:00
alwayscompiling b61008518c Moved message listener to a cog. Listener is set up on bot run. Listener listens to all messages. Pulls score and image from a message sent in submission channel if a user has an active submission. 2022-07-10 20:05:20 -04:00
alwayscompiling 198d9008b9 Fixed missing parameter. 2022-07-09 19:46:50 -04:00
alwayscompiling ae1d820297 Refactored register command to be one command that takes in parameter. 2022-07-09 14:21:45 -04:00
alwayscompiling a602347d09 Removing TODOs. 2022-07-09 13:57:48 -04:00
alwayscompiling 03ed723f5c Add input verification in verification_view.py. 2022-07-09 13:53:23 -04:00
alwayscompiling 18db48a55e Sending to verification view. 2022-07-09 11:43:49 -04:00
alwayscompiling 7854be890f Entire submission process works somehow. 2022-07-09 11:24:29 -04:00
alwayscompiling 6a7207d710 Slight organization changes. Decided to use new message for score input. 2022-07-07 22:57:38 -04:00
alwayscompiling f9800677cc Got response of dropdown. 2022-07-06 22:16:22 -04:00
alwayscompiling 3d16fe81e5 First dropdown works. 2022-07-06 21:37:46 -04:00
alwayscompiling c4261fc7f4 Changed where submission views live. 2022-07-06 21:05:04 -04:00
alwayscompiling d0428afb3f Add example config, ignore working config. 2022-07-06 20:39:59 -04:00
alwayscompiling a92f790a5f Adding new boss. 2022-07-04 10:15:01 -04:00
alwayscompiling 22d368f0a8 Adding submission button for all highscore messages. 2022-07-04 10:13:16 -04:00
alwayscompiling 84cf3781ee Adding map to track message id to boss. 2022-07-03 23:01:24 -04:00
alwayscompiling 8a20c1f50a Solved message deletion. 2022-07-03 22:34:16 -04:00
alwayscompiling a8aad570d3 Add logging message for admins, trying to move message with buttons. 2022-07-03 17:29:39 -04:00
alwayscompiling 02ba90134e Removing some local variables. 2022-07-03 17:13:23 -04:00
alwayscompiling 0834634cde Verification button working. 2022-07-03 17:11:06 -04:00
alwayscompiling 445c77b012 Renamed utils folder because import shenanigans with module with same name. 2022-07-03 16:51:54 -04:00
alwayscompiling b3af09f1c6 Changed utils to data_storage where I didn't need to. 2022-07-03 16:39:43 -04:00
alwayscompiling dc0fd694d3 Fix circular dependencies after refractor. 2022-07-03 16:38:22 -04:00
alwayscompiling 6d41baef98 Adding missed utils refactor. 2022-07-03 16:32:51 -04:00
alwayscompiling f29e459b2e Refractor utils to one file. 2022-07-03 16:12:15 -04:00
alwayscompiling 35aa1f34f4 Merge branch 'score-verification' of github.com:alwayscompiling/pvm-highscores into score-verification 2022-07-03 16:09:26 -04:00
alwayscompiling c046fa2c45 Using asserts for error catching. 2022-07-03 13:33:13 -04:00
alwayscompiling ffeb8119f7 Verification message is sent. 2022-07-03 11:46:07 -04:00
alwayscompiling a94ccdc425 Adding verification channel register command. 2022-07-03 11:21:06 -04:00
alwayscompiling 04faff3bdb Adding verification channel register command. 2022-07-03 11:20:08 -04:00
alwayscompiling cfde28ad4b Removing errors in help_command.py. 2022-07-03 10:47:06 -04:00
alwayscompiling f23cd71647 Editing information that would be posted from help command. 2022-07-03 10:35:08 -04:00
alwayscompiling 75fdef6388 Adding premade help command. 2022-07-03 10:29:18 -04:00
alwayscompiling b1b22fb595 Changing message to be a code block for monospace. Easier formatting. 2022-07-03 10:13:58 -04:00
25 changed files with 2349 additions and 475 deletions
+4 -1
View File
@@ -131,4 +131,7 @@ dmypy.json
nextcord.log nextcord.log
# data file for bot # data file for bot
highscores_data.json highscores_data.json
highscores_config.json
deleted_backups/
highscores_data/
+190
View File
@@ -0,0 +1,190 @@
"""File for Category class."""
import json
from classes.score import Score, ScoreTime # pylint: disable=import-error
class Category:
"""Stores a category's information and relative scores."""
def __init__(self, name: str) -> None:
"""
Default constructor for Category class. Creates empty category and populates with defaults.
Args:
name (str): Category's name
"""
self._name: str = name
self._ascending: bool = False
self._time_record: bool = False
self._scores_map: dict = {}
def __str__(self) -> str:
"""
Returns Category class as a string for printing.
Returns:
str: Category class as a string
"""
return self.to_str()
@classmethod
def from_dict(cls, name: str, data: dict):
"""
Creates a Category class from the passed data.
Args:
name (str): Category name
data (dict): Rest of Category data in dictionary format
Returns:
Category: Populated Category object
"""
category = cls(name)
category.ascending = data["ascending"]
category.time_record = data["time_record"]
for user, score in data["scores"].items():
if category.time_record:
score_obj: ScoreTime = ScoreTime(user)
score_obj.value = float(score)
category.add_score(score_obj)
else:
score_obj: Score = Score(user)
score_obj.value = int(score)
category.add_score(score_obj)
return category
@property
def ascending(self) -> bool:
"""
Getter for is ascending.
Returns:
bool: is ascending
"""
return self._ascending
@ascending.setter
def ascending(self, ascending: bool) -> None:
"""
Setter for is ascending.
Args:
ascending (bool): is ascending
"""
self._ascending = ascending
@property
def name(self) -> str:
"""
Getter for name.
Returns:
str: name
"""
return self._name
@property
def time_record(self) -> bool:
"""
Getter for is time record.
Returns:
bool: is time record
"""
return self._time_record
@time_record.setter
def time_record(self, time_record: bool) -> None:
"""
Setter for is time record.
Args:
time_record (bool): is time record
"""
self._time_record = time_record
@property
def scores(self) -> "list[Score]":
"""
Getter for Scores list.
Returns:
list[Score]: List of Scores in this Category
"""
return self._scores_map.values()
def add_score(self, score: Score) -> None:
"""
Adds score to dictionary storage map. Sorts dictionary by score value.
Args:
score (Score): Score to be added
"""
if score.user in self._scores_map:
self._scores_map[score.user].value = score.value
else:
self._scores_map[score.user] = score
self._scores_map = dict(
sorted(self._scores_map.items(), key=lambda x: x[1].value, reverse=not self.ascending)
)
def clear(self) -> None:
"""
Clears all Scores tracked in score map.
This is a test function and shouldn't really be used.
"""
self._scores_map.clear()
def get_user_score(self, user: str) -> Score:
"""
Gets a user's score. Returns None if user is not found.
Args:
user (str): Name of score to find
Returns:
Score: User's Score or None
"""
if user in self._scores_map:
return self._scores_map[user]
return None
def remove_score(self, score: str) -> None:
"""
Removes a user's score from dictionary storage.
Args:
score (str): user whose score to remove
"""
score = self._scores_map.pop(score)
del score
def to_dict(self) -> dict:
"""
Saves category data as a python dictionary.
Does not include category name. It should be the key of above data structure.
Returns:
dict: Category class in dictionary format
"""
data = {}
data["ascending"] = self.ascending
data["time_record"] = self.time_record
data["scores"] = {}
for score in self.scores:
data["scores"][score.user] = score.value
return data
def to_str(self) -> str:
"""
Returns Category object as a string.
Returns:
str: string of json representation of Category
"""
return json.dumps(self.to_dict(), indent=4)
+211
View File
@@ -0,0 +1,211 @@
"""File for Guild class."""
import json
from classes.table import Table # pylint: disable=import-error
class Guild:
"""Stores a guild's information."""
def __init__(self, guild_id: int) -> None:
"""
Default constructor for Guild class. Initializes channel ids as 0 as they won't be
registered yet. Sets default username length of 12. Default highscores size to 3.
Args:
guild_id (int): Discord Guild id of new Guild
"""
self._guild_id: int = guild_id
self._highscore_channel_id: int = 0
self._submission_channel_id: int = 0
self._table_map: dict = {}
self._username_length: int = 12
self._verification_channel_id: int = 0
def __str__(self) -> str:
"""
Returns Guild class as a string for printing.
Returns:
str: Guild class as a string
"""
return self.to_str()
@classmethod
def from_dict(cls, guild_id: int, data: dict):
"""
Creates a Guild class from the passed data.
Args:
guild_id (int): Guild's id
data (dict): Rest of Guild data in dictionary format
Returns:
Guild: Populated Guild object
"""
guild = cls(guild_id)
guild.highscore_channel = data["highscore_channel_id"]
guild.submission_channel = data["submission_channel_id"]
guild.username_length = data["username_length"]
guild.verification_channel = data["verification_channel_id"]
for table_name, table_data in data["tables"].items():
table = Table.from_dict(table_name, table_data)
guild.add_table(table)
return guild
@property
def guild_id(self) -> int:
"""
Getter for Guild id
Returns:
int: Guild id
"""
return self._guild_id
@property
def highscore_channel(self) -> int:
"""
Getter for highscore channel id.
Returns:
int: highscore channel id
"""
return self._highscore_channel_id
@highscore_channel.setter
def highscore_channel(self, channel: int) -> None:
"""
Setter for highscore channel id.
Args:
channel (int): highscore channel id
"""
self._highscore_channel_id = channel
@property
def submission_channel(self) -> int:
"""
Getter for submission channel id.
Returns:
int: submission channel id
"""
return self._submission_channel_id
@submission_channel.setter
def submission_channel(self, channel: id) -> None:
"""
Setter fro submission channel id.
Args:
channel (id): submission channel id
"""
self._submission_channel_id = channel
@property
def tables(self) -> "list[Table]":
"""
Getter for Tables list.
Returns:
list[Table]: List of Tables in this Guild
"""
return self._table_map.values()
@property
def username_length(self) -> int:
"""
Getter for username length.
Returns:
int: username length
"""
return self._username_length
@username_length.setter
def username_length(self, length: int) -> None:
"""
Setter for username length.
Args:
length (int): username length
"""
self._username_length = length
@property
def verification_channel(self) -> int:
"""
Getter for verification channel id.
Returns:
int: verification channel id
"""
return self._verification_channel_id
@verification_channel.setter
def verification_channel(self, channel: int) -> None:
"""
Setter for verification channel id
Args:
channel (int): verification channel id
"""
self._verification_channel_id = channel
def add_table(self, table: Table) -> None:
"""
Adds a Table to table map. Shouldn't exist already.
Args:
table (Table): new Table to add
"""
if table.name not in self._table_map:
self._table_map[table.name] = table
def get_table(self, table_name: str) -> Table:
"""
Getter for specific Table. Returns None if Table is not found.
Args:
table_name (str): Name of Table to find
Returns:
Table: Table corresponding to Name. None if not found.
"""
if table_name in self._table_map:
return self._table_map[table_name]
return None
def to_dict(self) -> dict:
"""
Creates python dictionary object from class data.
Does not include guild_id in dict, it should be the key of above data structure.
Returns:
dict: Guild class in dictionary format
"""
data: dict = {}
data["highscore_channel_id"] = self.highscore_channel
data["submission_channel_id"] = self.submission_channel
data["tables"] = {}
data["username_length"] = self.username_length
data["verification_channel_id"] = self.verification_channel
for table in self.tables:
data["tables"][table.name] = table.to_dict()
return data
def to_str(self) -> str:
"""
Returns Guild object as a string.
Returns:
str: string of json representation of Guild
"""
return json.dumps(self.to_dict(), indent=4)
+153
View File
@@ -0,0 +1,153 @@
"""Class and functions for storing and retrieving highscores data."""
import os
import json
from classes.guild import Guild # pylint: disable=import-error
from classes.table import Table # pylint: disable=import-error
from classes.category import Category # pylint: disable=import-error
HIGHSCORES_BACKUP_DIR = "deleted_backups"
HIGHSCORES_CONFIG_DIR = "example_config"
HIGHSCORES_DATA_DIR = "highscores_data"
class HighscoresData:
"""Class for storing and retrieving highscores data."""
def __init__(self) -> None:
"""
Default constructor for Highscores Data. Opens up known highscores data on creation.
"""
self._guild_map: dict = {}
# Ensure that data directories exist.
if not os.path.exists(HIGHSCORES_BACKUP_DIR):
os.mkdir(HIGHSCORES_BACKUP_DIR)
if not os.path.exists(HIGHSCORES_DATA_DIR):
os.mkdir(HIGHSCORES_DATA_DIR)
self.open_highscores_data()
def __str__(self) -> str:
"""
Returns Highscores class as a string for printing.
Returns:
str: Highscores class as a string
"""
data: dict = {}
for guild_id, guild_obj in self._guild_map.items():
data[guild_id] = guild_obj.to_dict()
return "Highscores Data\n" + json.dumps(data, indent=4)
@property
def guilds(self) -> "list[Guild]":
"""
Getter for Guilds list.
Returns:
list[Guild]: List of Guilds tracked by Highscores Bot
"""
return self._guild_map.values()
def add_configuration(self, guild_id: int, config_str: str) -> None:
"""
Adds an example configuration to a Guild. Uses guild id to know which guild and
config name to open a file in example_config.
Args:
guild_id (int): Guild id to add config to
config_str (str): Config name to add
"""
config = self._open_highscores_config(config_str)
guild = self.get_guild(guild_id)
for table_name, categorys in config["tables"].items():
table_obj = Table(table_name)
table_obj.size = config["table_size"]
for category_name in categorys:
category = Category(category_name)
category.ascending = config["categories"][category.name]["ascending"]
category.time_record = config["categories"][category.name]["is_time_record"]
table_obj.add_category(category)
guild.add_table(table_obj)
def add_guild(self, guild_id: int) -> None:
"""
Creates a new Guild and adds to Guild map.
Args:
guild_id (int): Guild Id to add
"""
guild: Guild = Guild(guild_id)
self._guild_map[guild_id] = guild
def get_guild(self, guild_id: int) -> Guild:
"""
Getter for Guild object that corresponds with the guild_id.
Args:
guild_id (int): id of Guild to retrieve.
Returns:
Guild: Guild object for guild_id
"""
return self._guild_map[guild_id]
def remove_guild(self, guild_id: int) -> None:
"""
Removes a guild's data from object. Saves backup.
Args:
guild_id (int): guild id to remove
"""
self._guild_map.pop(guild_id)
os.rename(
f"{HIGHSCORES_DATA_DIR}/{guild_id}.json", f"{HIGHSCORES_BACKUP_DIR}/{guild_id}.json"
)
def _open_highscores_config(self, config: str) -> dict:
"""
Opens the highscores config file and returns in dictionary format.
Will be replaced when config class is created.
Args:
config (str): Name of config to load.
Returns:
dict: dict of highscores config
"""
with open(f"{HIGHSCORES_CONFIG_DIR}/{config}.json", encoding="utf-8", mode="r") as file:
return json.load(file)
def open_highscores_data(self):
"""
Opens all saved highscores data in the HIGHSCORES_DATA_DIR dir. Creates a Guild object from
each file and adds to guild map.
"""
for data_file in os.listdir(HIGHSCORES_DATA_DIR):
guild_id: int = int(data_file.split(".", maxsplit=1)[0])
with open(f"{HIGHSCORES_DATA_DIR}/{data_file}", encoding="utf-8", mode="r") as file:
guild_obj: Guild = Guild.from_dict(guild_id, json.load(file))
self._guild_map[guild_obj.guild_id] = guild_obj
def save_all_data(self) -> None:
"""
Calls save_guild_data for each guild tracked in guild map.
"""
for guild in self.guilds:
self.save_guild_data(guild)
def save_guild_data(self, guild: Guild) -> None:
"""
Saves a guild to its own json file.
Args:
guild (Guild): Guild to save
"""
with open(
f"{HIGHSCORES_DATA_DIR}/{guild.guild_id}.json", encoding="utf-8", mode="w"
) as file:
file.write(guild.to_str()) # should use str function
+144
View File
@@ -0,0 +1,144 @@
"""File for Score classes"""
import time
class Score:
"""Parent class for Scores. Stores common information for a Score."""
# no user setter, it should never change.
def __init__(self, user: str) -> None:
"""
Default constructor for Score class.
Args:
user (str): Score's user
value (int): Score's value
"""
self._user: str = user
self._value: int = -1
def __str__(self) -> str:
"""
Returns Score class as a string for printing.
Returns:
str: Score class as a string
"""
return self.to_str()
@property
def user(self) -> str:
"""
Getter for score user.
Returns:
str: user
"""
return self._user
@property
def value(self) -> int:
"""
Getter for score value.
Returns:
int: value
"""
return self._value
@value.setter
def value(self, value: int) -> None:
"""
Setter for score value.
Args:
value (int): score value input
"""
self._value = value
def get_str(self) -> str:
"""
Gets score's value as string.
Returns:
str: Score's value
"""
return str(self._value)
def set_value_str(self, value: str) -> None:
"""
Setter for score value from string.
Args:
value (str): int as a string
"""
self._value = int(value)
def to_str(self) -> str:
"""
Returns Score object as a string.
Returns:
str: string of json representation of Score
"""
return f"{self.user}: {self.get_str()}"
class ScoreTime(Score):
"""Class for a time based score. Inherits from Score."""
def __init__(self, user: str) -> None: # pylint: disable=super-init-not-called
"""
Default constructor for ScoreTime class.
Args:
user (str): Score's user
score (float): Score's value
"""
# store time score as seconds.
self._user: str = user
self._score: float = -1.0
@property
def value(self) -> float:
"""
Getter for score value.
Returns:
float: value
"""
return self._score
@value.setter
def value(self, value: float) -> None:
"""
Setter for score value.
Args:
value (float): value
"""
self._score = value
def get_str(self) -> str:
"""
Gets score's value as string. Turns seconds into MM:SS.ms format.
Returns:
str: value in time format.
"""
milliseconds = round((self.value - int(self.value)) * 10)
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(self.value))
def set_value_str(self, value: str) -> None:
"""
Setter for score value from a string.
Args:
value (str): string representation of time
"""
if value.find(":"):
self._score = int(value.split(":")[0]) * 60 + float(value.split(":")[1])
else:
self._score = float(value)
+256
View File
@@ -0,0 +1,256 @@
"""
Contains classes and enum for tracking data on submissions.
"""
from enum import Enum
from nextcord import Embed
class SubmissionState(Enum):
"""
Enum for which state a submission is in
"""
CATEGORY = 1
SCORE = 2
PROOF = 3
NAME = 4
SUBMIT = 5
VERIFY = 6
class Submission:
"""
Class for an individual submission. Contains submission information such as
guild id, message id, table, category, username, score and submission state.
"""
def __init__(self, guild_id: int, table: str, username: str) -> None:
"""
Default constructor for Submission. Just need to know guild_id, table, and username.
Score and category are defaulted at empty, message id is 0, and submission state
is SubmissionState.CATEGORY.
"""
self._category: str = ""
self._guild_id: int = guild_id
self._message_id: int = 0
self._score: str = ""
self._submission_state: SubmissionState = SubmissionState.CATEGORY
self._table: str = table
self._username: str = username
@property
def category(self) -> str:
"""
Getter for Submission's Category name.
Returns:
str: Category name.
"""
return self._category
@category.setter
def category(self, category: str) -> None:
"""
Setter for Submission's Category name.
Args:
category (str): Category name
"""
self._category = category
@property
def guild_id(self) -> int:
"""
Getter for guild id Submission is happening in. Will not change.
Returns:
int: guild id
"""
return self._guild_id
@property
def message_id(self) -> int:
"""
Getter for message id that Submission has a view on.
Returns:
int: message id
"""
return self._message_id
@message_id.setter
def message_id(self, m_id: int) -> None:
"""
Setter for message id that Submission has a view on.
Args:
m_id (int): message id
"""
self._message_id = m_id
@property
def score(self) -> str:
"""
Getter for score.
Returns:
str: score
"""
return self._score
@score.setter
def score(self, score: str) -> None:
"""
Setter for score.
Args:
score (str): score
"""
self._score = score
@property
def submission_state(self) -> SubmissionState:
"""
Getter for state Submission is in.
Returns:
SubmissionState: State the Submission is in
"""
return self._submission_state
@submission_state.setter
def submission_state(self, state: SubmissionState) -> None:
"""
Setter for state Submission is in.
Args:
state (SubmissionState): State to change to
"""
self._submission_state = state
@property
def table(self) -> str:
"""
Getter for table Submission is for.
Returns:
str: table name
"""
return self._table
@table.setter
def table(self, table: str) -> None:
"""
Getter for table Submission is for.
Args:
table (str): table name
"""
self._table = table
@property
def username(self) -> str:
"""
Getter for Submission owner's username.
Returns:
str: Submission owner's username
"""
return self._username
@username.setter
def username(self, username: str) -> None:
"""
Setter for Submission owner's username.
Args:
username (str): Submission owner's username
"""
self._username = username
def get_embed(self) -> Embed:
"""Creates an embed based off of the submission state and fields filled out."""
state: SubmissionState = self.submission_state
if state is SubmissionState.CATEGORY:
description = "Step 1: Choose your category!"
elif state is SubmissionState.SCORE:
description = "Step 2: Input your score!"
elif state is SubmissionState.PROOF:
description = "Step 3: Submit your proof!"
elif state is SubmissionState.NAME:
description = (
"Step 4 (Optional): Change name on submission! (proceed to next if skipping)"
)
elif state is SubmissionState.SUBMIT:
description = "Step 5: Submit if everything looks correct!"
elif state is SubmissionState.VERIFY:
description = "Review fields and accept."
embed = Embed(title="Highscores Submission", description=description)
embed.add_field(name="User Name", value=self.username, inline=False)
embed.add_field(name="Table", value=self.table, inline=False)
if self.category:
embed.add_field(name="Category", value=self.category, inline=False)
if self.score:
embed.add_field(name="Score", value=self.score, inline=False)
return embed
class SubmissionMap:
"""
Tracks all active submission.
"""
def __init__(self) -> None:
"""
Default constructor for SubmissionMap. Created empty map.
"""
self._submission_map: dict = {}
def add_submission(self, user_id: int, submission: Submission) -> None:
"""
Adds new submission to submission map. User id is used as a key, a user should only
have one submission at a time.
Args:
user_id (int): User id of submitter. Unique, used as key.
submission (Submission): Submission object
"""
self._submission_map[user_id] = submission
def get_user_submission(self, user_id: int) -> Submission:
"""
Getter for a user's Submission.
Args:
user_id (int): user id
Returns:
Submission: user's submission
"""
return self._submission_map[user_id]
def has_user(self, user_id: int) -> bool:
"""
Tells if user has an active submission in submission map.
Args:
user_id (int): user id
Returns:
bool: bool indicating if user id is in submission map
"""
return user_id in self._submission_map
def remove_user_submission(self, user_id: int) -> None:
"""
Removes a user's Submission. Use when Submission is complete.
Args:
user_id (int): user id to identify user's Submission
"""
self._submission_map.pop(user_id)
+164
View File
@@ -0,0 +1,164 @@
"""File for Table class."""
import json
from classes.category import Category # pylint: disable=import-error
class Table:
"""Stores a table's information a list of categories."""
def __init__(self, name: str) -> None:
"""
Default constructor for Table class.
Creates object and fills out class variables with defaults.
Args:
name (str): Table's name
"""
self._name: str = name
self._message_id: int = 0
self._categories_map: dict = {}
self._size = 3
def __str__(self) -> str:
"""
Returns Table class as a string for printing.
Returns:
str: Table class as a string
"""
return self.to_str()
@classmethod
def from_dict(cls, table_name: str, data: dict):
"""
Creates a Table class from the passed data.
Args:
table_name (str): Table name
data (dict): Rest of Table data in dictionary format
Returns:
Table: Populated Table object
"""
table = cls(table_name)
table.message_id = data["message_id"]
table.size = data["table_size"]
for cat_name, cat in data["categories"].items():
category = Category.from_dict(cat_name, cat)
table.add_category(category)
return table
@property
def categories(self) -> "list[Category]":
"""
Getter for Categories list.
Returns:
list[Category]: List of Categories in this Table
"""
return self._categories_map.values()
@property
def message_id(self) -> int:
"""
Getter for message id
Returns:
int: message id
"""
return self._message_id
@message_id.setter
def message_id(self, m_id: int) -> None:
"""
Setter for message id.
Args:
m_id (int): message id
"""
self._message_id = m_id
@property
def name(self) -> str:
"""
Getter for name.
Returns:
str: name
"""
return self._name
@property
def size(self) -> int:
"""
Getter for size of table.
Returns:
int: Size of table
"""
return self._size
@size.setter
def size(self, size: int) -> None:
"""
Setter for size of table.
Args:
size (int): New size of table
"""
self._size = size
def add_category(self, category: Category) -> None:
"""
Adds a Category to categories map. Shouldn't exist already.
Args:
category (Category): New Category to add
"""
if category.name not in self._categories_map:
self._categories_map[category.name] = category
def get_category(self, category_name: str) -> Category:
"""
Getter for specific category. Returns None if not found.
Args:
category_name (str): name of category to return
Returns:
Category: Category corresponding to name. None if not found.
"""
if category_name in self._categories_map:
return self._categories_map[category_name]
return None
def to_dict(self) -> dict:
"""
Saves Table as a python dictionary.
Does not include table name. It should be the key of above data structure.
Returns:
dict: Table class in dictionary format
"""
data = {}
data["message_id"] = self.message_id
data["table_size"] = self.size
data["categories"] = {}
for category in self.categories:
data["categories"][category.name] = category.to_dict()
return data
def to_str(self) -> str:
"""
Returns Table object as a string.
Returns:
str: string of json representation of Table
"""
return json.dumps(self.to_dict(), indent=4)
+51
View File
@@ -0,0 +1,51 @@
"""
Bot command for deleting the guild's scores.
"""
from nextcord.ext import commands
import nextcord
from highscores import highscores_data # pylint: disable=import-error
class DeleteScores(commands.Cog, name="Delete Scores"):
"""
Warning: This command only works for developers.
Deletes this guild's scores.
"""
def __init__(self):
"""
Default constructor for DeleteScores class. Does nothing.
"""
@nextcord.slash_command(
description="Deletes all scores for this guild. Used for testing.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def delete_scores(self, interaction: nextcord.Interaction):
"""
A command which deletes all scores in all tables for this guild.
Command limits to admins to prevent devs from usings on non-test servers.
"""
# only allow AlwaysCompiling to use.
# If you are a developer and want to use command, can add name to list
if interaction.user.id == 206602713511886849:
for table in highscores_data.get_guild(interaction.guild_id).tables:
for category in table.categories:
category.clear()
highscores_data.save_guild_data(highscores_data.get_guild(interaction.guild_id))
await interaction.response.send_message(
f"Cleared score data for {interaction.guild.name}.", ephemeral=True
)
else:
await interaction.response.send_message(
"You shouldn't be using this message.", ephemeral=True
)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(DeleteScores())
+43
View File
@@ -0,0 +1,43 @@
"""
Defines cog for guild listeners. Contains functions for various guild events.
"""
from nextcord.ext import commands
import nextcord
from highscores import highscores_data # pylint: disable=import-error
class GuildListener(commands.Cog, name="Guild Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild):
"""Listener for Guild Join. Creates skeleton for new guild joined and saves it."""
highscores_data.add_guild(guild.id)
# This add config will be removed eventually. Config adding will have to be controlled by commands.
highscores_data.add_configuration(guild.id, "runescape")
highscores_data.save_guild_data(highscores_data.get_guild(guild.id))
welcome_message: str = "Hello, I keep track of Highscores for you!"
welcome_message += " Currently only supporting RuneScape boss highscores,"
welcome_message += " but will be more dynamic support in future."
welcome_message += ' For assistance on getting started, use "?register help".'
await guild.system_channel.send(content=welcome_message)
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild):
"""Listener for Guild Remove. Removes data from highscores_data."""
print(f"Removing data for guild {guild.name} (id {guild.id}).")
highscores_data.remove_guild(guild.id)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(GuildListener(bot))
+115
View File
@@ -0,0 +1,115 @@
"""
Defines cog for on message listener.
"""
from datetime import date
import random
import re
from nextcord.ext import commands
import nextcord
from classes.submission import SubmissionState # pylint: disable=import-error
from highscores import highscores_data, submission_objects # pylint: disable=import-error
class SubmissionListener(commands.Cog, name="Message Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: nextcord.Message):
"""Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot.
if message.author.id == self.bot.user.id:
return
guild_id = message.guild.id
# troll addition for friends birthday
if guild_id == 239758371358638080 and date.today() == date(2022, 7, 31):
# 5% chance to send the message
chance = random.randint(1, 20)
if chance == 1:
await message.channel.send("Happy Birthday Chris <@642330132958281750>")
if submission_objects.has_user(message.author.id):
submission = submission_objects.get_user_submission(message.author.id)
message_id = submission.message_id
submission_channel_id = highscores_data.get_guild(guild_id).submission_channel
submission_message: nextcord.Message = (
await self.bot.get_guild(guild_id)
.get_channel(submission_channel_id)
.fetch_message(message_id)
)
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission.submission_state
if state == SubmissionState.SCORE and message.content:
# check with regex if score matches correctly
is_time_record = (
highscores_data.get_guild(guild_id)
.get_table(submission.table)
.get_category(submission.category)
.time_record
)
time_regex = re.search(
"^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content
)
int_regex = re.search("^[0-9]+$", message.content)
if time_regex is None and is_time_record:
embed = submission_message.embeds[0]
embed.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
)
elif int_regex is None and not is_time_record:
embed = submission_message.embeds[0]
embed.add_field(name="Error", value="Number record must only be number.")
else:
score = message.content
if is_time_record:
# standardize time score format.
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# score is already fine.
pass
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
# need to add leading colon and 0
score = "0:" + score
elif re.search("^[0-9](\\.[0-9])?$", score):
# need to add 0 in seconds and leading colon and 0
score = "0:0" + score
elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove
score = score.lstrip("0")
submission.score = score
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission.username = message.content[
: highscores_data.get_guild(guild_id).username_length
]
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
await message.delete()
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(SubmissionListener(bot))
+47 -25
View File
@@ -3,44 +3,66 @@ Bot command to post the highscores
""" """
from nextcord.ext import commands from nextcord.ext import commands
from highscores import highscores_config # pylint: disable=import-error import nextcord
from cogs.submission.submission_views import SubmissionCreateButton # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error from highscores import highscores_data # pylint: disable=import-error
from utils import data_storage # pylint: disable=import-error from utilities.utils import send_highscore_message # pylint: disable=import-error
from utils import highscore_message # pylint: disable=import-error
class PostHighscores(commands.Cog, name="Post Highscores"): class PostHighscores(commands.Cog, name="Post Highscores"):
"""Actions of bot command""" """Posts Highscores."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for PostHighscores class
Args:
bot (commands.Bot): Bot object. Required to register submission button view.
"""
self.bot = bot self.bot = bot
@commands.command(name="post-scores") @nextcord.slash_command(
async def post_highscores(self, ctx: commands.Context): description="Triggers post/update of highscores to registered highscore channel.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def post_highscore_tables(self, interaction: nextcord.Interaction) -> None:
""" """
A command which posts the highscore in the designated channel. Sends warning if no channel A command which posts the highscore in the registered channel.
has been registered. Sends warning if no channel has been registered.
Usage:
```
?post-scores
```
""" """
channel_id = highscores_data["channel_id"] guild = highscores_data.get_guild(interaction.guild_id)
print(f"Printing out highscores information in channel {channel_id}") print(f"Printing out highscores information in channel {guild.highscore_channel}")
channel = self.bot.get_channel(channel_id) channel = interaction.guild.get_channel(guild.highscore_channel)
if channel is None:
response = 'Registered Highscores channel does not exist or was never registered. \
Register with "?register" command.'
await ctx.send(response)
return
for boss in highscores_config["bosses"]:
await highscore_message.send_highscore_message(channel, boss["boss"])
data_storage.save_highscores_data(highscores_data) # Error checking for registered highscore channel
error_response = "Registered Highscores channel does not exist or was never registered. "
error_response += 'Use "/register" to register channels.'
assert channel is not None, await interaction.response(error_response, ephemeral=True)
# Error checking for tables existing
# TODO fill out response string once functionality created.
error_response = "No tables have been created for this guild. Please create some tables."
assert (
len(highscores_data.get_guild(interaction.guild_id).tables) > 0
), await interaction.response(error_response, ephemeral=True)
# awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True)
for table in highscores_data.get_guild(interaction.guild_id).tables:
message: nextcord.Message = await send_highscore_message(channel, guild, table)
await message.edit(view=SubmissionCreateButton(self.bot))
# have to save data here in case message id was changed. rather save here after all
# messages have been sent.
highscores_data.save_guild_data(guild)
await interaction.followup.send(f"Posted all tables to <#{channel.id}>.", ephemeral=True)
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
"""Adds functon to bot""" """Adds function to bot"""
bot.add_cog(PostHighscores(bot)) bot.add_cog(PostHighscores(bot))
+56
View File
@@ -0,0 +1,56 @@
"""
Functions for controlling the bot's presence.
"""
import random
from nextcord.ext import commands
from nextcord.ext import tasks
import nextcord
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=2)
async def periodic_change_presence(self):
"""Loops every 2 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
"Now with slash commands!",
"Vuln Pls.",
"All your base are belong to us.",
"Double plus good.",
"That's a lot of damage!",
]
status = random.choice(statuses)
status += f" | {len(self.bot.guilds)} servers"
await self.bot.change_presence(activity=nextcord.Game(status))
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(BotPresence(bot))
+114
View File
@@ -0,0 +1,114 @@
"""
Bot command for registering various channels to the bot.
"""
from nextcord.ext import commands
import nextcord
from highscores import highscores_data # pylint: disable=import-error
class Register(commands.Cog, name="Register"):
"""
Class holds various register commands.
"""
def __init__(self) -> None:
"""
Default constructor for Register class. Does nothing.
"""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def register(self):
"""
Parent slash command, never called.
"""
@register.subcommand(description="Registers the channel for perminent highscores messages.")
async def highscore(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Command function to register the highscore printing channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (nextcord.abc.GuildChannel, optional): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None.
"""
if not channel:
channel = interaction.channel
channel_id = channel.id
guild = highscores_data.get_guild(interaction.guild_id)
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
guild.highscore_channel = channel_id
highscores_data.save_guild_data(guild)
@register.subcommand(description="Registers the channel for submission messages.")
async def submission(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Command function to register the submission message channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (nextcord.abc.GuildChannel, optional): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None.
"""
if not channel:
channel = interaction.channel
channel_id = channel.id
guild = highscores_data.get_guild(interaction.guild_id)
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
guild.submission_channel = channel_id
highscores_data.save_guild_data(guild)
@register.subcommand(description="Registers the channel for verification messages.")
async def verify(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Command function to register the verification message channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (nextcord.abc.GuildChannel, optional): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None.
"""
if not channel:
channel = interaction.channel
channel_id = channel.id
guild = highscores_data.get_guild(interaction.guild_id)
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
guild.verification_channel = channel_id
highscores_data.save_guild_data(guild)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot) -> None:
"""Adds function to bot"""
bot.add_cog(Register())
@@ -1,35 +0,0 @@
"""
Bot command to register the highscores channel
"""
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from utils import data_storage # pylint: disable=import-error
class RegisterHighscoreChannel(commands.Cog, name="Register HighScore Channel"):
"""Actions of bot command"""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="register")
async def register_highscores(self, ctx: commands.Context):
"""A command which registers the channel command is sent in as channel to post highscores in
Usage:
```
?register (in channel to post highscores in)
```
"""
print(f"Registering {ctx.channel.id}.")
# respond to the message
await ctx.send(f"Registered <#{ctx.channel.id}> as channel to post highscores in.")
highscores_data["channel_id"] = ctx.channel.id
data_storage.save_highscores_data(highscores_data)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds functon to bot"""
bot.add_cog(RegisterHighscoreChannel(bot))
-33
View File
@@ -1,33 +0,0 @@
"""
Bot command to submit a score
"""
from nextcord.ext import commands
from utils import highscore_manage # pylint: disable=import-error
# from utils import highscore_manage # pylint: disable=import-error
class RegisterHighscoreChannel(commands.Cog, name="Score"):
"""Actions of bot command"""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="score")
async def score(self, ctx: commands.Context, boss_name: str, category: str, score: str):
"""A command which calls a function to submit the score to highscores. Gathers required
information from command and passes on.
Usage:
```
?score "Boss Name" "Category" "score"
```
"""
await highscore_manage.submit_score(self, ctx, boss_name, category, score)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds functon to bot"""
bot.add_cog(RegisterHighscoreChannel(bot))
+28
View File
@@ -0,0 +1,28 @@
"""
Cog for creating submission process.
"""
from nextcord.ext import commands
from .submission_views import SubmissionCreateButton
from .verification_view import VerificationView
class SubmissionCog(commands.Cog, name="Submission Cog"):
"""Constructs submission method."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""Called when cog is loaded"""
self.bot.add_view(SubmissionCreateButton(self.bot))
self.bot.add_view(VerificationView())
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(SubmissionCog(bot))
+231
View File
@@ -0,0 +1,231 @@
"""Views used for submission process."""
from typing import Optional
import nextcord
from nextcord.ext import commands
from classes.submission import Submission, SubmissionState # pylint: disable=import-error
from highscores import ( # pylint: disable=import-error
highscores_data,
highscores_message_map,
submission_objects,
)
from .verification_view import VerificationView
def category_select_options(guild_id: int, boss_name: str) -> "list[nextcord.SelectOption]":
"""Returns list of categories for a given boss."""
options: "list[nextcord.SelectOption]" = []
category_list = highscores_data.get_guild(guild_id).get_table(boss_name).categories
for category in category_list:
options.append(nextcord.SelectOption(label=category.name))
return options
############ Items used in submission process ############
class SubmissionDropdown(nextcord.ui.Select):
"""Defines dropdown menu for submission"""
def __init__(
self,
bot: commands.Bot,
options: "list[nextcord.SelectOption]",
):
super().__init__(
placeholder="Choose submission category.", min_values=1, max_values=1, options=options
)
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id)
submission.category = self.values[0]
embed = submission.get_embed()
await interaction.message.edit(
embed=embed,
)
class BackButton(nextcord.ui.Button):
"""Defines view for back button."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Back", style=nextcord.ButtonStyle.primary, custom_id="back")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id)
state: SubmissionState = submission.submission_state
state = SubmissionState(state.value - 1)
submission.submission_state = state
embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class CancelButton(nextcord.ui.Button):
"""Deletes message to cancel interaction.."""
def __init__(self):
super().__init__(label="Cancel", style=nextcord.ButtonStyle.red, custom_id="cancel")
async def callback(self, interaction: nextcord.Interaction):
submission_objects.remove_user_submission(interaction.user.id)
await interaction.message.delete()
class NextButton(nextcord.ui.Button):
"""Defines view for next button."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Next", style=nextcord.ButtonStyle.primary, custom_id="next")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
# check if fields have been filled out for each state
submission = submission_objects.get_user_submission(interaction.user.id)
state: SubmissionState = submission.submission_state
if state == SubmissionState.CATEGORY and not submission.category:
await interaction.send("Please choose a category.", ephemeral=True)
elif state == SubmissionState.SCORE and not submission.score:
await interaction.send("Please send a score.", ephemeral=True)
elif state == SubmissionState.PROOF and not len(interaction.message.attachments) > 0:
await interaction.send("Please attach picture for proof.", ephemeral=True)
else:
state = SubmissionState(state.value + 1)
submission.submission_state = state
embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class SubmitButton(nextcord.ui.Button):
"""Defines button to submit a user's score."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Submit", style=nextcord.ButtonStyle.green, custom_id="submit")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
verification_channel_id = highscores_data.get_guild(
interaction.guild_id
).verification_channel
channel = interaction.guild.get_channel(verification_channel_id)
submission = submission_objects.get_user_submission(interaction.user.id)
error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins."
assert channel is not None, await channel.send(error_response)
submission.submission_state = SubmissionState.VERIFY
embed = submission.get_embed()
await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(),
embed=embed,
content=f"Submission from {interaction.user.display_name}",
)
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
submission_objects.remove_user_submission(interaction.user.id)
############ Submission Views ############
class SubmissionCreateButton(nextcord.ui.View):
"""Defines view for score submission button."""
def __init__(self, bot: commands.Bot):
super().__init__(timeout=None)
self._bot = bot
@nextcord.ui.button(
label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate"
)
async def submitcreate_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for score submission."""
# Don't allow a user to start a new submission if one already exists
if submission_objects.has_user(interaction.user.id):
await interaction.send("Please finish in progress submission first.", ephemeral=True)
return
table = highscores_message_map[interaction.guild.id][interaction.message.id]
channel = interaction.guild.get_channel(
highscores_data.get_guild(interaction.guild_id).submission_channel
)
# a user should only have 1 submission active at once.
# split username on pipe then limit length.
username: str = interaction.user.display_name.split("|")[0][
: highscores_data.get_guild(interaction.guild_id).username_length
]
submission: Submission = Submission(interaction.guild_id, table, username)
submission_objects.add_submission(interaction.user.id, submission)
embed = submission.get_embed()
submission_message = await channel.send(
f"<@{interaction.user.id}>",
view=SubmissionView(
self._bot,
interaction.user.id,
),
embed=embed,
)
# update the message_id once its created
submission.message_id = submission_message.id
class SubmissionView(nextcord.ui.View):
"""Defines view submitting. Adjusts to all states."""
def __init__(
self,
bot: commands.Bot,
submission_user: int,
*,
timeout: Optional[float] = 300,
):
super().__init__(timeout=timeout)
self._submission_user = submission_user
self._bot = bot
self._state = submission_objects.get_user_submission(self._submission_user).submission_state
self._boss_name = submission_objects.get_user_submission(self._submission_user).table
self._guild_id = submission_objects.get_user_submission(self._submission_user).guild_id
# add items depending on given state
if self._state is SubmissionState.CATEGORY:
self.add_item(
SubmissionDropdown(bot, category_select_options(self._guild_id, self._boss_name))
)
self.add_item(CancelButton())
self.add_item(NextButton(self._bot))
elif (
self._state is SubmissionState.SCORE
or self._state is SubmissionState.PROOF
or self._state is SubmissionState.NAME
):
self.add_item(BackButton(self._bot))
self.add_item(NextButton(self._bot))
elif self._state is SubmissionState.SUBMIT:
self.add_item(BackButton(self._bot))
self.add_item(SubmitButton(self._bot))
async def interaction_check(self, interaction: nextcord.Interaction) -> bool:
check = self._submission_user == interaction.user.id
if not check:
await interaction.send("This is not your submission.", ephemeral=True)
return check
async def on_timeout(self) -> None:
# remove user from submission objects after time out. Allows user to start new submission.
submission_objects.remove_user_submission(self._submission_user)
+65
View File
@@ -0,0 +1,65 @@
"""View for verification buttons"""
import nextcord
from classes.score import Score, ScoreTime # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import submit_score # pylint: disable=import-error
class VerificationView(nextcord.ui.View):
"""Defines views for verifying score submissions"""
def __init__(self):
super().__init__(timeout=None)
@nextcord.ui.button(
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission"
)
async def deny_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for denying score submission."""
await interaction.message.delete()
@nextcord.ui.button(
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission"
)
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for approving score submission."""
content = interaction.message.embeds[0].to_dict()
for field in content["fields"]:
if field["name"] == "Table":
table = field["value"]
elif field["name"] == "Category":
category = field["value"]
elif field["name"] == "Score":
score = field["value"]
elif field["name"] == "User Name":
user = field["value"]
try:
submission_guild = highscores_data.get_guild(interaction.guild_id)
submission_table = submission_guild.get_table(table)
submission_category = submission_table.get_category(category)
if submission_category.time_record:
submission_score: ScoreTime = ScoreTime(user)
submission_score.set_value_str(score)
else:
submission_score: Score = Score(user)
submission_score.set_value_str(score)
await submit_score(
interaction,
submission_guild,
submission_table,
submission_category,
submission_score,
)
await interaction.message.edit(f"Approved by <@{interaction.user.id}>.", view=None)
except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None)
+335
View File
@@ -0,0 +1,335 @@
{
"tables": {
"The Ambassador": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Araxxi": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Arch-Glacor": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Enrage",
"Spooned Log"
],
"Astellarn": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"The Barrows Brothers": [
"Kill Count",
"Spooned Log"
],
"The Barrows: Rise of the Six": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Beastmaster Durzag": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Black Stone Dragon": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Chaos Elemental": [
"Kill Count",
"Spooned Log"
],
"Commander Zilyana": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Corporeal Beast": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Crassian Leviathan": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Croesus": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Dagannoth Kings": [
"Kill Count",
"Spooned Log"
],
"General Graardor": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Giant Mole": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Gregorovic": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Har-Aken": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Helwyr": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Kalphite King": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kalphite Queen": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kerapac": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"King Black Dragon": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kree'arra": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"K'ril Tsutsaroth": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Legiones": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"The Magister": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Masuta the Ascended": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Nex": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Nex - Angel of Death": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Queen Black Dragon": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Raksha": [
"Solo Kill Count",
"Solo Kill Time",
"Duo Kill Count",
"Duo Kill Time",
"Spooned Log"
],
"Rex Matriarchs": [
"Kill Count",
"Spooned Log"
],
"The Sanctum Guardian": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Seiryu the Azure Serpent": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Solak": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Taraket the Necromancer": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Telos": [
"Kill Count",
"Kill Time",
"Enrage",
"Spooned Log"
],
"The Twin Furies": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"TzKal-Zuk": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"TzTok-Jad": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Verak Lith": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Vindicta & Gorvek": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Vorago": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Yakamaru": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Zamorak, Lord of Chaos (solo)": [
"Kill Count",
"Kill Time",
"Enrage",
"Spooned Log"
],
"Zamorak, Lord of Chaos (group)": [
"Kill Count",
"Kill Time",
"Enrage"
]
},
"categories": {
"Duo Kill Count": {
"ascending": false,
"is_time_record": false
},
"Duo Kill Time": {
"ascending": true,
"is_time_record": true
},
"Enrage": {
"ascending": false,
"is_time_record": false
},
"Group Kill Count": {
"ascending": false,
"is_time_record": false
},
"Group Kill Time": {
"ascending": true,
"is_time_record": true
},
"Hardmode Kill Count": {
"ascending": false,
"is_time_record": false
},
"Hardmode Kill Time": {
"ascending": true,
"is_time_record": true
},
"Kill Count": {
"ascending": false,
"is_time_record": false
},
"Kill Time": {
"ascending": true,
"is_time_record": true
},
"Solo Kill Count": {
"ascending": false,
"is_time_record": false
},
"Solo Kill Time": {
"ascending": true,
"is_time_record": true
},
"Spooned Log": {
"ascending": true,
"is_time_record": false
}
},
"table_size": 3
}
+19 -11
View File
@@ -4,12 +4,14 @@ main file for pvm-highscores bot
import logging import logging
import os import os
import sys
import nextcord
from nextcord.ext import commands from nextcord.ext import commands
import nextcord
import config import config
from utils import data_storage from classes.highscores_data import HighscoresData
from classes.submission import SubmissionMap
logger = logging.getLogger("nextcord") logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@@ -18,18 +20,30 @@ handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(me
logger.addHandler(handler) logger.addHandler(handler)
highscores_config = data_storage.open_highscores_config() highscores_data: HighscoresData = HighscoresData()
highscores_data = data_storage.open_highscores_data() highscores_message_map = {}
for guild in highscores_data.guilds:
highscores_message_map[guild.guild_id] = {}
for table in guild.tables:
highscores_message_map[guild.guild_id][table.message_id] = table.name
submission_objects: SubmissionMap = SubmissionMap()
def main(): def main():
"""Main function for bot.""" """Main function for bot."""
min_python_version = (3, 8)
if sys.version_info < min_python_version:
print("ERROR: Python version must be >= 3.8")
sys.exit()
# Allows privledged intents for monitoring members joining, roles editing, and role assignments # Allows privledged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well # These need to be enabled in the developer portal as well
intents = nextcord.Intents.default() intents = nextcord.Intents.default()
# Required in order to read messages (eg. prefix commands) # Required in order to read messages (eg. prefix commands)
intents.message_content = True intents.message_content = True # pylint: disable=assigning-non-slot
# To enable the guilds priveleged intent: # To enable the guilds priveleged intent:
# intents.guilds = True # intents.guilds = True
@@ -37,15 +51,9 @@ def main():
# To enable the members priveleged intent: # To enable the members priveleged intent:
# intents.members = True # intents.members = True
# Set custom status to "Listening to ?help"
activity = nextcord.Activity(
type=nextcord.ActivityType.listening, name=f"{config.BOT_PREFIX}help"
)
bot = commands.Bot( bot = commands.Bot(
commands.when_mentioned_or(config.BOT_PREFIX), commands.when_mentioned_or(config.BOT_PREFIX),
intents=intents, intents=intents,
activity=activity,
) )
# Get the modules of all cogs whose directory structure is ./cogs/<module_name> # Get the modules of all cogs whose directory structure is ./cogs/<module_name>
-202
View File
@@ -1,202 +0,0 @@
{
"bosses": [
{
"boss": "The Ambassador",
"hardmode": true
},
{
"boss": "Araxxi",
"hardmode": false
},
{
"boss": "Arch-Glacor",
"hardmode": true
},
{
"boss": "Astellarn",
"hardmode": true
},
{
"boss": "The Barrows Brothers",
"hardmode": false
},
{
"boss": "The Barrows: Rise of the Six",
"hardmode": false
},
{
"boss": "Beastmaster Durzag",
"hardmode": false
},
{
"boss": "Black Stone Dragon",
"hardmode": true
},
{
"boss": "Chaos Elemental",
"hardmode": false
},
{
"boss": "Commander Zilyana",
"hardmode": true
},
{
"boss": "Corporeal Beast",
"hardmode": false
},
{
"boss": "Crassian Leviathan",
"hardmode": true
},
{
"boss": "Croesus",
"hardmode": false
},
{
"boss": "Dagannoth Kings",
"hardmode": false
},
{
"boss": "General Graardor",
"hardmode": true
},
{
"boss": "Giant Mole",
"hardmode": true
},
{
"boss": "Gregorovic",
"hardmode": true
},
{
"boss": "Har-Aken",
"hardmode": false
},
{
"boss": "Helwyr",
"hardmode": true
},
{
"boss": "Kalphite King",
"hardmode": false
},
{
"boss": "Kalphite Queen",
"hardmode": false
},
{
"boss": "Kerapac",
"hardmode": true
},
{
"boss": "King Black Dragon",
"hardmode": false
},
{
"boss": "Kree'arra",
"hardmode": true
},
{
"boss": "K'ril Tsutsaroth",
"hardmode": true
},
{
"boss": "Legiones",
"hardmode": false
},
{
"boss": "The Magister",
"hardmode": false
},
{
"boss": "Masuta the Ascended",
"hardmode": true
},
{
"boss": "Nex",
"hardmode": false
},
{
"boss": "Nex - Angel of Death",
"hardmode": false
},
{
"boss": "Queen Black Dragon",
"hardmode": false
},
{
"boss": "Raksha",
"hardmode": true
},
{
"boss": "Rex Matriarchs",
"hardmode": false
},
{
"boss": "The Sanctum Guardian",
"hardmode": true
},
{
"boss": "Seiryu the Azure Serpent",
"hardmode": true
},
{
"boss": "Solak",
"hardmode": false
},
{
"boss": "Taraket the Necromancer",
"hardmode": true
},
{
"boss": "Telos",
"hardmode": false
},
{
"boss": "The Twin Furies",
"hardmode": true
},
{
"boss": "TzKal-Zuk",
"hardmode": true
},
{
"boss": "TzTok-Jad",
"hardmode": false
},
{
"boss": "Verak Lith",
"hardmode": true
},
{
"boss": "Vindicta & Gorvek",
"hardmode": true
},
{
"boss": "Vorago",
"hardmode": true
},
{
"boss": "Yakamaru",
"hardmode": false
}
],
"categories": {
"Kill Count": {
"hardmode": true,
"ascending": false,
"is_time_record": false
},
"Kill Time": {
"hardmode": true,
"ascending": true,
"is_time_record": true
},
"Spooned Log": {
"hardmode": false,
"ascending": true,
"is_time_record": false
}
},
"highscore_size": 3
}
+123
View File
@@ -0,0 +1,123 @@
"""
Contains various utlity functions for highscores bot
"""
import nextcord
from highscores import highscores_data, highscores_message_map # pylint: disable=import-error
from classes.score import Score # pylint: disable=import-error
from classes.guild import Guild # pylint: disable=import-error
from classes.category import Category # pylint: disable=import-error
from classes.table import Table # pylint: disable=import-error
def format_highscore_message(guild: Guild, table: Table) -> str:
"""
Creates a formatted string for a boss's highscore discord message.
This formatted string is in discord's code block to ensure monospace text.
Loops through each category to build the message one place at a time.
Args:
guild (Guild): Guild object that is posting the message.
table (Table): Table object that contains the contents of the message.
Returns:
str: Formatted string for discord message
"""
ret_string = f"**{table.name}**\n```"
# Gather all categories and add to string
for category in table.categories:
ret_string += f"{category.name}".ljust(26, " ")
ret_string += "\n"
# list rankings in a single line. Start with first place for each category and then continue
for i in range(table.size):
for category in table.categories:
if len(category.scores) > i:
# pad username to max length.
# have to convert category.scores into list to make it subscriptable. no idea why.
username: str = list(category.scores)[i].user.ljust(guild.username_length, " ")
score: str = list(category.scores)[i].get_str()
# pad score to correct length
# MM:SS.ms is 7 characters long
if category.time_record:
# if there are single digit minutes, add a space for formatting.
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
else:
score = score.rjust(7, " ")
ret_string += f"{i+1}: {username} - {score}".ljust(26, " ")
else:
ret_string += f"{i+1}: submit your score".ljust(26, " ")
ret_string += "\n"
return ret_string + "```"
async def send_highscore_message(
channel: nextcord.TextChannel, guild: Guild, table: Table
) -> nextcord.Message:
"""
Function takes care of checking if a message exists for a table.
Creates and sends/edits the message.
Args:
channel (nextcord.TextChannel): Discord channel object where message is sent
guild (Guild): Guild object for Guild that is posting the message
table (Table): Table to post to the message
Returns:
nextcord.Message: Message object that was posted with table contents
"""
highscore_string = format_highscore_message(guild, table)
message_id = table.message_id
try:
message: nextcord.Message = await channel.fetch_message(message_id)
await message.edit(content=highscore_string)
except nextcord.NotFound:
# message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string)
message_id = message.id
table.message_id = message_id
# save message -> boss_name in message_map
# get guild id from channel
# channel should come from the guild that is requesting this highscores message anyways.
# check if guild is in message map already
if not channel.guild.id in highscores_message_map:
highscores_message_map[channel.guild.id] = {}
highscores_message_map[channel.guild.id][message_id] = table.name
return message
async def submit_score(
interaction: nextcord.Interaction, guild: Guild, table: Table, category: Category, score: Score
) -> None:
"""
Adds passed score to table. Then updates table message by calling send_highscore_message.
Ensures that a highscores channel exists.
Args:
interaction (nextcord.Interaction): Interaction that caused the score submission
guild (Guild): Guild to submit score to
table (Table): Table to submit score to
category (Category): Category to submit score to
score (Score): Score to submit
"""
category.add_score(score)
highscores_data.save_guild_data(guild)
# edit message
print(f"Editting message {guild.highscore_channel}")
channel = interaction.guild.get_channel(guild.highscore_channel)
error_response = "Registered Highscores channel does not exist or was never registered."
error_response += 'Register with "?register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(channel, guild, table)
-53
View File
@@ -1,53 +0,0 @@
"""
Contains functions for saving and loading highscore data and configs.
"""
import json
import os
HIGHSCORES_CONFIG_FILE = "highscores_config.json"
HIGHSCORES_DATA_FILE = "highscores_data.json"
def open_highscores_data():
"""
Opens a highscores_data.json file if one exists, otherwise creates a skeleton of the data.
@return: dict of highscores data.
"""
if os.path.exists(HIGHSCORES_DATA_FILE):
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="r") as file:
data = json.load(file)
else:
# create the json object
data = {"channel_id": -1}
config = open_highscores_config()
for boss in config["bosses"]:
data[boss["boss"]] = {"message_id": 0}
boss_categories = {}
for key, value in config["categories"].items():
rank_list = []
boss_categories[key] = rank_list
if boss["hardmode"] and value["hardmode"]:
rank_list = []
boss_categories["Hardmode " + key] = rank_list
data[boss["boss"]]["categories"] = boss_categories
return data
def open_highscores_config():
"""
Opens the highscores config file and returns.
@return: dict of highscores config.
"""
# Load bot config
with open(HIGHSCORES_CONFIG_FILE, encoding="utf-8", mode="r") as file:
return json.load(file)
def save_highscores_data(data):
"""
Saves the highscores data dict into a json file.
@param data: dict of highscores data.
"""
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="w") as file:
file.write(json.dumps(data, indent=4))
-60
View File
@@ -1,60 +0,0 @@
"""
Contains various functions for managing the highscores data
"""
from nextcord.ext import commands
from highscores import highscores_config # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from utils import highscore_message # pylint: disable=import-error
from utils import data_storage # pylint: disable=import-error
async def submit_score(self, ctx: commands.Context, boss_name: str, category: str, score: str):
"""
Adds a score to the high scores and sorts. Removes a score if larger than size limit.
@param boss: the boss to add score to
@param category: the category to add score to
@param user: user who submitted score
@param score: score (int) to add
"""
# TODO update user submission to not have dupe users
# create score tuple for either time or int
# define sort index
if highscores_config["categories"][category]["is_time_record"]:
score_list = score.split(":")
minutes = score_list[0]
seconds = score_list[1]
score_seconds = int(minutes) * 60 + int(seconds)
score_tuple = (ctx.author.display_name, score, score_seconds)
sort_index = 2
else:
score_tuple = (ctx.author.display_name, int(score))
sort_index = 1
scores = highscores_data[boss_name]["categories"][category]
scores.append(score_tuple)
# sort scores.
ascending = highscores_config["categories"][category]["ascending"]
scores.sort(key=lambda x: x[sort_index], reverse=not ascending)
# enforce highscore size
while len(scores) > highscores_config["highscore_size"]:
scores.pop()
highscores_data[boss_name]["categories"][category] = scores
# edit message
channel_id = highscores_data["channel_id"]
print(f"Editting message {channel_id}")
channel = self.bot.get_channel(channel_id)
if channel is None:
response = 'Registered Highscores channel does not exist or was never registered. \
Register with "?register" command.'
await ctx.send(response)
return
await highscore_message.send_highscore_message(channel, boss_name)
# save data
data_storage.save_highscores_data(highscores_data)
-55
View File
@@ -1,55 +0,0 @@
"""
Contains various functions for sending/editing the highscore messages.
"""
import nextcord
from highscores import highscores_config # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
def format_highscore_message(boss_name: str):
"""
returns a formatted string for a boss's highscore
@param boss: the dictionary object for the boss
@return the formatted string
"""
ret_string = f"**{boss_name}**\n"
boss_data = highscores_data[boss_name]
# Gather all categories and add to string
for key, value in boss_data["categories"].items():
ret_string += f"{key}".ljust(50, " ")
ret_string += "\n"
highscore_size = highscores_config["highscore_size"]
# list rankings in a single line. Start with first place for each category and then continue
for i in range(highscore_size):
for key, value in boss_data["categories"].items():
if len(value) > i:
score = value[i]
ret_string += f"{i+1}: {score[0]} - {score[1]}".ljust(50, " ")
else:
ret_string += f"{i+1}: submit your score".ljust(50, " ")
ret_string += "\n"
return ret_string
async def send_highscore_message(channel, boss_name: str):
"""
Function takes care of checking if a message exists for a boss, then creating and sending/editing the message
@param channel: the channel to send the message in/search for message id
@param boss: the boss to send the message about
"""
highscore_string = format_highscore_message(boss_name)
message_id = highscores_data[boss_name]["message_id"]
try:
message = await channel.fetch_message(message_id)
await message.edit(content=highscore_string)
except nextcord.NotFound:
# message doesn't exist.
message = await channel.send(highscore_string)
message_id = message.id
highscores_data[boss_name]["message_id"] = message_id