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
19 changed files with 764 additions and 258 deletions
+18 -7
View File
@@ -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:
"""
+35 -1
View File
@@ -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"] = {}
+10
View File
@@ -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:
"""
+10
View File
@@ -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.
+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))
@@ -1,32 +1,27 @@
"""
Bot command for registering various channels to the bot.
Commands for managing the Guild's settings.
"""
from typing import Optional
from nextcord import Embed, Interaction, Permissions, SlashOption
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.
Args:
commands.Cog: Parent class
name (str, optional): Name of Cog. Defaults to "Register".
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=Permissions(administrator=True))
async def register(self):
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def register(self, interaction: nextcord.Interaction):
"""
Parent slash command, never called.
"""
@@ -35,7 +30,7 @@ class Register(commands.Cog, name="Register"):
async def highscore(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel = SlashOption(
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
@@ -62,7 +57,7 @@ class Register(commands.Cog, name="Register"):
async def submission(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel = SlashOption(
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
@@ -74,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:
@@ -89,7 +84,7 @@ class Register(commands.Cog, name="Register"):
async def verify(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel = SlashOption(
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
@@ -101,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:
@@ -112,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)
@@ -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)
-42
View File
@@ -1,42 +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
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:
for table in highscores_data.get_guild(ctx.guild.id).tables:
for category in table.categories:
category.clear()
highscores_data.save_guild_data(highscores_data.get_guild(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))
-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
-57
View File
@@ -1,57 +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
```
"""
guild = highscores_data.get_guild(ctx.guild.id)
print(f"Printing out highscores information in channel {guild.highscore_channel}")
channel = ctx.guild.get_channel(guild.highscore_channel)
error_response = "Registered Highscores channel does not exist or was never registered. "
error_response += 'Register with "?register" command.'
assert channel is not None, await ctx.send(error_response)
if len(highscores_data.get_guild(ctx.guild.id).tables) < 1:
await channel.send(
"No tables have been created for this guild. Please create some tables."
)
return
for table in highscores_data.get_guild(ctx.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)
# 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))
+7 -4
View File
@@ -35,14 +35,17 @@ class BotPresence(commands.Cog, name="Bot Presence"):
async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
f"?help | {len(self.bot.guilds)} servers",
f"Vuln Pls. | {len(self.bot.guilds)} servers",
f"All your base are belong to us. | {len(self.bot.guilds)} servers",
f"Double plus good. | {len(self.bot.guilds)} servers",
"Now with slash commands!",
"Vuln Pls.",
"All your base are belong to us.",
"Double plus good.",
"That's a lot of damage!",
]
status = random.choice(statuses)
status += f" | {len(self.bot.guilds)} servers"
await self.bot.change_presence(activity=nextcord.Game(status))
+1 -1
View File
@@ -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)
+3 -1
View File
@@ -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)
+3 -3
View File
@@ -6,8 +6,8 @@ import logging
import os
import sys
import nextcord
from nextcord.ext import commands
import nextcord
import config
from classes.highscores_data import HighscoresData
@@ -33,9 +33,9 @@ submission_objects: SubmissionMap = SubmissionMap()
def main():
"""Main function for bot."""
min_python_version = (3, 7)
min_python_version = (3, 8)
if sys.version_info < min_python_version:
print("ERROR: Python version must be >= 3.7")
print("ERROR: Python version must be >= 3.8")
sys.exit()
# Allows privledged intents for monitoring members joining, roles editing, and role assignments
+2 -2
View File
@@ -1,2 +1,2 @@
nextcord>=2.0.0rc1
python-dotenv>=0.20.0
nextcord>=2.3.2
python-dotenv>=0.20.0
+130 -9
View File
@@ -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.