Compare commits

...
Author SHA1 Message Date
alwayscompiling c4d5477f8b Merge branch '1-add-more-administrative-commands' into develop 2022-11-24 17:55:06 -05:00
alwayscompiling f5342f1541 Fill functionality for delete user from_table and from_category. 2022-11-24 17:47:21 -05:00
alwayscompiling 1e336e290d Catching AttributeError in autofill functions when invalid variables are sent. 2022-11-24 17:45:40 -05:00
alwayscompiling 9ebbca2557 Fill out docstrings. 2022-11-24 17:05:45 -05:00
alwayscompiling 8a2108fc14 Create shell commands and autofill functions. Fix set initializer in utils.autofill_users. 2022-11-22 17:12:26 -05:00
alwayscompiling 03c7a9303a Small rewrite to consolidate autofill_user function. Sort imports in message_management.py. 2022-11-22 17:02:18 -05:00
alwayscompiling a0c5527bf6 Create autofill functions for users in category and in table. Add function in category class to return a list of all users. 2022-11-22 16:26:31 -05:00
alwayscompiling 8155fb0c8f Reorganized commands to facilitate new commands. 2022-11-22 16:01:55 -05:00
alwayscompiling 06b46ef27c Create general autofill table and category util functions. Update table class to have get categories function. 2022-11-17 14:50:48 -05:00
alwayscompiling 1858f0c37d Add refresh table command. 2022-11-16 16:30:34 -05:00
alwayscompiling 792a225987 Correct docstring 2022-11-15 16:17:22 -05:00
alwayscompiling 2e324dc9c5 Use the new category pad length variable. 2022-11-15 15:52:58 -05:00
alwayscompiling 09f7a4323c Correct category pad default. 2022-11-15 15:50:12 -05:00
alwayscompiling dd73e9b472 Add commands to edit username length and category pad. 2022-11-15 15:48:17 -05:00
alwayscompiling b4f5ddc822 Update to nextcord 2.3.2. 2022-11-12 01:16:04 -05:00
alwayscompiling d2f33959ed Create remove_user command. 2022-08-13 21:46:41 -04:00
alwayscompiling 652533d071 Should have handled username updating code in the command, didn't need to create new functions for the command. 2022-08-13 21:26:50 -04:00
alwayscompiling 73e350c845 Save takes guild object, not id. 2022-08-13 20:46:59 -04:00
alwayscompiling 9dec15dbfa Correct table update_username. 2022-08-13 20:45:29 -04:00
alwayscompiling 41ba4dc23c Save data after modified. 2022-08-13 20:40:06 -04:00
alwayscompiling 790cff3406 Clear view on approval. 2022-08-13 20:33:14 -04:00
alwayscompiling 7fb87ae12f Add username setter. 2022-08-13 20:30:21 -04:00
alwayscompiling 2407e61913 Correcting embed access. 2022-08-13 20:30:03 -04:00
alwayscompiling 6147263647 Add UpdateUsernameVerify view to MessageManagementCog. 2022-08-13 20:23:56 -04:00
alwayscompiling cb71cbe1d5 Create update_username functions for category and table. 2022-08-13 20:08:57 -04:00
alwayscompiling 080356fe9c Create Guild.update_username(). 2022-08-13 19:48:29 -04:00
alwayscompiling d4e19284f0 Create view for verifyng username update request. 2022-08-12 23:18:21 -04:00
alwayscompiling ae9153b78e Change submission views to be more informative. 2022-08-12 22:49:54 -04:00
alwayscompiling ca58e9ac26 Create initial update_username command. 2022-08-12 22:39:12 -04:00
alwayscompiling 432888e468 Tag user on submission verify. 2022-08-12 22:28:14 -04:00
alwayscompiling c918b9492b Renamed to data_management. Better description. 2022-08-11 23:02:44 -04:00
alwayscompiling caef6a3652 Created command to clear table messages. 2022-08-11 23:00:11 -04:00
alwayscompiling b2acafd6e7 Changed design to be one class per file. 2022-08-11 22:44:22 -04:00
alwayscompiling 349fdfe18d Renamed python files to more describe their contents. Intend on having more than 1 class per file. 2022-08-11 22:17:52 -04:00
alwayscompiling efed6e001f Moved all commands into single commands cog. 2022-08-11 21:55:06 -04:00
alwayscompiling 6b0e61cdf7 Update nextcord. 2022-08-08 22:12:41 -04:00
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 e2b0c0ea89 Enforcing python version because ordered dicts in 3.7 is required. 2022-07-29 14:27:10 -04:00
29 changed files with 2169 additions and 877 deletions
+2 -2
View File
@@ -132,6 +132,6 @@ nextcord.log
# data file for bot # data file for bot
highscores_data.json highscores_data.json
highscores_message_map.json
highscores_config.json highscores_config.json
deleted_backups/ deleted_backups/
highscores_data/
+201
View File
@@ -0,0 +1,201 @@
"""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_users(self) -> "list[str]":
"""
Returns list of users in this category.
Returns:
list[str]: List of all users in category.
"""
return self._scores_map.keys()
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, user: str) -> Score:
"""
Removes a user's score from dictionary storage.
Args:
user (str): user whose score to remove
Returns:
Score: User's Score or None
"""
if user in self._scores_map:
return self._scores_map.pop(user)
return None
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)
+245
View File
@@ -0,0 +1,245 @@
"""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._category_pad_length: int = 26
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.category_pad_length = data["category_pad_length"]
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 category_pad_length(self) -> int:
"""
Getter for Category Pad Length.
Returns:
int: Length to pad each category entry.
"""
return self._category_pad_length
@category_pad_length.setter
def category_pad_length(self, length: int) -> None:
"""
Setter for Category Pad Length.
Args:
length (int): New length to set.
"""
self._category_pad_length = length
@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 get_tables(self) -> dict:
"""
Getter for dict of table names and tables.
Returns:
dict: Dictionary consisting of the Table Names and the Table object itself.
"""
# I don't know if this is const data. Don't want to allow data to be written as a result of this function.
return self._table_map
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["category_pad_length"] = self.category_pad_length
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
+154
View File
@@ -0,0 +1,154 @@
"""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
@user.setter
def user(self, username: str) -> None:
"""
Setter for score's username.
Args:
username (str): username to set to
"""
self._user = username
@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)
+174
View File
@@ -0,0 +1,174 @@
"""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_categories(self) -> dict:
"""
Getter for dict of category names and categories.
Returns:
dict: Dictionary consisting of Category names and Category objects.
"""
# I don't know if this is const data. Don't want to allow data to be written as a result of this function.
return self._categories_map
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)
+23
View File
@@ -0,0 +1,23 @@
"""
Cog loader. Adds all command cogs to the bot.
"""
from nextcord.ext import commands
# Command classes
from .message_management import MessageManagementCog
from .settings_management import SettingsManagementCog
from .data_management import DataManagementCog
# Views for commands
from .views.update_username_verify import UpdateUsernameVerifyView
# 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 all command classes to bot.
"""
bot.add_cog(DataManagementCog(bot))
bot.add_cog(MessageManagementCog(bot))
bot.add_cog(SettingsManagementCog())
+243
View File
@@ -0,0 +1,243 @@
"""
Commands for managing the tables/categories/scores for a Guild.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import ( # pylint: disable=import-error
autofill_categories,
autofill_tables,
autofill_users,
send_highscore_message,
)
from .views.update_username_verify import UpdateUsernameVerifyView
class DataManagementCog(commands.Cog, name="Table Management"):
"""
Class holds commands for managing the data storage of the highscores.
"""
def __init__(self, bot: commands.Bot):
"""
Default constructor for DataManagement class. Sets internal bot variable.
"""
self._bot: commands.Bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""
Called when cog is loaded. Adds views required for DataManagement commands.
"""
self._bot.add_view(UpdateUsernameVerifyView())
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def delete(self, interaction: nextcord.Interaction):
"""
Parent slash command for deleting things. Never called.
"""
@delete.subcommand(description="Deletes all scores for this guild. Used for testing. Do Not Use.")
async def scores(self, interaction: nextcord.Interaction):
"""
Warning: This command only works for developers.
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 command. Deletes everything.", ephemeral=True
)
@delete.subcommand()
async def user(self, interaction: nextcord.Interaction):
"""
Subcommand parent for deleting user information. Never called.
"""
@user.subcommand(
description="Removes all of a user's scores. Warning: data is destroyed, use with case.",
)
async def from_all(
self,
interaction: nextcord.Interaction,
username: str = nextcord.SlashOption(
description="User to remove scores for. Note: This is not always a user's discord name."
),
):
"""
Searches all of guild's scores data for any score from the user. Removes. Updates table
message is user was removed from that table.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
username (str): User to remove from highscores
"""
guild = highscores_data.get_guild(interaction.guild_id)
user_was_found: bool = False
# remove user from all tables and categories
for table in guild.tables:
table_updated: bool = False
for category in table.categories:
# user has a score in category
if category.remove_score(username):
user_was_found = True
table_updated = True
if table_updated:
await send_highscore_message(interaction.guild.get_channel(guild.highscore_channel), guild, table)
if not user_was_found:
await interaction.send(f'User "{username}" was not found.', ephemeral=True)
else:
await interaction.send(f'User "{username}" was removed from all tables.')
highscores_data.save_guild_data(guild)
@user.subcommand(description="Deletes a user's scores from a category.")
async def from_category(
self,
interaction: nextcord.Interaction,
table_name: str = nextcord.SlashOption(description="Table to delete user from.", required=True),
category_name: str = nextcord.SlashOption(description="Category to delete user from.", required=True),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
):
"""
Removes a user from a category. Uses autofill functions to fill out table_name, category_name, and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
table_name (str): Table to remove user from. Is SlashOption.
category_name (str): Category to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
guild = highscores_data.get_guild(interaction.guild_id)
try:
guild.get_table(table_name).get_category(category_name).remove_score(user_name)
await send_highscore_message(
interaction.guild.get_channel(guild.highscore_channel), guild, guild.get_table(table_name)
)
await interaction.response.send_message(
f'User "{user_name}" removed from "{category_name}".', ephemeral=True
)
except nextcord.errors.ApplicationInvokeError:
await interaction.response.send_message("Invalid parameter(s).", ephemeral=True)
@user.subcommand(description="Deletes a user's scores from entire table.")
async def from_table(
self,
interaction: nextcord.Interaction,
table_name: str = nextcord.SlashOption(description="Table to delete user from.", required=True),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
):
"""
Removes user from a table. Uses autofill functions to fill out table_name and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
table_name (str): Table to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
guild = highscores_data.get_guild(interaction.guild_id)
try:
for category in guild.get_table(table_name).categories:
category.remove_score(user_name)
await send_highscore_message(
interaction.guild.get_channel(guild.highscore_channel), guild, guild.get_table(table_name)
)
await interaction.response.send_message(f'User "{user_name}" removed from all categories.', ephemeral=True)
except nextcord.errors.ApplicationInvokeError:
await interaction.response.send_message("Invalid parameter(s).", ephemeral=True)
@from_category.on_autocomplete("table_name")
@from_table.on_autocomplete("table_name")
async def autocomplete_table(self, interaction: nextcord.Interaction, table_name: str):
"""
Autocomplete function for table names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
table_name (str): String to be autocompleted with Table name.
"""
await interaction.response.send_autocomplete(autofill_tables(interaction.guild_id, table_name))
@from_category.on_autocomplete("category_name")
async def autocomplete_category(self, interaction: nextcord.Interaction, category_name: str, table_name: str):
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_name (str): String to be autocompleted with Category name.
table_name (str): Table to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, table_name, category_name)
)
@from_category.on_autocomplete("user_name")
@from_table.on_autocomplete("user_name")
async def autocomplete_user(
self, interaction: nextcord.Interaction, user_name: str, table_name: str, category_name: Optional[str]
):
"""
Autocomplete functions for user names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): String to be autocompleted with User name.
table_name (str): Table to find category from.
category_name (Optional[str]): Category to find User from, can be None.
"""
await interaction.response.send_autocomplete(
autofill_users(interaction.guild_id, table_name, category_name, user_name)
)
@nextcord.slash_command(description="Request a username update. Admins must approve.")
async def update_username(
self,
interaction: nextcord.Interaction,
old_username: str = nextcord.SlashOption(description="Username currently stored in highscores.", required=True),
new_username: str = nextcord.SlashOption(description="Username to update to.", required=True),
):
"""
Creates a request to send to admins for changing a username. Uses
UpdateUsernameVerifyView to do the work.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
old_username (str): Username to replace.
new_username (str): Username to replace old with.
"""
guild = highscores_data.get_guild(interaction.guild_id)
verify_channel = interaction.guild.get_channel(guild.verification_channel)
embed: nextcord.Embed = nextcord.Embed(title="Username change request.")
embed.add_field(name="Old Username", value=old_username)
embed.add_field(name="New Username", value=new_username)
await verify_channel.send(
content=f"Username change request from <@{interaction.user.id}>.",
embed=embed,
view=UpdateUsernameVerifyView(),
)
await interaction.send("Request sent to admins for verification.", ephemeral=True)
+137
View File
@@ -0,0 +1,137 @@
"""
Commands for managing/displaying the highscores messages.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from cogs.submission.submission_views import SubmissionCreateButton # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import autofill_tables, send_highscore_message # pylint: disable=import-error
class MessageManagementCog(commands.Cog, name="Message Management"):
"""
Class holds commands for managing the messages that display highscores.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for MessageManagement class
Args:
bot (commands.Bot): Bot object. Required to register submission button view.
"""
self.bot = bot
@nextcord.slash_command(
description="Clears highscores channel of all table messages. Does not change data.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def delete_table_messages(self, interaction: nextcord.Interaction) -> None:
"""
Deletes all table messages from highscores channel. Does not change data.
Does not delete highscores channel id.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
"""
guild = highscores_data.get_guild(interaction.guild_id)
highscores_channel: nextcord.TextChannel = interaction.guild.get_channel(guild.highscore_channel)
await interaction.response.defer(ephemeral=True)
for table in guild.tables:
try:
table_message: nextcord.Message = await highscores_channel.fetch_message(table.message_id)
await table_message.delete()
except nextcord.NotFound:
# silently ignore that messages are not found. They got deleted already.
pass
await interaction.send("Table messages deleted.", ephemeral=True)
@nextcord.slash_command(
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 registered channel.
Sends warning if no channel has been registered.
Either creates new messages for each table if one doesn't exist
or edits the existing message.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
guild = highscores_data.get_guild(interaction.guild_id)
print(f"Printing out highscores information in channel {guild.highscore_channel}")
channel = interaction.guild.get_channel(guild.highscore_channel)
# 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.send(
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.send(f"Posted all tables to <#{channel.id}>.", ephemeral=True)
@nextcord.slash_command(
description="Manually refresh a table. Use when changing settings or data manually changed.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def refresh_table(
self,
interaction: nextcord.Interaction,
table_name: Optional[str] = nextcord.SlashOption(description="Name of table.", required=True),
) -> None:
"""
Refreshes a table given by user.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
table (Optional[str]): Table name to refresh. Not optional. Is SlashOption.
"""
guild = highscores_data.get_guild(interaction.guild_id)
table = guild.get_table(table_name)
channel = interaction.guild.get_channel(guild.highscore_channel)
message: nextcord.Message = await send_highscore_message(channel, guild, table)
await message.edit(view=SubmissionCreateButton(self.bot))
await interaction.response.send_message(f"Updated table {table_name}!", ephemeral=True)
@refresh_table.on_autocomplete("table_name")
async def refresh_table_autofill(self, interaction: nextcord.Interaction, table_name: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
table_name (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_tables(interaction.guild_id, table_name))
+159
View File
@@ -0,0 +1,159 @@
"""
Commands for managing the Guild's settings.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
class SettingsManagementCog(commands.Cog, name="Settings Management"):
"""
Class holds commands for managing Guild's settings.
"""
def __init__(self) -> None:
"""
Default constructor for SettingsManagement class. Does nothing.
"""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def register(self, interaction: nextcord.Interaction):
"""
Parent slash command, never called.
"""
@register.subcommand(description="Registers the channel for perminent highscores messages.")
async def highscore(
self,
interaction: nextcord.Interaction,
channel: Optional[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: Optional[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 Optional[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: Optional[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 (Optional[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)
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def set(self, interaction: nextcord.Interaction):
"""
Parent slash command for setting settings. Never called.
"""
@set.subcommand(
description="Set character length for a category. Can cut off username/score. Affects all categories."
)
async def category_pad_length(
self,
interaction: nextcord.Interaction,
length: Optional[int] = nextcord.SlashOption(
description="Number of characters.", required=True, default=26
),
) -> None:
"""
Changes guild's category pad length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
length (Optional[int]): Number of characters. Is SlashOption. Defaults to 26.
"""
guild = highscores_data.get_guild(interaction.guild_id)
guild.category_pad_length = length
highscores_data.save_guild_data(guild)
await interaction.response.send_message(
f"Category pad length set to {length}", ephemeral=True
)
@set.subcommand(description="Set username length. Limits all submissions to this length.")
async def username_length(
self,
interaction: nextcord.Interaction,
length: Optional[int] = nextcord.SlashOption(
description="Number of characters.", required=True, default=12
),
) -> None:
"""
Changes guild's username length limit.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
length (Optional[int]): Number of charactesr. Is SlashOption. Defaults to 12.
"""
guild = highscores_data.get_guild(interaction.guild_id)
guild.username_length = length
highscores_data.save_guild_data(guild)
await interaction.response.send_message(f"Username length set to {length}", ephemeral=True)
@@ -0,0 +1,82 @@
"""View for allowing a username update."""
import nextcord
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import send_highscore_message # pylint: disable=import-error
class UpdateUsernameVerifyView(nextcord.ui.View):
"""Defines view for allowing a username update."""
def __init__(self):
"""
Default constructor for UpdateUsernameVerifyView. Calls super constructor with no timeout.
"""
super().__init__(timeout=None)
@nextcord.ui.button(
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-username-update"
)
async def deny_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""
Button for denying username update. Deletes message.
Args:
button (nextcord.ui.Button): unused
interaction (nextcord.Interaction): Interaction with the button
"""
await interaction.message.delete()
@nextcord.ui.button(
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-username-update"
)
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""
Button for approving username update. Tells guild data to update username, then updates
message to show it is complete.
Args:
button (nextcord.ui.Button): unused
interaction (nextcord.Interaction): Interaction with the button
"""
embed_content = interaction.message.embeds[0].to_dict()
old_username: str = embed_content["fields"][0]["value"]
new_username: str = embed_content["fields"][1]["value"]
guild = highscores_data.get_guild(interaction.guild_id)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
await interaction.edit(content=content, view=None)
user_was_found: bool = False
# update username in all tables and categories
for table in guild.tables:
table_updated: bool = False
for category in table.categories:
score = category.remove_score(old_username)
if score:
# score exists
score.user = new_username
category.add_score(score)
table_updated = True
user_was_found = True
if table_updated:
await send_highscore_message(
interaction.guild.get_channel(guild.highscore_channel), guild, table
)
if not user_was_found:
await interaction.send(
f'User "{old_username}" was not found. Inform user to correct username.',
ephemeral=True,
)
else:
highscores_data.save_guild_data(guild)
-40
View File
@@ -1,40 +0,0 @@
"""
Bot command for deleting the guild's scores.
"""
from nextcord import Embed
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from utilities.highscores_data import save_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, bot: commands.Bot):
self.bot = bot
@commands.command(name="delete-scores")
@commands.has_permissions(administrator=True)
async def delete_scores(self, ctx: commands.Context):
"""A command which deletes all scores for this guild.
Usage:
```
?delete_scores
```
"""
# only allow AlwaysCompiling to use.
# If you are a developer and want to use command, can add name to list
if ctx.author.id == 206602713511886849:
highscores_data.reset_guild_data(ctx.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(DeleteScores(bot))
+5 -2
View File
@@ -17,7 +17,10 @@ class GuildListener(commands.Cog, name="Guild Listener"):
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): async def on_guild_join(self, guild: nextcord.Guild):
"""Listener for Guild Join. Creates skeleton for new guild joined and saves it.""" """Listener for Guild Join. Creates skeleton for new guild joined and saves it."""
highscores_data.add_new_server(guild.id) 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: str = "Hello, I keep track of Highscores for you!"
welcome_message += " Currently only supporting RuneScape boss highscores," welcome_message += " Currently only supporting RuneScape boss highscores,"
@@ -30,7 +33,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
async def on_guild_remove(self, guild: nextcord.Guild): async def on_guild_remove(self, guild: nextcord.Guild):
"""Listener for Guild Remove. Removes data from highscores_data.""" """Listener for Guild Remove. Removes data from highscores_data."""
print(f"Removing data for guild {guild.name} (id {guild.id}).") print(f"Removing data for guild {guild.name} (id {guild.id}).")
highscores_data.remove_guild(guild.id, guild.name) highscores_data.remove_guild(guild.id)
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
-26
View File
@@ -1,26 +0,0 @@
"""
Help command
"""
from nextcord.ext import commands
from .help_command import NewHelpCommand
class HelpCog(commands.Cog):
"""Help Command"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self._original_help_command = bot.help_command
bot.help_command = NewHelpCommand()
bot.help_command.cog = self
def cog_unload(self):
self.bot.help_command = self._original_help_command
# setup functions for bot
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(HelpCog(bot))
-84
View File
@@ -1,84 +0,0 @@
"""
Help command. Copied from discord-bot-template.
"""
import nextcord
from nextcord.embeds import EmptyEmbed
from nextcord.ext import commands
import config # pylint: disable=import-error
class NewHelpCommand(commands.MinimalHelpCommand):
"""Custom help command override using embeds"""
COLOUR = nextcord.Colour.blurple()
def get_ending_note(self):
"""Returns note to display at the bottom"""
invoked_with = self.invoked_with
return f"Use {config.BOT_PREFIX}{invoked_with} [command] for more info on a command."
def get_command_signature(self, command: commands.core.Command):
"""Retrieves the signature portion of the help page."""
return f"{command.qualified_name} {command.signature}"
async def send_bot_help(self, mapping: dict):
"""implements bot command help page"""
embed = nextcord.Embed(title="Bot Commands", colour=self.COLOUR)
avatar = self.context.bot.user.avatar
avatar_url = avatar.url if avatar else EmptyEmbed
embed.set_author(name=self.context.bot.user.name, icon_url=avatar_url)
description = self.context.bot.description
if description:
embed.description = description
for cog, commands in mapping.items(): # pylint: disable=redefined-outer-name
name = "No Category" if cog is None else cog.qualified_name
filtered = await self.filter_commands(commands, sort=True)
if filtered:
# \u2002 = middle dot
value = "\u2002".join(f"{config.BOT_PREFIX}{c.name}" for c in filtered)
if cog and cog.description:
value = f"{cog.description}\n{value}"
embed.add_field(name=name, value=value)
embed.set_footer(text=self.get_ending_note())
await self.get_destination().send(embed=embed)
async def send_cog_help(self, cog: commands.Cog):
"""implements cog help page"""
embed = nextcord.Embed(title=f"{cog.qualified_name} Commands", colour=self.COLOUR)
if cog.description:
embed.description = cog.description
filtered = await self.filter_commands(cog.get_commands(), sort=True)
for command in filtered:
embed.add_field(
name=self.get_command_signature(command),
value=command.short_doc or "...",
inline=False,
)
embed.set_footer(text=self.get_ending_note())
await self.get_destination().send(embed=embed)
async def send_group_help(self, group: commands.Group):
"""implements group help page and command help page"""
embed = nextcord.Embed(title=group.qualified_name, colour=self.COLOUR)
if group.help:
embed.description = group.help
if isinstance(group, commands.Group):
filtered = await self.filter_commands(group.commands, sort=True)
for command in filtered:
embed.add_field(
name=self.get_command_signature(command),
value=command.short_doc or "...",
inline=False,
)
embed.set_footer(text=self.get_ending_note())
await self.get_destination().send(embed=embed)
# Use the same function as group help for command help
send_command_help = send_group_help # type: ignore
+31 -16
View File
@@ -2,15 +2,15 @@
Defines cog for on message listener. Defines cog for on message listener.
""" """
from datetime import date
import random
import re import re
from nextcord.ext import commands from nextcord.ext import commands
import nextcord import nextcord
from cogs.submission.submission_views import SubmissionState # pylint: disable=import-error from classes.submission import SubmissionState # pylint: disable=import-error
from cogs.submission.submission_views import get_submission_embed # pylint: disable=import-error from highscores import highscores_data, submission_objects # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from highscores import submission_objects # pylint: disable=import-error
class SubmissionListener(commands.Cog, name="Message Listener"): class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -22,10 +22,23 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: nextcord.Message): async def on_message(self, message: nextcord.Message):
"""Listener for message creation. Adds score and attachments to submission.""" """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 guild_id = message.guild.id
if message.author.id in submission_objects:
message_id = submission_objects[message.author.id]["message_id"] # troll addition for friends birthday
submission_channel_id = highscores_data.get_guild_submission_channel(guild_id) 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 = ( submission_message: nextcord.Message = (
await self.bot.get_guild(guild_id) await self.bot.get_guild(guild_id)
.get_channel(submission_channel_id) .get_channel(submission_channel_id)
@@ -34,12 +47,15 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
# need check if correct user and correct channel # need check if correct user and correct channel
if message.channel.id == submission_channel_id: if message.channel.id == submission_channel_id:
state = submission_objects[message.author.id]["submission_state"] state = submission.submission_state
if state == SubmissionState.SCORE and message.content: if state == SubmissionState.SCORE and message.content:
# check with regex if score matches correctly # check with regex if score matches correctly
is_time_record = highscores_data.get_config_time_record( is_time_record = (
submission_objects[message.author.id]["category"] highscores_data.get_guild(guild_id)
.get_table(submission.table)
.get_category(submission.category)
.time_record
) )
time_regex = re.search( time_regex = re.search(
@@ -71,9 +87,8 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score): elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove # if there is a minute 10ths place 0, remove
score = score.lstrip("0") score = score.lstrip("0")
submission_objects[message.author.id]["score"] = score submission.score = score
embed = get_submission_embed(message.author.id) embed = submission.get_embed()
await submission_message.edit( await submission_message.edit(
embed=embed, embed=embed,
) )
@@ -82,10 +97,10 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
files=[await attch.to_file() for attch in message.attachments], files=[await attch.to_file() for attch in message.attachments],
) )
elif state == SubmissionState.NAME and message.content: elif state == SubmissionState.NAME and message.content:
submission_objects[message.author.id]["username"] = message.content[ submission.username = message.content[
: highscores_data.get_guild_username_length(guild_id) : highscores_data.get_guild(guild_id).username_length
] ]
embed = get_submission_embed(message.author.id) embed = submission.get_embed()
await submission_message.edit( await submission_message.edit(
embed=embed, embed=embed,
) )
-47
View File
@@ -1,47 +0,0 @@
"""
Bot command to post the highscores
"""
from nextcord.ext import commands
import nextcord
from cogs.submission.submission_views import SubmissionCreateButton # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import send_highscore_message # pylint: disable=import-error
class PostHighscores(commands.Cog, name="Post Highscores"):
"""Posts Highscores."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="post-scores")
@commands.has_permissions(administrator=True)
async def post_highscores(self, ctx: commands.Context):
"""
A command which posts the highscore in the designated channel.
Sends warning if no channel has been registered.
Usage:
```
?post-scores
```
"""
highscore_channel_id = highscores_data.get_guild_highscores_channel(ctx.guild.id)
print(f"Printing out highscores information in channel {highscore_channel_id}")
channel = ctx.guild.get_channel(highscore_channel_id)
error_response = "Registered Highscores channel does not exist or was never registered. "
error_response += 'Register with "?register" command.'
assert channel is not None, await ctx.send(error_response)
for table in highscores_data.get_table_names(ctx.guild.id):
message: nextcord.Message = await send_highscore_message(channel, table, ctx.guild.id)
await message.edit(view=SubmissionCreateButton(self.bot))
# 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(PostHighscores(bot))
+8 -5
View File
@@ -16,7 +16,7 @@ class BotPresence(commands.Cog, name="Bot Presence"):
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self): async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument
"""Listener for Guild Join. Updated presence on guild join.""" """Listener for Guild Join. Updated presence on guild join."""
await self.change_presence() await self.change_presence()
@@ -35,14 +35,17 @@ class BotPresence(commands.Cog, name="Bot Presence"):
async def change_presence(self): async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence.""" """Randomly chooses a presence and updates bot's presence."""
statuses = [ statuses = [
f"?help | {len(self.bot.guilds)} servers", "Now with slash commands!",
f"Vuln Pls. | {len(self.bot.guilds)} servers", "Vuln Pls.",
f"All your base are belong to us. | {len(self.bot.guilds)} servers", "All your base are belong to us.",
f"Double plus good. | {len(self.bot.guilds)} servers", "Double plus good.",
"That's a lot of damage!",
] ]
status = random.choice(statuses) status = random.choice(statuses)
status += f" | {len(self.bot.guilds)} servers"
await self.bot.change_presence(activity=nextcord.Game(status)) await self.bot.change_presence(activity=nextcord.Game(status))
-64
View File
@@ -1,64 +0,0 @@
"""
Bot command for registering various channels to the bot.
"""
from nextcord import Embed
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
class RegisterVerificationChannel(commands.Cog, name="Register"):
"""Bot command for registering various channels to the bot."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="register")
@commands.has_permissions(administrator=True)
async def register_highscores(self, ctx: commands.Context, registration_type: str):
"""A command which registers current channel as for various uses.
Usage:
```
?register verify (Use in channel you want to verify submissions in.)
?register highscore (Use in channel you want to show highscore messages in.)
?register submission (Use in channel you want submission messages to be send it.)
?register help (Shows help message.)
```
"""
# respond to the message
channel_id = ctx.channel.id
if registration_type == "verify":
await ctx.send(f"Registered <#{channel_id}> as channel to post verifications in.")
highscores_data.set_guild_verification_channel(ctx.guild.id, channel_id)
elif registration_type == "highscore":
await ctx.send(f"Registered <#{channel_id}> as channel to post highscores in.")
highscores_data.set_guild_highscore_channel(ctx.guild.id, channel_id)
elif registration_type == "submission":
await ctx.send(f"Registered <#{channel_id}> as channel to post submissions in.")
highscores_data.set_guild_submission_channel(ctx.guild.id, channel_id)
else:
embed = Embed(
title="Register command help", description="How to use the register command."
)
embed.add_field(
name="?register verify", value="Use in channel you want to verify submissions in."
)
embed.add_field(
name="?register highscore",
value="Use in channel you want to show highscore messages in.",
)
embed.add_field(
name="?register submission",
value="Use in channel you want submission messages to be send it.",
)
embed.add_field(name="?register help", value="Use to show this message.")
await ctx.reply(embed=embed)
# 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(RegisterVerificationChannel(bot))
-53
View File
@@ -1,53 +0,0 @@
"""
Bot command to submit a score
"""
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
from .verification_view import VerificationView
class Score(commands.Cog, name="Score"):
"""Submits a score."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""Called when the cog is loaded"""
self.bot.add_view(VerificationView())
@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"
This command still works if you prefer to use it over the ui.
```
"""
guild_data = highscores_data[str(ctx.guild.id)]
verification_channel_id = guild_data["verification_channel_id"]
channel = ctx.guild.get_channel(verification_channel_id)
error_response = "Registered Verification channel does not exist or was never registered."
error_response += "Contact server admins."
assert channel is not None, await ctx.send(error_response)
verification_string = f"{ctx.author.display_name}:{boss_name}:{category}:{score}"
await channel.send(
content=verification_string,
files=[await attch.to_file() for attch in ctx.message.attachments],
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(Score(bot))
-40
View File
@@ -1,40 +0,0 @@
"""View for verification buttons"""
import nextcord
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="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-score-command"
)
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for approving score submission."""
content = interaction.message.content.split(":")
user = content[0]
boss_name = content[1]
category = content[2]
score = content[3]
await submit_score(interaction, user, boss_name, category, score)
await interaction.message.delete()
await interaction.channel.send(
f"Approved: {boss_name} {category} {user} {score}",
files=[await attch.to_file() for attch in interaction.message.attachments],
)
@nextcord.ui.button(
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-score-command"
)
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()
+55 -91
View File
@@ -1,63 +1,25 @@
"""Views used for submission process.""" """Views used for submission process."""
from typing import Optional from typing import Optional
from enum import Enum
import nextcord import nextcord
from nextcord import Embed
from nextcord.ext import commands from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error from classes.submission import Submission, SubmissionState # pylint: disable=import-error
from highscores import highscores_message_map # pylint: disable=import-error from highscores import ( # pylint: disable=import-error
from highscores import submission_objects # pylint: disable=import-error highscores_data,
highscores_message_map,
submission_objects,
)
from .verification_view import VerificationView from .verification_view import VerificationView
class SubmissionState(Enum):
"""Enum for which state a submission is in"""
CATEGORY = 1
SCORE = 2
PROOF = 3
NAME = 4
SUBMIT = 5
VERIFY = 6
def get_submission_embed(userid: int) -> nextcord.Embed:
"""Creates an embed based off of the submission state and fields filled out."""
usersubmission: dict = submission_objects[userid]
state: SubmissionState = usersubmission["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=usersubmission["username"], inline=False)
embed.add_field(name="Boss", value=usersubmission["boss_name"], inline=False)
if usersubmission["category"]:
embed.add_field(name="Category", value=usersubmission["category"], inline=False)
if usersubmission["score"]:
embed.add_field(name="Score", value=usersubmission["score"], inline=False)
return embed
def category_select_options(guild_id: int, boss_name: str) -> "list[nextcord.SelectOption]": def category_select_options(guild_id: int, boss_name: str) -> "list[nextcord.SelectOption]":
"""Returns list of categories for a given boss.""" """Returns list of categories for a given boss."""
options: "list[nextcord.SelectOption]" = [] options: "list[nextcord.SelectOption]" = []
category_list = highscores_data.get_table_categories(guild_id, boss_name) category_list = highscores_data.get_guild(guild_id).get_table(boss_name).categories
for category in category_list: for category in category_list:
options.append(nextcord.SelectOption(label=category)) options.append(nextcord.SelectOption(label=category.name))
return options return options
@@ -79,8 +41,9 @@ class SubmissionDropdown(nextcord.ui.Select):
self._bot = bot self._bot = bot
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
submission_objects[interaction.user.id]["category"] = self.values[0] submission = submission_objects.get_user_submission(interaction.user.id)
embed = get_submission_embed(interaction.user.id) submission.category = self.values[0]
embed = submission.get_embed()
await interaction.message.edit( await interaction.message.edit(
embed=embed, embed=embed,
) )
@@ -94,10 +57,11 @@ class BackButton(nextcord.ui.Button):
self._bot = bot self._bot = bot
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
state: SubmissionState = submission_objects[interaction.user.id]["submission_state"] submission = submission_objects.get_user_submission(interaction.user.id)
state: SubmissionState = submission.submission_state
state = SubmissionState(state.value - 1) state = SubmissionState(state.value - 1)
submission_objects[interaction.user.id]["submission_state"] = state submission.submission_state = state
embed = get_submission_embed(interaction.user.id) embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id)) await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
@@ -108,7 +72,7 @@ class CancelButton(nextcord.ui.Button):
super().__init__(label="Cancel", style=nextcord.ButtonStyle.red, custom_id="cancel") super().__init__(label="Cancel", style=nextcord.ButtonStyle.red, custom_id="cancel")
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
submission_objects.pop(interaction.user.id) submission_objects.remove_user_submission(interaction.user.id)
await interaction.message.delete() await interaction.message.delete()
@@ -121,22 +85,18 @@ class NextButton(nextcord.ui.Button):
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
# check if fields have been filled out for each state # check if fields have been filled out for each state
state: SubmissionState = submission_objects[interaction.user.id]["submission_state"] submission = submission_objects.get_user_submission(interaction.user.id)
if ( state: SubmissionState = submission.submission_state
state == SubmissionState.CATEGORY if state == SubmissionState.CATEGORY and not submission.category:
and not submission_objects[interaction.user.id]["category"]
):
await interaction.send("Please choose a category.", ephemeral=True) await interaction.send("Please choose a category.", ephemeral=True)
elif ( elif state == SubmissionState.SCORE and not submission.score:
state == SubmissionState.SCORE and not submission_objects[interaction.user.id]["score"]
):
await interaction.send("Please send a score.", ephemeral=True) await interaction.send("Please send a score.", ephemeral=True)
elif state == SubmissionState.PROOF and not len(interaction.message.attachments) > 0: elif state == SubmissionState.PROOF and not len(interaction.message.attachments) > 0:
await interaction.send("Please attach picture for proof.", ephemeral=True) await interaction.send("Please attach picture for proof.", ephemeral=True)
else: else:
state = SubmissionState(state.value + 1) state = SubmissionState(state.value + 1)
submission_objects[interaction.user.id]["submission_state"] = state submission.submission_state = state
embed = get_submission_embed(interaction.user.id) embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id)) await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
@@ -148,27 +108,28 @@ class SubmitButton(nextcord.ui.Button):
self._bot = bot self._bot = bot
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
verification_channel_id = highscores_data.get_guild_verification_channel( verification_channel_id = highscores_data.get_guild(
interaction.guild_id interaction.guild_id
) ).verification_channel
channel = interaction.guild.get_channel(verification_channel_id) 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. \ error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins." Contact server admins."
assert channel is not None, await channel.send(error_response) assert channel is not None, await channel.send(error_response)
submission_objects[interaction.user.id]["submission_state"] = SubmissionState.VERIFY submission.submission_state = SubmissionState.VERIFY
embed = get_submission_embed(interaction.user.id) embed = submission.get_embed()
await channel.send( await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments], files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(), view=VerificationView(),
embed=embed, embed=embed,
content=f"Submission from {interaction.user.display_name}", content=f"Submission from <@{interaction.user.id}>.",
) )
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None) await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
submission_objects.pop(interaction.user.id) submission_objects.remove_user_submission(interaction.user.id)
############ Submission Views ############ ############ Submission Views ############
@@ -189,31 +150,25 @@ class SubmissionCreateButton(nextcord.ui.View):
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Button for score submission.""" """Button for score submission."""
# Don't allow a user to start a new submission if one already exists # Don't allow a user to start a new submission if one already exists
if interaction.user.id in submission_objects: if submission_objects.has_user(interaction.user.id):
await interaction.send("Please finish in progress submission first.", ephemeral=True) await interaction.send("Please finish in progress submission first.", ephemeral=True)
return return
boss_name = highscores_message_map[interaction.guild.id][interaction.message.id] table = highscores_message_map[interaction.guild.id][interaction.message.id]
channel = interaction.guild.get_channel( channel = interaction.guild.get_channel(
highscores_data.get_guild_submission_channel(interaction.guild_id) highscores_data.get_guild(interaction.guild_id).submission_channel
) )
# a user should only have 1 submission active at once. # a user should only have 1 submission active at once.
# submitting with dummy message_id until message created # split username on pipe then limit length.
# split username on pipe. username: str = interaction.user.display_name.split("|")[0][
submission_objects[interaction.user.id] = { : highscores_data.get_guild(interaction.guild_id).username_length
"message_id": 0, ]
"guild_id": interaction.guild_id, submission: Submission = Submission(interaction.guild_id, table, username)
"submission_state": SubmissionState.CATEGORY,
"boss_name": boss_name,
"score": "",
"category": "",
"username": interaction.user.display_name.split("|")[0][
: highscores_data.get_guild_username_length(interaction.guild_id)
],
}
embed = get_submission_embed(interaction.user.id) submission_objects.add_submission(interaction.user.id, submission)
embed = submission.get_embed()
submission_message = await channel.send( submission_message = await channel.send(
f"<@{interaction.user.id}>", f"<@{interaction.user.id}>",
@@ -225,7 +180,7 @@ class SubmissionCreateButton(nextcord.ui.View):
) )
# update the message_id once its created # update the message_id once its created
submission_objects[interaction.user.id]["message_id"] = submission_message.id submission.message_id = submission_message.id
class SubmissionView(nextcord.ui.View): class SubmissionView(nextcord.ui.View):
@@ -236,14 +191,14 @@ class SubmissionView(nextcord.ui.View):
bot: commands.Bot, bot: commands.Bot,
submission_user: int, submission_user: int,
*, *,
timeout: Optional[float] = 180, timeout: Optional[float] = 300,
): ):
super().__init__(timeout=timeout) super().__init__(timeout=timeout)
self._submission_user = submission_user self._submission_user = submission_user
self._bot = bot self._bot = bot
self._state = submission_objects[self._submission_user]["submission_state"] self._state = submission_objects.get_user_submission(self._submission_user).submission_state
self._boss_name = submission_objects[self._submission_user]["boss_name"] self._boss_name = submission_objects.get_user_submission(self._submission_user).table
self._guild_id = submission_objects[self._submission_user]["guild_id"] self._guild_id = submission_objects.get_user_submission(self._submission_user).guild_id
# add items depending on given state # add items depending on given state
if self._state is SubmissionState.CATEGORY: if self._state is SubmissionState.CATEGORY:
@@ -264,4 +219,13 @@ class SubmissionView(nextcord.ui.View):
self.add_item(SubmitButton(self._bot)) self.add_item(SubmitButton(self._bot))
async def interaction_check(self, interaction: nextcord.Interaction) -> bool: async def interaction_check(self, interaction: nextcord.Interaction) -> bool:
return self._submission_user == interaction.user.id 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)
+25 -4
View File
@@ -1,6 +1,9 @@
"""View for verification buttons""" """View for verification buttons"""
import nextcord 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 from utilities.utils import submit_score # pylint: disable=import-error
@@ -29,8 +32,8 @@ class VerificationView(nextcord.ui.View):
content = interaction.message.embeds[0].to_dict() content = interaction.message.embeds[0].to_dict()
for field in content["fields"]: for field in content["fields"]:
if field["name"] == "Boss": if field["name"] == "Table":
boss_name = field["value"] table = field["value"]
elif field["name"] == "Category": elif field["name"] == "Category":
category = field["value"] category = field["value"]
elif field["name"] == "Score": elif field["name"] == "Score":
@@ -39,8 +42,26 @@ class VerificationView(nextcord.ui.View):
user = field["value"] user = field["value"]
try: try:
await submit_score(interaction, user, boss_name, category, score) submission_guild = highscores_data.get_guild(interaction.guild_id)
await interaction.message.edit(f"Approved by <@{interaction.user.id}>.", view=None) 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,
)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
await interaction.message.edit(content=content, view=None)
except UnboundLocalError: except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.") await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None) await interaction.message.edit(view=None)
@@ -1,8 +1,8 @@
{ {
"tables": { "tables": {
"The Ambassador": [ "The Ambassador": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -21,8 +21,8 @@
"Spooned Log" "Spooned Log"
], ],
"Astellarn": [ "Astellarn": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -42,8 +42,8 @@
"Spooned Log" "Spooned Log"
], ],
"Black Stone Dragon": [ "Black Stone Dragon": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -65,8 +65,8 @@
"Spooned Log" "Spooned Log"
], ],
"Crassian Leviathan": [ "Crassian Leviathan": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -160,8 +160,8 @@
"Spooned Log" "Spooned Log"
], ],
"Masuta the Ascended": [ "Masuta the Ascended": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -193,15 +193,15 @@
"Spooned Log" "Spooned Log"
], ],
"The Sanctum Guardian": [ "The Sanctum Guardian": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
], ],
"Seiryu the Azure Serpent": [ "Seiryu the Azure Serpent": [
"Kill Count", "Solo Kill Count",
"Kill Time", "Solo Kill Time",
"Group Kill Count", "Group Kill Count",
"Group Kill Time", "Group Kill Time",
"Spooned Log" "Spooned Log"
@@ -331,5 +331,5 @@
"is_time_record": false "is_time_record": false
} }
}, },
"highscore_size": 3 "table_size": 3
} }
+14 -8
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 utilities.highscores_data import HighscoresData 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)
@@ -21,17 +23,21 @@ logger.addHandler(handler)
highscores_data: HighscoresData = HighscoresData() highscores_data: HighscoresData = HighscoresData()
highscores_message_map = {} highscores_message_map = {}
for guild in highscores_data.get_guilds(): for guild in highscores_data.guilds:
highscores_message_map[guild] = {} highscores_message_map[guild.guild_id] = {}
for table in highscores_data.get_table_names(guild): for table in guild.tables:
message_id = highscores_data.get_table_message_id(guild, table) highscores_message_map[guild.guild_id][table.message_id] = table.name
highscores_message_map[guild][message_id] = table
submission_objects: dict = {} 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()
+2 -2
View File
@@ -1,2 +1,2 @@
nextcord>=2.0.0rc1 nextcord>=2.3.2
python-dotenv>=0.20.0 python-dotenv>=0.20.0
-327
View File
@@ -1,327 +0,0 @@
"""Class and functions for storing and retrieving highscores data."""
import os
import json
HIGHSCORES_CONFIG_FILE = "highscores_config.json"
HIGHSCORES_DATA_FILE = "highscores_data.json"
class HighscoresData:
"""Class for storing and retrieving highscores data."""
def __init__(self) -> None:
self._config = open_highscores_config()
self._data = open_highscores_data()
def add_new_server(self, guild_id: int):
"""
Creates new skeleton data dict and adds to self for new guild.
@param guild_id: guild_id to add.
"""
# create the dict
skeleton = {
"highscore_channel_id": -1,
"verification_channel_id": -1,
"username_length": 12,
}
config: dict = open_highscores_config()
skeleton["tables"] = {}
for boss, categories in config["tables"].items():
skeleton["tables"][boss] = {"message_id": 0}
boss_categories = {}
for category in categories:
rank_list = []
boss_categories[category] = rank_list
skeleton["tables"][boss]["categories"] = boss_categories
self._data[str(guild_id)] = skeleton
def add_score(
self, guild_id: int, boss_name: str, category: str, user: str, score: str
) -> None:
"""
Adds a new score to guilds highscore data.
Ensures that there is no user duplicate and then sorts the scores
@param guild_id: guild id to retrieve from.
@param boss_name: boss to add score to.
@param category: category to add score to.
@param user: user that submitted score.
@param score: score to add.
"""
scores: "list[tuple]" = self._data[str(guild_id)]["tables"][boss_name]["categories"][
category
]
ascending: bool = self._config["categories"][category]["ascending"]
time_record: bool = self._config["categories"][category]["is_time_record"]
# create score tuple for either time or int
# define sort index
if time_record:
if score.find(":") == -1:
score_seconds = float(score)
else:
minutes = score.split(":")[0]
seconds = score.split(":")[1]
score_seconds = int(minutes) * 60 + float(seconds)
score_tuple = (user, score, score_seconds)
sort_index = 2
else:
score_tuple = (user, int(score))
sort_index = 1
# if user is in an existing tuple, delete it.
users_scores: "list[tuple]" = [score_tuple]
for tup in scores:
if user == tup[0]:
# Add tup to users_scores, remove from scores
users_scores.append(tup)
# sort users_scores
users_scores.sort(
key=lambda x: x[sort_index],
reverse=not ascending,
)
for index, tup in enumerate(users_scores):
if index == 0:
# first score is the best score.
if tup not in scores:
scores.append(tup)
else:
# discard all other scores from scores list.
if tup in scores:
scores.remove(tup)
# sort scores.
scores.sort(
key=lambda x: x[sort_index],
reverse=not ascending,
)
self._data[str(guild_id)]["tables"][boss_name]["categories"][category] = scores
save_highscores_data(self._data)
def get_categories_data(self, guild_id: int, table: str) -> "list[tuple]":
"""
Returns categories data in a list of tuples. Calls dict function items().
@param guild_id: guild's id to retrieve from.
@param table: table to get category data from
@returns: list of tuples containing all category and relative score data.
"""
return self._data[str(guild_id)]["tables"][table]["categories"].items()
def get_config_highscore_size(self) -> int:
"""
Gets the defined size of highscore for config.
@returns: int highscore size
"""
return self._config["highscore_size"]
def get_config_time_record(self, category: str) -> bool:
"""
Tells if the category is a time record
@param category: category name to retrieve from.
@returns: bool indicating time record or not.
"""
return self._config["categories"][category]["is_time_record"]
def get_guilds(self) -> "list[int]":
"""
Gets a list of guilds tracked by data
@returns: list of ints representing guild ids
"""
guilds: "list[int]" = []
for guild, data in self._data.items(): # pylint: disable=unused-variable
guilds.append(int(guild))
return guilds
def get_guild_highscores_channel(self, guild_id: int) -> int:
"""
Returns the channel id to post highscore messages to.
@param guild_id: guild id to retrieve from.
@returns: channel id of highscores channel.
"""
return self._data[str(guild_id)]["highscore_channel_id"]
def get_guild_submission_channel(self, guild_id: int) -> int:
"""
Returns the channel id to post submission messages to.
@param guild_id: guild id to retrieve from.
@returns: channel id of submission channel.
"""
return self._data[str(guild_id)]["submission_channel_id"]
def get_guild_username_length(self, guild_id: int) -> int:
"""
Returns username length as set by each guild.
@param guild_id: guild id to retrieve from.
@returns: int of username length.
"""
return self._data[str(guild_id)]["username_length"]
def get_guild_verification_channel(self, guild_id: int) -> int:
"""
Returns the channel id to post verification messages to.
@param guild_id: guild id to retrieve from.
@returns: channel id of verification channel.
"""
return self._data[str(guild_id)]["verification_channel_id"]
def get_table_categories(self, guild_id: int, table: str) -> "list[str]":
"""
Returns list of categories for a guild's table
@param guild_id: guild id to retrieve from.
@param table: the table to retreive from.
@returns: string list of categories in given table.
"""
categories: "list[str]" = []
for category, data in self._data[str(guild_id)][ # pylint: disable=unused-variable
"tables"
][table]["categories"].items():
categories.append(category)
return categories
def get_table_message_id(self, guild_id: int, table: str) -> int:
"""
Returns the message id for given guild's table.
@param guild_id: guild id to retrieve from.
@param table: the table to retrieve from.
@returns: int id of message for table's highscores.
"""
return self._data[str(guild_id)]["tables"][table]["message_id"]
def get_table_names(self, guild_id) -> "list[str]":
"""
Returns a list of table names.
@param guild_id: guild id to retrieve from.
@returns: list of table names
"""
tables: "list[str]" = []
for table, data in self._data[str(guild_id)][ # pylint: disable=unused-variable
"tables"
].items():
tables.append(table)
return tables
def remove_guild(self, guild_id: int, guild_name: str) -> None:
"""
Removes a guild's data from object. Saves backup.
@param guild_id: guild id to remove.
@param guild_name: name of guild for backup.
"""
old_data = self._data.pop(str(guild_id))
save_highscores_data(self._data)
if not os.path.exists("deleted_backups"):
os.mkdir("deleted_backups")
with open(f"deleted_backups/{guild_name}_backup.json", encoding="utf-8", mode="w") as file:
file.write(json.dumps(old_data, indent=4))
def reset_guild_data(self, guild_id: int) -> None:
"""
Resets highscore data for a guild. Should only be used for development really...
@param guild_id: guild id to reset data for
"""
guild_data: dict = self._data[str(guild_id)]
for boss, boss_data in guild_data["tables"].items():
for category, category_data in boss_data["categories"].items():
category_data = []
boss_data["categories"][category] = category_data
guild_data["tables"][boss] = boss_data
self._data[str(guild_id)] = guild_data
save_highscores_data(self._data)
def set_guild_highscore_channel(self, guild_id: int, channel_id: int) -> None:
"""
Sets the guild's highscores channel.
@param guild_id: guild id to set to.
@param channel_id: channel id to set.
"""
self._data[str(guild_id)]["highscore_channel_id"] = channel_id
def set_guild_submission_channel(self, guild_id: int, channel_id: int) -> None:
"""
Sets the guild's submission channel.
@param guild_id: guild id to set to.
@param channel_id: channel id to set.
"""
self._data[str(guild_id)]["submission_channel_id"] = channel_id
def set_guild_verification_channel(self, guild_id: int, channel_id: int) -> None:
"""
Sets the guild's verification channel.
@param guild_id: guild id to set to.
@param channel_id: channel id to set.
"""
self._data[str(guild_id)]["verification_channel_id"] = channel_id
def set_table_message_id(self, guild_id: int, table: str, message_id: int) -> None:
"""
Sets the message id for given guild's table.
Saves highscores data.
@param guild_id: guild id to set to.
@param table: the table to set to.
@param message_id: message id to set.
"""
self._data[str(guild_id)]["tables"][table]["message_id"] = message_id
save_highscores_data(self._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 open_highscores_data():
"""
Opens a highscores_data.json file if one exists, otherwise returns empty dict.
@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:
data = {}
return data
def save_highscores_data(data: dict):
"""
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))
+184 -50
View File
@@ -3,69 +3,199 @@ Contains various utlity functions for highscores bot
""" """
import nextcord import nextcord
from highscores import highscores_data # pylint: disable=import-error
from highscores import highscores_message_map # pylint: disable=import-error from classes.category import Category # pylint: disable=import-error
from classes.guild import Guild # pylint: disable=import-error
from classes.score import Score # pylint: disable=import-error
from classes.table import Table # pylint: disable=import-error
from highscores import highscores_data, highscores_message_map # pylint: disable=import-error
def format_highscore_message(boss_name: str, guild_id: int): def autofill_categories(guild_id: int, table_name: str, category_begin: str) -> "list[str]":
""" """
returns a formatted string for a boss's highscore Returns all category names that start with category_begin.
@param boss: the dictionary object for the boss If category_begin is empty, returns list of all category names.
@param guild_id: the guild id the message is for
@return the formatted string Args:
guild_id (int): Guild id that holds the table and categories.
table_name (str): Table name to search in.
category_begin (str): The beginning of a category name to search with. Can be empty.
Returns:
list[str]: List of all category names that start with category_begin.
""" """
ret_string = f"**{boss_name}**\n```"
categories_list = highscores_data.get_guild(guild_id).get_table(table_name).get_categories().keys()
# if string empty, return all category names
if not category_begin:
return categories_list
# else find all categories that start with category_begin
categories = [category for category in categories_list if category.lower().startswith(category_begin.lower())]
return categories
def autofill_category_users(guild_id: int, table_name: str, category_name: str, user_begin: str) -> "list[str]":
"""
Returns list of users that are in the table and category that also start with the user_begin string.
Args:
guild_id (int): Guild id corresponding to table and category.
table_name (str): Table to look in for category.
category_name (str): Category to look in for users.
user_begin (str): Beginning of user name to search with. Can be empty.
Returns:
list[str]: List of users that start with user_begin.
"""
try:
users_list: "list[str]" = (
highscores_data.get_guild(guild_id).get_table(table_name).get_category(category_name).get_users()
)
# if string empty, return all users
if not user_begin:
return users_list
# else find all users that start with user_begin
users = [user for user in users_list if user.lower().startswith(user_begin.lower())]
return users
except AttributeError:
return []
def autofill_tables(guild_id: int, table_begin: str) -> "list[str]":
"""
Returns all table names that start with table_begin. If table_begin is empty, returns list of all table names.
Args:
guild_id (int): Guild id that holds the tables.
table_begin (str): The beginning of a table name to search with. Can be empty.
Returns:
list[str]: List of table names that start with table_begin.
"""
tables_list = highscores_data.get_guild(guild_id).get_tables().keys()
# if string empty, return all table names
if not table_begin:
return tables_list
# else find all tables that start with table_begin
tables = [table for table in tables_list if table.lower().startswith(table_begin.lower())]
return tables
def autofill_users(guild_id: int, table_name: str, category_name: str, user_begin: str) -> "list[str]":
"""
Returns list of users that match user_begin. Either all users in table or just users in category if category is
None. Might be sexpensive due to unioning all users in table.
Args:
guild_id (int): Guild id corresponding to table and category.
table_name (str): Table to look in for category.
category_name (str): Category to look in for users. Can be None if not looking for category.
user_begin (str): Beginning of user name to search with. Can be empty.
Returns:
list[str]: List of users that start with user_begin.
"""
try:
# If no category name, get all users in table.
if category_name is None:
user_set: set = set()
for category in highscores_data.get_guild(guild_id).get_table(table_name).get_categories().values():
user_set = user_set | set(category.get_users())
users_list = list(user_set)
else:
users_list: "list[str]" = (
highscores_data.get_guild(guild_id).get_table(table_name).get_category(category_name).get_users()
)
# if string empty, return all users
if not user_begin:
return users_list
# else find all users that start with user_begin
users = [user for user in users_list if user.lower().startswith(user_begin.lower())]
return users
except AttributeError:
return []
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 # Gather all categories and add to string
for key, value in highscores_data.get_categories_data(guild_id, boss_name): for category in table.categories:
ret_string += f"{key}".ljust(26, " ") ret_string += f"{category.name}".ljust(guild.category_pad_length, " ")
ret_string += "\n" ret_string += "\n"
highscore_size = highscores_data.get_config_highscore_size()
# list rankings in a single line. Start with first place for each category and then continue # list rankings in a single line. Start with first place for each category and then continue
for i in range(highscore_size): for i in range(table.size):
for key, value in highscores_data.get_categories_data(guild_id, boss_name): for category in table.categories:
if len(value) > i: if len(category.scores) > i:
score_pair = value[i]
# pad username to max length. # pad username to max length.
username: str = score_pair[0].ljust( # have to convert category.scores into list to make it subscriptable. no idea why.
highscores_data.get_guild_username_length(guild_id), " " username: str = list(category.scores)[i].user.ljust(guild.username_length, " ")
)
score: str = str(score_pair[1]) score: str = list(category.scores)[i].get_str()
# pad score to correct length # pad score to correct length
# MM:SS.ms is 7 characters long # MM:SS.ms is 7 characters long
if highscores_data.get_config_time_record(key): if category.time_record:
# if there are single digit minutes, add a space for formatting. # if there are single digit minutes, add a space for formatting.
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2: if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score score = " " + score
else: else:
score = score.rjust(7, " ") score = score.rjust(7, " ")
ret_string += f"{i+1}: {username} - {score}".ljust(26, " ") ret_string += f"{i+1}: {username} - {score}".ljust(guild.category_pad_length, " ")
else: else:
ret_string += f"{i+1}: submit your score".ljust(26, " ") ret_string += f"{i+1}:".ljust(guild.category_pad_length, " ")
ret_string += "\n" ret_string += "\n"
return ret_string + "```" return ret_string + "```"
async def send_highscore_message( async def send_highscore_message(channel: nextcord.TextChannel, guild: Guild, table: Table) -> nextcord.Message:
channel: nextcord.TextChannel, boss_name: str, guild_id: int
) -> nextcord.Message:
""" """
Function takes care of checking if a message exists for a boss, then Function takes care of checking if a message exists for a table.
creating and sending/editing the message Creates and sends/edits the message.
@param channel: the channel to send the message in/search for message id
@param boss: the boss to send the message about
@param guild_id: the guild id to send message to
@return: The nextcord message
"""
highscore_string = format_highscore_message(boss_name, guild_id)
message_id = highscores_data.get_table_message_id(guild_id, boss_name) 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: try:
message: nextcord.Message = await channel.fetch_message(message_id) message: nextcord.Message = await channel.fetch_message(message_id)
await message.edit(content=highscore_string) await message.edit(content=highscore_string)
@@ -73,38 +203,42 @@ async def send_highscore_message(
# message doesn't exist. # message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string) message: nextcord.Message = await channel.send(highscore_string)
message_id = message.id message_id = message.id
highscores_data.set_table_message_id(guild_id, boss_name, message_id) table.message_id = message_id
# save message -> boss_name in message_map # save message -> boss_name in message_map
# get guild id from channel # get guild id from channel
# channel should come from the guild that is requesting this highscores message anyways. # channel should come from the guild that is requesting this highscores message anyways.
# check if guild is in message map already # check if guild is in message map already
if not channel.guild.id in highscores_message_map: if not channel.guild.id in highscores_message_map:
highscores_message_map[channel.guild.id] = {} highscores_message_map[channel.guild.id] = {}
highscores_message_map[channel.guild.id][message_id] = boss_name highscores_message_map[channel.guild.id][message_id] = table.name
return message return message
async def submit_score( async def submit_score(
interaction: nextcord.Interaction, user: str, boss_name: str, category: str, score: str interaction: nextcord.Interaction, guild: Guild, table: Table, category: Category, score: Score
): ) -> None:
""" """
Adds a score to the high scores and sorts. Removes a score if larger than size limit. Adds passed score to table. Then updates table message by calling send_highscore_message.
@param boss: the boss to add score to Ensures that a highscores channel exists.
@param category: the category to add score to
@param user: user who submitted score Args:
@param score: score to add 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
""" """
highscores_data.add_score(interaction.guild_id, boss_name, category, user, score) category.add_score(score)
highscores_data.save_guild_data(guild)
# edit message # edit message
highscore_channel_id = highscores_data.get_guild_highscores_channel(interaction.guild_id) print(f"Editting message {guild.highscore_channel}")
print(f"Editting message {highscore_channel_id}") channel = interaction.guild.get_channel(guild.highscore_channel)
channel = interaction.guild.get_channel(highscore_channel_id)
error_response = "Registered Highscores channel does not exist or was never registered." error_response = "Registered Highscores channel does not exist or was never registered."
error_response += 'Register with "?register" command.' error_response += 'Register with "?register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True) assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(channel, boss_name, interaction.guild_id) await send_highscore_message(channel, guild, table)