diff --git a/classes/category.py b/classes/category.py index e442f8b..cb00c5e 100644 --- a/classes/category.py +++ b/classes/category.py @@ -128,9 +128,7 @@ class Category: 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) - ) + self._scores_map = dict(sorted(self._scores_map.items(), key=lambda x: x[1].value, reverse=not self.ascending)) def clear(self) -> None: """ @@ -139,6 +137,15 @@ class Category: """ 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. @@ -153,15 +160,19 @@ class Category: return self._scores_map[user] return None - def remove_score(self, score: str) -> None: + def remove_score(self, user: str) -> Score: """ Removes a user's score from dictionary storage. Args: - score (str): user whose score to remove + user (str): user whose score to remove + + Returns: + Score: User's Score or None """ - score = self._scores_map.pop(score) - del score + if user in self._scores_map: + return self._scores_map.pop(user) + return None def to_dict(self) -> dict: """ diff --git a/classes/guild.py b/classes/guild.py index 30c1177..bcdd4fe 100644 --- a/classes/guild.py +++ b/classes/guild.py @@ -16,6 +16,7 @@ class Guild: 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 @@ -45,6 +46,7 @@ class Guild: 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"] @@ -56,10 +58,30 @@ class Guild: 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 + Getter for Guild id. Returns: int: Guild id @@ -180,6 +202,17 @@ class Guild: 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. @@ -190,6 +223,7 @@ class Guild: """ 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"] = {} diff --git a/classes/score.py b/classes/score.py index 1f184f8..c6f4344 100644 --- a/classes/score.py +++ b/classes/score.py @@ -38,6 +38,16 @@ class Score: """ 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: """ diff --git a/classes/table.py b/classes/table.py index 0f2af7e..e12224e 100644 --- a/classes/table.py +++ b/classes/table.py @@ -123,6 +123,16 @@ class Table: 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. diff --git a/cogs/commands/__init__.py b/cogs/commands/__init__.py new file mode 100644 index 0000000..d383dad --- /dev/null +++ b/cogs/commands/__init__.py @@ -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()) diff --git a/cogs/commands/data_management.py b/cogs/commands/data_management.py new file mode 100644 index 0000000..8bce454 --- /dev/null +++ b/cogs/commands/data_management.py @@ -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) diff --git a/cogs/commands/message_management.py b/cogs/commands/message_management.py new file mode 100644 index 0000000..27b609b --- /dev/null +++ b/cogs/commands/message_management.py @@ -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)) diff --git a/cogs/register/__init__.py b/cogs/commands/settings_management.py similarity index 54% rename from cogs/register/__init__.py rename to cogs/commands/settings_management.py index 40ee7c8..22f251c 100644 --- a/cogs/register/__init__.py +++ b/cogs/commands/settings_management.py @@ -1,26 +1,27 @@ """ -Bot command for registering various channels to the bot. +Commands for managing the Guild's settings. """ +from typing import Optional -from nextcord.ext import commands import nextcord +from nextcord.ext import commands from highscores import highscores_data # pylint: disable=import-error -class Register(commands.Cog, name="Register"): +class SettingsManagementCog(commands.Cog, name="Settings Management"): """ - Class holds various register commands. + Class holds commands for managing Guild's settings. """ def __init__(self) -> None: """ - Default constructor for Register class. Does nothing. + Default constructor for SettingsManagement class. Does nothing. """ @nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True)) - async def register(self): + async def register(self, interaction: nextcord.Interaction): """ Parent slash command, never called. """ @@ -29,7 +30,7 @@ class Register(commands.Cog, name="Register"): async def highscore( self, interaction: nextcord.Interaction, - channel: nextcord.abc.GuildChannel = nextcord.SlashOption( + channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption( description="Channel to register", required=False, channel_types=[nextcord.ChannelType.text], @@ -56,7 +57,7 @@ class Register(commands.Cog, name="Register"): async def submission( self, interaction: nextcord.Interaction, - channel: nextcord.abc.GuildChannel = nextcord.SlashOption( + channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption( description="Channel to register", required=False, channel_types=[nextcord.ChannelType.text], @@ -68,7 +69,7 @@ class Register(commands.Cog, name="Register"): Args: interaction (nextcord.Interaction): Interaction that invoked the command - channel (nextcord.abc.GuildChannel, optional): Channel to register + channel Optional[nextcord.abc.GuildChannel], optional): Channel to register Is a SlashOption. Limited to text channels, Defaults to None. """ if not channel: @@ -83,7 +84,7 @@ class Register(commands.Cog, name="Register"): async def verify( self, interaction: nextcord.Interaction, - channel: nextcord.abc.GuildChannel = nextcord.SlashOption( + channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption( description="Channel to register", required=False, channel_types=[nextcord.ChannelType.text], @@ -95,7 +96,7 @@ class Register(commands.Cog, name="Register"): Args: interaction (nextcord.Interaction): Interaction that invoked the command - channel (nextcord.abc.GuildChannel, optional): Channel to register + channel (Optional[nextcord.abc.GuildChannel], optional): Channel to register Is a SlashOption. Limited to text channels, Defaults to None. """ if not channel: @@ -106,9 +107,53 @@ class Register(commands.Cog, name="Register"): 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. + """ -# This function will be called when this extension is loaded. -# It is necessary to add these functions to the bot. -def setup(bot: commands.Bot) -> None: - """Adds function to bot""" - bot.add_cog(Register()) + @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) diff --git a/cogs/commands/views/update_username_verify.py b/cogs/commands/views/update_username_verify.py new file mode 100644 index 0000000..4695865 --- /dev/null +++ b/cogs/commands/views/update_username_verify.py @@ -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) diff --git a/cogs/delete_scores/__init__.py b/cogs/delete_scores/__init__.py deleted file mode 100644 index 9c417b5..0000000 --- a/cogs/delete_scores/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Bot command for deleting the guild's scores. -""" - -from nextcord.ext import commands -import nextcord - -from highscores import highscores_data # pylint: disable=import-error - - -class DeleteScores(commands.Cog, name="Delete Scores"): - """ - Warning: This command only works for developers. - Deletes this guild's scores. - """ - - def __init__(self): - """ - Default constructor for DeleteScores class. Does nothing. - """ - - @nextcord.slash_command( - description="Deletes all scores for this guild. Used for testing.", - default_member_permissions=nextcord.Permissions(administrator=True), - ) - async def delete_scores(self, interaction: nextcord.Interaction): - """ - A command which deletes all scores in all tables for this guild. - Command limits to admins to prevent devs from usings on non-test servers. - """ - # only allow AlwaysCompiling to use. - # If you are a developer and want to use command, can add name to list - if interaction.user.id == 206602713511886849: - for table in highscores_data.get_guild(interaction.guild_id).tables: - for category in table.categories: - category.clear() - highscores_data.save_guild_data(highscores_data.get_guild(interaction.guild_id)) - await interaction.response.send_message( - f"Cleared score data for {interaction.guild.name}.", ephemeral=True - ) - else: - await interaction.response.send_message( - "You shouldn't be using this message.", ephemeral=True - ) - - -# This function will be called when this extension is loaded. -# It is necessary to add these functions to the bot. -def setup(bot: commands.Bot): - """Adds function to bot""" - bot.add_cog(DeleteScores()) diff --git a/cogs/post_highscores/__init__.py b/cogs/post_highscores/__init__.py deleted file mode 100644 index b6b39bd..0000000 --- a/cogs/post_highscores/__init__.py +++ /dev/null @@ -1,68 +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) -> None: - """ - Default constructor for PostHighscores class - - Args: - bot (commands.Bot): Bot object. Required to register submission button view. - """ - self.bot = bot - - @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. - """ - 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.response(error_response, ephemeral=True) - - # awaiting response because posting scores takes a while - await interaction.response.defer(ephemeral=True) - - for table in highscores_data.get_guild(interaction.guild_id).tables: - message: nextcord.Message = await send_highscore_message(channel, guild, table) - await message.edit(view=SubmissionCreateButton(self.bot)) - - # have to save data here in case message id was changed. rather save here after all - # messages have been sent. - highscores_data.save_guild_data(guild) - - await interaction.followup.send(f"Posted all tables to <#{channel.id}>.", ephemeral=True) - - -# This function will be called when this extension is loaded. -# It is necessary to add these functions to the bot. -def setup(bot: commands.Bot): - """Adds function to bot""" - bot.add_cog(PostHighscores(bot)) diff --git a/cogs/submission/submission_views.py b/cogs/submission/submission_views.py index 35a4989..2398393 100644 --- a/cogs/submission/submission_views.py +++ b/cogs/submission/submission_views.py @@ -126,7 +126,7 @@ class SubmitButton(nextcord.ui.Button): files=[await attch.to_file() for attch in interaction.message.attachments], view=VerificationView(), 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) submission_objects.remove_user_submission(interaction.user.id) diff --git a/cogs/submission/verification_view.py b/cogs/submission/verification_view.py index 3568706..c073490 100644 --- a/cogs/submission/verification_view.py +++ b/cogs/submission/verification_view.py @@ -59,7 +59,9 @@ class VerificationView(nextcord.ui.View): submission_category, submission_score, ) - await interaction.message.edit(f"Approved by <@{interaction.user.id}>.", view=None) + content = interaction.message.content + content += f"\nApproved by <@{interaction.user.id}>." + await interaction.message.edit(content=content, view=None) except UnboundLocalError: await interaction.send("Instruct user to submit with all fields.") await interaction.message.edit(view=None) diff --git a/requirements.txt b/requirements.txt index 95e4c61..412a87a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -nextcord>=2.1.0 -python-dotenv>=0.20.0 \ No newline at end of file +nextcord>=2.3.2 +python-dotenv>=0.20.0 diff --git a/utilities/utils.py b/utilities/utils.py index fae5544..5be80b8 100644 --- a/utilities/utils.py +++ b/utilities/utils.py @@ -3,11 +3,134 @@ Contains various utlity functions for highscores bot """ import nextcord -from highscores import highscores_data, highscores_message_map # pylint: disable=import-error -from classes.score import Score # pylint: disable=import-error -from classes.guild import Guild # pylint: disable=import-error + from classes.category import Category # pylint: disable=import-error +from classes.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 autofill_categories(guild_id: int, table_name: str, category_begin: str) -> "list[str]": + """ + Returns all category names that start with category_begin. + If category_begin is empty, returns list of all category names. + + 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. + """ + + 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: @@ -27,7 +150,7 @@ def format_highscore_message(guild: Guild, table: Table) -> str: # Gather all categories and add to string for category in table.categories: - ret_string += f"{category.name}".ljust(26, " ") + ret_string += f"{category.name}".ljust(guild.category_pad_length, " ") ret_string += "\n" # list rankings in a single line. Start with first place for each category and then continue @@ -49,17 +172,15 @@ def format_highscore_message(guild: Guild, table: Table) -> str: else: 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: - ret_string += f"{i+1}: submit your score".ljust(26, " ") + ret_string += f"{i+1}:".ljust(guild.category_pad_length, " ") ret_string += "\n" return ret_string + "```" -async def send_highscore_message( - channel: nextcord.TextChannel, guild: Guild, table: Table -) -> nextcord.Message: +async def send_highscore_message(channel: nextcord.TextChannel, guild: Guild, table: Table) -> nextcord.Message: """ Function takes care of checking if a message exists for a table. Creates and sends/edits the message.