Compare commits

..
Author SHA1 Message Date
alwayscompiling 0bae0ea5ff Remove custom mysqldb image, just use latest public. Move bot docker to base directory. 2022-12-10 16:12:56 -05:00
alwayscompiling c67e9ae617 Use pymysql for sql connection. 2022-12-10 16:12:21 -05:00
alwayscompiling 7e23ac14ff Create dockerfile for bot. Add bot to compose. 2022-12-10 12:46:12 -05:00
alwayscompiling a0a7e37921 Remove guild join stuff again. 2022-12-09 16:43:56 -05:00
alwayscompiling c01c888503 Merge branch '21-allow-users-to-modify-tablescategories' into develop 2022-12-08 23:17:21 -05:00
alwayscompiling 58dbe547bb Add submit button on group create. 2022-12-08 23:16:56 -05:00
alwayscompiling 3cf4fc6b33 Merge branch '21-allow-users-to-modify-tablescategories' into develop 2022-12-08 23:09:07 -05:00
alwayscompiling 411f8ab275 Add a deletion confirmation. 2022-12-08 22:59:00 -05:00
alwayscompiling 8952c4a352 Remove onjoin message. 2022-12-08 13:44:34 -05:00
alwayscompiling 93248797a0 Fill out rest of group and category command. Fill out doc strings. 2022-12-08 13:41:15 -05:00
alwayscompiling dace72f5cc Correct empty message id handling. 2022-12-08 13:28:13 -05:00
alwayscompiling f468a028d0 Changed send_highscore_message to take guild rather than channel. 2022-12-08 13:02:39 -05:00
alwayscompiling a9b889234a Start filling out create category. 2022-12-07 22:40:47 -05:00
alwayscompiling 88cec73644 Create command skeletons. Update set to conform to min and max values decided in create. 2022-12-07 22:24:38 -05:00
alwayscompiling 91e57c6555 Standardize autocomplete function names. 2022-12-07 21:52:36 -05:00
alwayscompiling 6c5f40f788 Refractor command files into single command files. 2022-12-07 21:46:04 -05:00
alwayscompiling a9920db381 Don't auto add runescape groups on join. 2022-12-07 20:54:49 -05:00
alwayscompiling d04b8c5e32 Squashed commit of the following:
commit 3627419e12
Merge: 2db744d 3446ce9
Author: alwayscompiling <[email protected]>
Date:   Tue Dec 6 22:36:51 2022 -0500

    Merge branch '45-sql-connection-timing-out' into develop

commit 3446ce9ca2
Author: alwayscompiling <[email protected]>
Date:   Tue Dec 6 22:27:05 2022 -0500

    Change session usage to on use basis.

commit 2db744d170
Author: alwayscompiling <[email protected]>
Date:   Sun Dec 4 21:44:54 2022 -0500

    Add restart policy.

commit fa33be1b4a
Author: alwayscompiling <[email protected]>
Date:   Sun Dec 4 21:25:57 2022 -0500

    Renamed docker compose file.
2022-12-06 23:02:01 -05:00
alwayscompiling 3627419e12 Merge branch '45-sql-connection-timing-out' into develop 2022-12-06 22:36:51 -05:00
alwayscompiling 582b88e248 Create set command over Settings Management. 2022-12-04 22:42:55 -05:00
alwayscompiling 2db744d170 Add restart policy. 2022-12-04 21:44:54 -05:00
alwayscompiling fa33be1b4a Renamed docker compose file. 2022-12-04 21:25:57 -05:00
18 changed files with 720 additions and 353 deletions
+21
View File
@@ -0,0 +1,21 @@
FROM python:3.8.10-slim
# install dependencies for mysqlclient
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
RUN pip install --upgrade pip
WORKDIR /home
COPY classes classes/
COPY cogs cogs/
COPY utilities utilities/
COPY highscores.py highscores.py
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
# remove build deps that aren't needed to run
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "./highscores.py"]
+10 -7
View File
@@ -4,10 +4,11 @@ Cog loader. Adds all command cogs to the bot.
from nextcord.ext import commands
# Command classes
from .data_management import DataManagementCog
from .message_management import MessageManagementCog
from .settings_management import SettingsManagementCog
from .create import CreateCog
from .delete import DeleteCog
from .refresh import RefreshCog
from .set import SetCog
from .update import UpdateCog
# This function will be called when this extension is loaded.
@@ -16,6 +17,8 @@ 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())
bot.add_cog(CreateCog(bot))
bot.add_cog(DeleteCog())
bot.add_cog(RefreshCog(bot))
bot.add_cog(SetCog())
bot.add_cog(UpdateCog(bot))
+172
View File
@@ -0,0 +1,172 @@
"""
Commands for creating items in bot.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from sqlalchemy import exc, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from cogs.submission.submission_views import SubmissionCreateButton
from utilities.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
class CreateCog(commands.Cog, name="Create Cog"):
"""
Class holds commands for creating things.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for CreateCog class.
"""
self._bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def create(self, interaction: nextcord.Interaction):
"""
Parent slash command for creating things. Never called.
"""
# need all these arguments because discord command.
# pylint: disable=too-many-arguments
@create.subcommand(description="Create a new Category. Must not exist already in Group.")
async def category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True),
category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True),
is_ascending: bool = nextcord.SlashOption(description="If higher value is better score.", required=True),
is_time_record: bool = nextcord.SlashOption(
description="If the score should be shown in time format. Cannot be changed.", required=True
),
is_validated: bool = nextcord.SlashOption(
description="If scores need to be validated before posted.", required=True
),
size: int = nextcord.SlashOption(
description="How many scores to post in Category. Default is 3.", required=False, default=3, min_value=1
),
) -> None:
"""
Creates a new category to be placed into group_id group. Fails if the category name already exists within the
group. Then calls send_highscore_message().
Args:
interaction (nextcord.Interaction): Interaction for the command.
group_id (str): Id of group to add category to. Is SlashOption.
category_name (str): Name of new category. Is SlashOption.
is_ascending (bool): If the scores are sorted ascending or not. Is SlashOption.
is_time_record (bool): If the scores are a time record or not. Is SlashOption.
is_validated (bool): If the scores should be validated or not. Is SlashOption.
size (int): How many sores to show in the category message. Is SlashOption.
"""
new_category = Category(
CategoryName=category_name,
GroupId=int(group_id),
Size=size,
IsAscending=is_ascending,
IsTimeRecord=is_time_record,
IsValidated=is_validated,
)
# Open connection to engine and add new category.
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
try:
session.add(new_category)
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'"{category_name}" has been added to "{group.GroupName}"!', ephemeral=True
)
except exc.IntegrityError:
# Category name already exists in group
session.rollback()
await interaction.response.send_message(
f'"{category_name}" already exists in "{group.GroupName}". Please rename.', ephemeral=True
)
# need all these arguments because discord command.
# pylint: disable=too-many-arguments
@create.subcommand(description="Create a new Category Group. Must not exist already for Guild.")
async def group(
self,
interaction: nextcord.Interaction,
group_name: str = nextcord.SlashOption(description="Name of new Group.", required=True),
pad_length: int = nextcord.SlashOption(
description="Number of characters wide a category is printed. Default is 26.",
required=False,
default=26,
min_value=7,
max_value=MAX_LINE_LENGTH,
),
username_length: int = nextcord.SlashOption(
description="Number of characters of a username to display. Default is 12.",
required=False,
default=12,
min_value=1,
max_value=30,
),
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to post Group in. If left empty, defaults to current channel.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Creates a new category group to be placed into the database with the guild's id. Fails if the category group
name already exists within the guild. Then calls send_highscore_message().
Args:
interaction (nextcord.Interaction): Interaction for the command.
group_name (str): Name of new Group. Is SlashOption.
pad_length (int): Number of characters wide a category is printed. Is SlashOption.
username_length (int): Number of characters of a username to display. Is SlashOption.
channel (Optional[nextcord.abc.GuildChannel]): Channel to post group in. If None, defaults to current
channel. Is SlashOption.
"""
# if no channel provided, use current channel.
if channel is None:
channel = interaction.channel
new_group = CategoryGroup(
GroupName=group_name,
ChannelId=channel.id,
GuildId=interaction.guild_id,
PadLength=pad_length,
UsernameLength=username_length,
)
with Session(engine) as session:
try:
session.add(new_group)
session.commit()
message: nextcord.Message = await send_highscore_message(session, interaction.guild, new_group)
await message.edit(view=SubmissionCreateButton(self._bot))
await interaction.response.send_message(f'"{group_name}" has been created!', ephemeral=True)
except exc.IntegrityError:
# Category Group already exists in guild
session.rollback()
await interaction.response.send_message(
f'"{group_name}" already exists in "{interaction.guild.name}". Please rename.', ephemeral=True
)
@category.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@@ -1,5 +1,5 @@
"""
Commands for managing the tables/categories/scores for a Guild.
Commands for deleting groups/categories/scores in database and messages in discord..
"""
from typing import Optional
@@ -10,7 +10,7 @@ from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, Guild, Score, engine
from utilities.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import (
autofill_categories,
autofill_groups,
@@ -19,26 +19,18 @@ from utilities.utils import (
send_highscore_message,
)
from .views.update_username_verify import UpdateUsernameVerifyView
from .views.delete_confirm import DeleteConfirmView, create_delete_id
class DataManagementCog(commands.Cog, name="Table Management"):
class DeleteCog(commands.Cog, name="Table Management"):
"""
Class holds commands for managing the data storage of the highscores.
Class holds commands for deleting.
"""
def __init__(self, bot: commands.Bot):
def __init__(self) -> None:
"""
Default constructor for DataManagement class. Sets internal bot variable.
Default constructor for DeleteCog class.
"""
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):
@@ -47,15 +39,60 @@ class DataManagementCog(commands.Cog, name="Table Management"):
"""
@delete.subcommand()
async def user(self, interaction: nextcord.Interaction):
async def all(self, interaction: nextcord.Interaction):
"""
Subcommand parent for deleting user information. Never called.
Subcommand parent for deleting all of a thing. Never called.
"""
@user.subcommand(
@all.subcommand(description="Clears highscores channel of all table messages. Does not change data.")
async def channel_messages(
self,
interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to clear of messages.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Deletes all group messages from channel. Does not change data.
Does not delete highscores channel id. Uses current channel if not supplied a channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (Optional[nextcord.abc.GuildChannel]): Channel to clear of messages.
Is a SlashOption. Limited to text channels. Defaults to None.
"""
if not channel:
channel = interaction.channel
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.ChannelId == channel.id)
)
# TODO test when nothing in channel
groups = session.scalars(stmt)
await interaction.response.defer(ephemeral=True)
for group in groups:
try:
table_message: nextcord.Message = await channel.fetch_message(group.MessageId)
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)
@all.subcommand(
description="Removes all of a user's scores. Warning: data is destroyed, use with case.",
)
async def from_all(
async def users(
self,
interaction: nextcord.Interaction,
user_name: str = nextcord.SlashOption(
@@ -85,10 +122,50 @@ class DataManagementCog(commands.Cog, name="Table Management"):
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids))
for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild.get_channel(group.ChannelId), group)
await send_highscore_message(session, interaction.guild, group)
await interaction.send(f'User "{user_name}" was removed from all tables.', ephemeral=True)
@delete.subcommand(description="Delete a category from a group. Deletion is perminant.")
async def category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete from.", required=True),
category_id: str = nextcord.SlashOption(name="category", description="Category to delete.", required=True),
) -> None:
"""
Calls the delete confirmation view for the given group and category.
Args:
interaction (nextcord.Interaction): Interaction of command.
group_id (str): Group to delete from. Is SlashOption.
category_id (str): Category to delete. Is SlashOption.
"""
await interaction.response.send_modal(
DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id)))
)
@delete.subcommand(description="Delete a group. Deletion is perminant.")
async def group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete.", required=True),
) -> None:
"""
Calls the delete confirmation view for the given group.
Args:
interaction (nextcord.Interaction): Interaction of command.
group_id (str): Group to delete from. Is SlashOption.
"""
await interaction.response.send_modal(DeleteConfirmView(custom_id=create_delete_id(int(group_id))))
@delete.subcommand()
async def user(self, interaction: nextcord.Interaction):
"""
Subcommand parent for deleting user information. Never called.
"""
@user.subcommand(description="Deletes a user's scores from a category.")
async def from_category(
self,
@@ -122,7 +199,7 @@ class DataManagementCog(commands.Cog, name="Table Management"):
session.delete(session.scalar(stmt))
session.commit()
await send_highscore_message(session, interaction.guild.get_channel(group.ChannelId), group)
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from "{category_name}".', ephemeral=True
)
@@ -153,14 +230,16 @@ class DataManagementCog(commands.Cog, name="Table Management"):
session.delete(score)
session.commit()
await send_highscore_message(session, interaction.guild.get_channel(group.ChannelId), group)
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from all categories in {group.GroupName}.', ephemeral=True
)
@from_category.on_autocomplete("group_id")
@from_group.on_autocomplete("group_id")
async def autocomplete_group(self, interaction: nextcord.Interaction, group_id: str):
@category.on_autocomplete("group_id")
@group.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Autocomplete function for table names.
@@ -171,7 +250,8 @@ class DataManagementCog(commands.Cog, name="Table Management"):
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@from_category.on_autocomplete("category_id")
async def autocomplete_category(self, interaction: nextcord.Interaction, category_id: str, group_id: str):
@category.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str):
"""
Autocomplete function for category names.
@@ -186,7 +266,7 @@ class DataManagementCog(commands.Cog, name="Table Management"):
@from_category.on_autocomplete("user_name")
@from_group.on_autocomplete("user_name")
async def autocomplete_user(
async def user_autocomplete(
self, interaction: nextcord.Interaction, user_name: str, group_id: str, category_id: Optional[str] = None
):
"""
@@ -205,40 +285,8 @@ class DataManagementCog(commands.Cog, name="Table Management"):
)
)
@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.
"""
with Session(engine) as session:
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verify_channel = interaction.guild.get_channel(session.scalar(stmt))
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)
@update_username.on_autocomplete("old_username")
@from_all.on_autocomplete("user_name")
async def username_autofill(self, interaction: nextcord.Interaction, username: str):
@users.on_autocomplete("user_name")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str):
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
-157
View File
@@ -1,157 +0,0 @@
"""
Commands for managing/displaying the highscores messages.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from cogs.submission.submission_views import SubmissionCreateButton
from utilities.sql_data_classes import CategoryGroup, engine
from utilities.utils import autofill_groups, send_highscore_message
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_channel_messages(
self,
interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to clear of messages.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Deletes all group messages from channel. Does not change data.
Does not delete highscores channel id. Uses current channel if not supplied a channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (Optional[nextcord.abc.GuildChannel]): Channel to clear of messages.
Is a SlashOption. Limited to text channels. Defaults to None.
"""
if not channel:
channel = interaction.channel
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.ChannelId == channel.id)
)
# TODO test when nothing in channel
groups = session.scalars(stmt)
await interaction.response.defer(ephemeral=True)
for group in groups:
try:
table_message: nextcord.Message = await channel.fetch_message(group.MessageId)
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_groups(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.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == interaction.guild_id)
# awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True)
for group in session.scalars(stmt):
channel = interaction.guild.get_channel(group.ChannelId)
# 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)
message: nextcord.Message = await send_highscore_message(session, channel, group)
await message.edit(view=SubmissionCreateButton(self.bot))
await interaction.send(f"Posted all groups in {interaction.guild.name}'s highscores.", 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_group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Name of Group.", required=True),
) -> None:
"""
Refreshes a table given by user.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): Str representation of Group id to refresh. Is SlashOption.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
channel = interaction.guild.get_channel(group.ChannelId)
message: nextcord.Message = await send_highscore_message(session, channel, group)
await message.edit(view=SubmissionCreateButton(self.bot))
await interaction.response.send_message(f"Updated table {group.GroupName}!", ephemeral=True)
@refresh_group.on_autocomplete("group_id")
async def refresh_table_autofill(self, interaction: nextcord.Interaction, group_id: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
+104
View File
@@ -0,0 +1,104 @@
"""
Commands for refreshing highscore messages.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from cogs.submission.submission_views import SubmissionCreateButton
from utilities.sql_data_classes import CategoryGroup, engine
from utilities.utils import autofill_groups, send_highscore_message
class RefreshCog(commands.Cog, name="Refresh Cog"):
"""
Class holds commands for refreshing.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for RefreshCog class
Args:
bot (commands.Bot): Bot object. Required to register submission button view.
"""
self._bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def refresh(self, interaction: nextcord.Interaction):
"""
Parent slash command for refreshing things. Never called.
"""
@refresh.subcommand(description="Manually refresh a table. Use when changing settings or data manually changed.")
async def group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Name of Group.", required=True),
) -> None:
"""
Refreshes a table given by user.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): Str representation of Group id to refresh. Is SlashOption.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
message: nextcord.Message = await send_highscore_message(session, interaction.guild, group)
await message.edit(view=SubmissionCreateButton(self._bot))
await interaction.response.send_message(f"Updated table {group.GroupName}!", ephemeral=True)
@refresh.subcommand()
async def all(self, interaction: nextcord.Interaction):
"""
Subcommand parent for refreshing all things. Never called.
"""
@all.subcommand(description="Triggers post/update of highscores to registered highscore channels.")
async def groups(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.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == interaction.guild_id)
# awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True)
for group in session.scalars(stmt):
message: nextcord.Message = await send_highscore_message(session, interaction.guild, group)
await message.edit(view=SubmissionCreateButton(self._bot))
await interaction.send(f"Posted all groups in {interaction.guild.name}'s highscores.", ephemeral=True)
@group.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@@ -1,5 +1,5 @@
"""
Commands for managing the Guild's settings.
Set command heirarchy.
"""
from typing import Optional
@@ -11,26 +11,26 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import CategoryGroup, Guild, engine
from utilities.utils import autofill_groups
from utilities.utils import MAX_LINE_LENGTH, autofill_groups
class SettingsManagementCog(commands.Cog, name="Settings Management"):
class SetCog(commands.Cog, name="Set command."):
"""
Class holds commands for managing Guild's settings.
Class holds commands for setting variables in guild's database.
"""
def __init__(self) -> None:
"""
Default constructor for SettingsManagement class. Does nothing.
Default constructor for SetCog class. Does nothing.
"""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def register(self, interaction: nextcord.Interaction):
async def set(self, interaction: nextcord.Interaction):
"""
Parent slash command, never called.
Parent slash command for setting settings. Never called.
"""
@register.subcommand(description="Registers the channel for a Category Group's messages.")
@set.subcommand(description="Registers the channel for a Category Group's messages.")
async def group_output_channel(
self,
interaction: nextcord.Interaction,
@@ -68,8 +68,47 @@ class SettingsManagementCog(commands.Cog, name="Settings Management"):
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@register.subcommand(description="Registers the channel for submission messages.")
async def submission(
@set.subcommand(
description="Set character length for a category. Can cut off username/score. Affects all categories."
)
async def pad_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Category Group to assign pad length to.", required=True
),
length: int = nextcord.SlashOption(
description="Number of characters alloted to a category.",
required=True,
default=26,
min_value=7,
max_value=MAX_LINE_LENGTH,
),
) -> None:
"""
Changes guild's category pad length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
group_id (str): String representation of Group id to set pad length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 26.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.PadLength = length
session.commit()
await interaction.response.send_message(
f"Category pad length set to {length} for {group.GroupName}", ephemeral=True
)
@set.subcommand(description="Registers the channel for submission messages.")
async def submission_channel(
self,
interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
@@ -99,8 +138,38 @@ class SettingsManagementCog(commands.Cog, name="Settings Management"):
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@register.subcommand(description="Registers the channel for verification messages.")
async def verify(
@set.subcommand(description="Set username length for a Group of Categories.")
async def username_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to change username length of.", required=True
),
length: int = nextcord.SlashOption(
description="Number of characters.", required=True, default=12, min_value=1, max_value=30
),
) -> None:
"""
Changes guild's username length limit.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
length (int): Number of characters. Is SlashOption. Defaults to 12.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.UsernameLength = length
session.commit()
await interaction.response.send_message(f"Username length set to {length}.", ephemeral=True)
@set.subcommand(description="Registers the channel for verification messages.")
async def verify_channel(
self,
interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
@@ -129,77 +198,10 @@ class SettingsManagementCog(commands.Cog, name="Settings Management"):
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@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,
group_id: str = nextcord.SlashOption(
name="group", description="Category Group to assign pad length to.", required=True
),
length: 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
group_id (str): String representation of Group id to set pad length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 26.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.PadLength = length
session.commit()
await interaction.response.send_message(
f"Category pad length set to {length} for {group.GroupName}", ephemeral=True
)
@set.subcommand(description="Set username length for a Group of Categories.")
async def username_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to change username length of.", required=True
),
length: 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 (int): Number of characters. Is SlashOption. Defaults to 12.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.UsernameLength = length
session.commit()
await interaction.response.send_message(f"Username length set to {length}.", ephemeral=True)
@group_output_channel.on_autocomplete("group_id")
@category_pad_length.on_autocomplete("group_id")
@pad_length.on_autocomplete("group_id")
@username_length.on_autocomplete("group_id")
async def group_id_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Sends results from autofill_groups back to group_output_channel for autocompletion of group_id.
+84
View File
@@ -0,0 +1,84 @@
"""
Commands for updating information/variables held in database.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Guild, engine
from utilities.utils import autofill_users_in_guild
from .views.update_username_verify import UpdateUsernameVerifyView
class UpdateCog(commands.Cog, name="Update commands."):
"""
Class holds commands and views needed for updating things in bot.
"""
def __init__(self, bot: commands.Bot):
"""
Default constructor for Update class. Sets internal bot variable.
Args:
bot (commands.Bot): Bot object. Required to register update username button view.
"""
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()
async def update(self, interaction: nextcord.Interaction):
"""
Parent slash command for updating things. Never called.
"""
@update.subcommand(description="Request a username update. Admins must approve.")
async def 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.
"""
with Session(engine) as session:
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verify_channel = interaction.guild.get_channel(session.scalar(stmt))
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)
@username.on_autocomplete("old_username")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str):
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args:
interaction (nextcord.Interaction): Interaction with command.
old_username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+97
View File
@@ -0,0 +1,97 @@
"""
Contains view and helper functions for confirming and processing deletes.
"""
from typing import Optional
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import send_highscore_message
class DeleteConfirmView(nextcord.ui.Modal):
"""
View modal popup for confirming deletions.
"""
def __init__(self, custom_id: str):
super().__init__(title="Warning, deletion is perminant.", custom_id=custom_id)
self.checker = nextcord.ui.TextInput(
label='Type "delete" to confirm deletion.',
required=True,
placeholder="delete",
style=nextcord.TextInputStyle.short,
)
self.add_item(self.checker)
async def callback(self, interaction: nextcord.Interaction):
if self.checker.value.lower() != "delete":
await interaction.response.send_message('Response was not "delete". Canceling delete.')
return
del_vals = parse_delete_id(self.custom_id)
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == del_vals["group_id"])
group = session.scalar(stmt)
if del_vals["category_id"] is None:
# delete entire group
session.delete(group)
# delete the message related to group
channel: nextcord.TextChannel = interaction.guild.get_channel(group.ChannelId)
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.delete()
else:
stmt = (
delete(Category)
.where(Category.GroupId == del_vals["group_id"])
.where(Category.CategoryId == del_vals["category_id"])
)
session.execute(stmt)
# update the message
await send_highscore_message(session, interaction.guild, group)
session.commit()
await interaction.response.send_message("Deletion complete.", ephemeral=True)
def create_delete_id(group_id: int, category_id: Optional[int] = None) -> str:
"""
Creates a delete id string to tell the view what to delete.
Args:
group_id (int): group id to delete
category_id (Optional[int]): category id to delete. Can be None.
Returns:
str: string representation of the delete id.
"""
return f"delete-{group_id}-{category_id}"
def parse_delete_id(delete_id: str) -> dict:
"""
Returns a dict containing the values of a delete id.
Args:
id (str): the delete id
Returns:
dict: dict containing values from a delete id.
"""
vals = {}
group_id = delete_id.split("-")[1]
category_id = delete_id.split("-")[2]
vals["group_id"] = group_id
if category_id == "None":
vals["category_id"] = None
else:
vals["category_id"] = category_id
return vals
@@ -63,7 +63,7 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids))
for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild.get_channel(group.ChannelId), group)
await send_highscore_message(session, interaction.guild, group)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
+1 -10
View File
@@ -19,22 +19,13 @@ class GuildListener(commands.Cog, name="Guild Listener"):
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild):
"""Listener for Guild Join. Creates skeleton for new guild joined and saves it."""
"""Listener for Guild Join. Adds guild to database."""
new_guild = Guild(GuildId=guild.id)
with Session(engine) as session:
session.add(new_guild)
session.commit()
# This add config will be removed eventually. Config adding will have to be controlled by commands.
add_preset_configuration(guild.id, "runescape")
welcome_message: str = "Hello, I keep track of Highscores for you!"
welcome_message += " Currently only supporting RuneScape boss highscores,"
welcome_message += " but will be more dynamic support in future."
welcome_message += ' For assistance on getting started, use "?register help".'
await guild.system_channel.send(content=welcome_message)
# This function will be called when this extension is loaded.
+27
View File
@@ -0,0 +1,27 @@
version: "3"
services:
sqldb:
restart: unless-stopped
image: mysql:latest
container_name: "Highscores-DataBase"
volumes:
- highscores_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${SQL_PASS}
- MYSQL_ROOT_USERNAME=${SQL_USER}
bot:
restart: unless-stopped
build: .
container_name: "Highscores-Bot"
depends_on:
- sqldb
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN}
- SQL_DB=${SQL_DB}
- SQL_HOST=${SQL_HOST}
- SQL_PASS=${SQL_PASS}
- SQL_USER=${SQL_USER}
volumes:
highscores_data:
-3
View File
@@ -1,3 +0,0 @@
FROM mysql:latest
COPY highscores_database.sql /docker-entrypoint-initdb.d/
-15
View File
@@ -1,15 +0,0 @@
version: "3"
services:
sqldb:
image: "highscores_db"
container_name: "Highscores-DataBase"
volumes:
- highscores_data:/var/lib/mysql
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=${SQL_PASS}
- MYSQL_ROOT_USERNAME=${SQL_USER}
volumes:
highscores_data:
+2 -1
View File
@@ -2,4 +2,5 @@ nextcord>=2.3.2
python-dotenv>=0.20.0
pandas>=1.5.2
sqlalchemy>=1.4.44
mysqlclient>-2.1.1
pymysql>=1.0.2
cryptography>=2.8
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.automap import automap_base
from utilities import config
# connect with sql alchemy and autoload schema
url = f"mysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
url = f"mysql+pymysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
engine = create_engine(url)
Base = automap_base()
Base.prepare(autoload_with=engine)
+10 -18
View File
@@ -298,32 +298,31 @@ def get_time_str(time_in: float) -> str:
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
async def send_highscore_message(
session: Session, channel: nextcord.TextChannel, group: CategoryGroup
) -> nextcord.Message:
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message:
"""
Function takes care of checking if a message exists for a group.
Creates and sends/edits the message.
Args:
session (Session): The sqlalchemy session being used for this function call.
channel (nextcord.TextChannel): Discord channel object where message is sent
group (CategoryGroup): group to post to the message
guild (nextcord.Guild): the nextcord.Guild object requesting this message.
group (CategoryGroup): group to post to the message.
Returns:
nextcord.Message: Message object that was posted with group contents
nextcord.Message: Message object that was posted with group contents.
"""
highscore_string = format_highscore_message(session, group)
channel = guild.get_channel(group.ChannelId)
try:
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.edit(content=highscore_string)
except nextcord.NotFound:
if group.MessageId is None:
# message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string)
group.MessageId = message.id
session.commit()
else:
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.edit(content=highscore_string)
return message
@@ -367,14 +366,7 @@ async def submit_score(
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == group_id)
group = session.scalar(stmt)
# edit message
channel = interaction.guild.get_channel(group.ChannelId)
error_response = "Registered Highscores channel does not exist or was never registered."
error_response += 'Register with "/register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(session, channel, group)
await send_highscore_message(session, interaction.guild, group)
def add_preset_configuration(guild_id: int, config_str: str) -> None: