Compare commits

..
32 Commits
Author SHA1 Message Date
imcompiling c25d354fc0 Correct some parameter usage, handle errors if searches return nothing. 2025-03-14 16:09:52 -04:00
imcompiling e5d90a71d9 Correct parameter usage. 2025-03-14 16:02:18 -04:00
imcompiling 74ce22dd26 Was saving the message id to the wrong spot. 2025-03-13 00:39:47 -04:00
imcompiling 170ce1cbfb Use correct message id reference. 2025-03-12 21:30:56 -04:00
imcompiling e1b761e848 Use correct variable for guild name. 2025-03-12 21:29:01 -04:00
imcompiling 68592a7df8 Typo on col usage. 2025-03-12 21:27:29 -04:00
imcompiling b28938f66a Had col around the wrong variables. 2025-03-12 21:27:12 -04:00
imcompiling 3abbff13d5 Use walrus operator when assigning and checking for None. 2025-03-12 09:50:58 -04:00
imcompiling 0702198f0a Fix lint smells and update model usage in view. 2025-03-04 13:24:08 -05:00
imcompiling 43df7aeda7 Fix lint smells and update model usage in update. 2025-03-04 13:20:14 -05:00
imcompiling c8becb8203 Fix lint smells and update model usage in submission. 2025-03-04 13:15:20 -05:00
imcompiling 44d813504a Fix lint smells and update model usage in refresh and set. 2025-03-04 12:47:44 -05:00
imcompiling 3fc8910c9a Fix lint smell. 2025-03-04 11:19:06 -05:00
imcompiling 563b6ab888 Fix lint smells. Update model usage. 2025-03-04 11:18:40 -05:00
imcompiling d7e44d3a22 Fix lint smells and update model usage. 2025-03-04 11:04:10 -05:00
imcompiling a99b488ae1 Message id isn't known at object creation time. 2025-03-04 11:03:45 -05:00
imcompiling 85d0077107 Fix smells and update model uses for updating username verify. 2025-03-04 09:53:09 -05:00
imcompiling 9510c9eddc Add init for views directory. Fix smells and update model usage in delete confirm. 2025-03-04 09:47:39 -05:00
imcompiling 9550d1a68b Remove all pylint supressors. 2025-03-03 23:41:58 -05:00
imcompiling b04819aa36 Fix smells and model usage in guild listener. 2025-03-03 23:40:49 -05:00
imcompiling f43dccb12d Finish fixing lint smells in loops directory. 2025-03-03 23:36:07 -05:00
imcompiling 9cf05f44d0 Fix error in model.
The two channel ids need to be NULL by default, don't know them on object creation.
2025-03-03 23:32:03 -05:00
imcompiling f6ae34c9e6 Started fixing smells and updating model calls. 2025-03-03 23:29:46 -05:00
imcompiling 528af14e00 Update model usage in verification view. 2025-03-03 23:14:31 -05:00
imcompiling dbe652dcce Fix smells in submission init file. 2025-03-03 23:08:53 -05:00
imcompiling 1658638938 Remove unneeded parenthesis. 2025-03-03 23:08:41 -05:00
imcompiling 43f51f7f07 Test file after connecting to prod database and testing relationships. 2025-03-03 23:02:40 -05:00
imcompiling f74b08b02e Add another word to cspell. 2025-03-03 23:02:19 -05:00
imcompiling 9dd07bef93 Add an init file for utilities. 2025-03-03 23:01:58 -05:00
imcompiling 231c700946 Fix other errors with models. 2025-03-03 22:57:48 -05:00
imcompiling 37daa0e657 Update names used in constraints. 2025-03-03 22:46:33 -05:00
imcompiling 6dfd3acad0 Add SnakeCase column names that are already in database. 2025-03-03 22:44:14 -05:00
22 changed files with 335 additions and 298 deletions
+11 -2
View File
@@ -1,7 +1,16 @@
{ {
"cSpell.words": ["gmtime", "highscore", "highscores", "imcompiling", "nextcord"], "cSpell.words": [
"dotenv",
"gmtime",
"highscore",
"highscores",
"imcompiling",
"iterrows",
"nextcord",
"ondelete"
],
"python.analysis.diagnosticSeverityOverrides": { "python.analysis.diagnosticSeverityOverrides": {
"reportOptionalMemberAccess": false, "reportOptionalMemberAccess": "warning",
"reportArgumentType": "information", "reportArgumentType": "information",
"reportAttributeAccessIssue": "warning" "reportAttributeAccessIssue": "warning"
} }
+17 -15
View File
@@ -4,8 +4,10 @@ import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import exc, select from sqlalchemy import exc, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
from database.db import Category, CategoryGroup, engine from database.db import engine
from database.models import Category, CategoryGroup
from utilities.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message from utilities.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
@@ -52,30 +54,30 @@ class CreateCog(commands.Cog, name="Create Cog"):
size (int): How many sores to show in the category message. Is SlashOption. size (int): How many sores to show in the category message. Is SlashOption.
""" """
new_category = Category( new_category = Category(
CategoryName=category_name, name=category_name,
GroupId=int(group_id), group_id=int(group_id),
Size=size, size=size,
IsAscending=is_ascending, is_ascending=is_ascending,
IsTimeRecord=is_time_record, is_time_record=is_time_record,
IsValidated=is_validated, is_validated=is_validated,
) )
# Open connection to engine and add new category. # Open connection to engine and add new category.
with Session(engine) as session: with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id)) stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt) group = session.scalar(stmt)
try: try:
session.add(new_category) session.add(new_category)
session.commit() session.commit()
await send_highscore_message(session, interaction.guild, group) await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message( await interaction.response.send_message(
f'"{category_name}" has been added to "{group.GroupName}"!', ephemeral=True f'"{category_name}" has been added to "{group.name}"!', ephemeral=True
) )
except exc.IntegrityError: except exc.IntegrityError:
# Category name already exists in group # Category name already exists in group
session.rollback() session.rollback()
await interaction.response.send_message( await interaction.response.send_message(
f'"{category_name}" already exists in "{group.GroupName}". Please rename.', ephemeral=True f'"{category_name}" already exists in "{group.name}". Please rename.', ephemeral=True
) )
@create.subcommand(description="Create a new Category Group. Must not exist already for Guild.") @create.subcommand(description="Create a new Category Group. Must not exist already for Guild.")
@@ -123,11 +125,11 @@ class CreateCog(commands.Cog, name="Create Cog"):
channel_id: int = channel.id channel_id: int = channel.id
new_group = CategoryGroup( new_group = CategoryGroup(
GroupName=group_name, name=group_name,
ChannelId=channel_id, channel_id=channel_id,
GuildId=interaction.guild_id, guild_id=interaction.guild_id,
PadLength=pad_length, pad_length=pad_length,
UsernameLength=username_length, username_length=username_length,
) )
with Session(engine) as session: with Session(engine) as session:
+28 -20
View File
@@ -4,9 +4,10 @@ import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import distinct, select from sqlalchemy import distinct, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import Category, CategoryGroup, Score, engine from database.models import Category, CategoryGroup, Score
from utilities.utils import ( from utilities.utils import (
autofill_categories, autofill_categories,
autofill_groups, autofill_groups,
@@ -36,10 +37,9 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
async def channel_messages( async def channel_messages(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption( channel: nextcord.TextChannel | None = nextcord.SlashOption(
description="Channel to clear of messages.", description="Channel to clear of messages.",
required=False, required=False,
channel_types=[nextcord.ChannelType.text],
default=None, default=None,
), ),
) -> None: ) -> None:
@@ -54,13 +54,18 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
Is a SlashOption. Limited to text channels. Defaults to None. Is a SlashOption. Limited to text channels. Defaults to None.
""" """
if not channel: if not channel:
if isinstance(interaction.channel, nextcord.TextChannel):
channel = interaction.channel channel = interaction.channel
else:
# This should never happen.
await interaction.response.send_message("Channel is not a text channel somehow.", ephemeral=True)
return
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(CategoryGroup) select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id) .where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(CategoryGroup.ChannelId == channel.id) .where(col(CategoryGroup.channel_id) == channel.id)
) )
# TODO test when nothing in channel # TODO test when nothing in channel
groups = session.scalars(stmt) groups = session.scalars(stmt)
@@ -68,8 +73,9 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
for group in groups: for group in groups:
if group.message_id is not None:
try: try:
message: nextcord.Message = await channel.fetch_message(group.MessageId) message: nextcord.Message = await channel.fetch_message(group.message_id)
await message.delete() await message.delete()
except nextcord.NotFound: except nextcord.NotFound:
# silently ignore that messages are not found. They got deleted already. # silently ignore that messages are not found. They got deleted already.
@@ -97,24 +103,24 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
user_name (str): User to remove from highscores user_name (str): User to remove from highscores
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(distinct(Score.CategoryId)).where(Score.UserName == user_name) stmt = select(distinct(col(Score.category_id))).where(col(Score.username) == user_name)
category_ids: list[int] = list(session.scalars(stmt)) category_ids: list[int] = list(session.scalars(stmt))
stmt = select(Score).where(Score.UserName == user_name) stmt = select(Score).where(col(Score.username) == user_name)
for score in session.scalars(stmt): for score in session.scalars(stmt):
session.delete(score) session.delete(score)
session.commit() session.commit()
group_ids = select(Category.GroupId).where(Category.CategoryId.in_(category_ids)) group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids)) stmt = select(CategoryGroup).where(col(CategoryGroup.id).in_(group_ids))
for group in session.scalars(stmt): for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild, group) await send_highscore_message(session, interaction.guild, group)
await interaction.send(f'User "{user_name}" was removed from all Category Groups.', ephemeral=True) await interaction.send(f'User "{user_name}" was removed from all Category Groups.', ephemeral=True)
@delete.subcommand(description="Delete a category from a group. Deletion is perminant.") @delete.subcommand(description="Delete a category from a group. Deletion is permanent.")
async def category( async def category(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
@@ -133,7 +139,7 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id))) DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id)))
) )
@delete.subcommand(description="Delete a group. Deletion is perminant.") @delete.subcommand(description="Delete a group. Deletion is permanent.")
async def group( async def group(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
@@ -172,13 +178,15 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
user_name (str): The user to remove. Is SlashOption. user_name (str): The user to remove. Is SlashOption.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id)) stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt) group = session.scalar(stmt)
stmt = select(Category.CategoryName).where(Category.CategoryId == int(category_id)) stmt = select(col(Category.name)).where(col(Category.id) == int(category_id))
category_name = session.scalar(stmt) category_name = session.scalar(stmt)
stmt = select(Score).where(Score.CategoryId == int(category_id)).where(Score.UserName == user_name) stmt = (
select(Score).where(col(Score.category_id) == int(category_id)).where(col(Score.username) == user_name)
)
# there should only be one because category id and username are a composite primary key in score # there should only be one because category id and username are a composite primary key in score
session.delete(session.scalar(stmt)) session.delete(session.scalar(stmt))
@@ -205,10 +213,10 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
user_name (str): The user to remove. Is SlashOption. user_name (str): The user to remove. Is SlashOption.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id)) stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt) group = session.scalar(stmt)
category_ids = select(Category.CategoryId).where(Category.GroupId == int(group_id)) category_ids = select(col(Category.id)).where(col(Category.group_id) == int(group_id))
stmt = select(Score).where(Score.CategoryId.in_(category_ids)).where(Score.UserName == user_name) stmt = select(Score).where(col(Score.category_id).in_(category_ids)).where(col(Score.username) == user_name)
for score in session.scalars(stmt): for score in session.scalars(stmt):
session.delete(score) session.delete(score)
@@ -216,7 +224,7 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
await send_highscore_message(session, interaction.guild, group) await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message( await interaction.response.send_message(
f'User "{user_name}" removed from all categories in {group.GroupName}.', ephemeral=True f'User "{user_name}" removed from all categories in {group.name}.', ephemeral=True
) )
@from_category.on_autocomplete("group_id") @from_category.on_autocomplete("group_id")
+1 -1
View File
@@ -11,7 +11,7 @@ class HelpCog(commands.Cog, name="Help commands."):
"""Default constructor for HelpCog class. Does nothing.""" """Default constructor for HelpCog class. Does nothing."""
@nextcord.slash_command() @nextcord.slash_command()
async def help(self, interaction: nextcord.Interaction): async def help(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for help. Never called.""" """Parent slash command for help. Never called."""
@help.subcommand(description="Describes how to get started with Agon.") @help.subcommand(description="Describes how to get started with Agon.")
+8 -9
View File
@@ -1,14 +1,13 @@
""" """Commands for refreshing highscore messages."""
Commands for refreshing highscore messages.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import CategoryGroup, engine from database.models import CategoryGroup
from utilities.utils import autofill_groups, send_highscore_message from utilities.utils import autofill_groups, send_highscore_message
@@ -44,13 +43,13 @@ class RefreshCog(commands.Cog, name="Refresh Cog"):
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(CategoryGroup) select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id) .where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id)) .where(col(CategoryGroup.id) == int(group_id))
) )
group = session.scalar(stmt) group = session.scalar(stmt)
await send_highscore_message(session, interaction.guild, group) await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(f"Updated group {group.GroupName}!", ephemeral=True) await interaction.response.send_message(f"Updated group {group.name}!", ephemeral=True)
@refresh.subcommand() @refresh.subcommand()
async def all(self, interaction: nextcord.Interaction) -> None: async def all(self, interaction: nextcord.Interaction) -> None:
@@ -68,7 +67,7 @@ class RefreshCog(commands.Cog, name="Refresh Cog"):
interaction (nextcord.Interaction): Interaction that invoked the command. interaction (nextcord.Interaction): Interaction that invoked the command.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == interaction.guild_id) stmt = select(CategoryGroup).where(col(CategoryGroup.guild_id) == interaction.guild_id)
# awaiting response because posting scores takes a while # awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
+47 -45
View File
@@ -1,32 +1,25 @@
""" """Set command hierarchy."""
Set command heirarchy.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import CategoryGroup, Guild, engine from database.models import CategoryGroup, Guild
from utilities.utils import MAX_LINE_LENGTH, autofill_groups from utilities.utils import MAX_LINE_LENGTH, autofill_groups
class SetCog(commands.Cog, name="Set command."): class SetCog(commands.Cog, name="Set command."):
""" """Class holds commands for setting variables in guild's database."""
Class holds commands for setting variables in guild's database.
"""
def __init__(self) -> None: def __init__(self) -> None:
""" """Default constructor for SetCog class. Does nothing."""
Default constructor for SetCog class. Does nothing.
"""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True)) @nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def set(self, interaction: nextcord.Interaction): async def set(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for setting settings. Never called."""
Parent slash command for setting settings. Never called.
"""
@set.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( async def group_output_channel(
@@ -51,17 +44,18 @@ class SetCog(commands.Cog, name="Set command."):
channel (nextcord.abc.GuildChannel, optional): Channel to register channel (nextcord.abc.GuildChannel, optional): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None. Is a SlashOption. Limited to text channels, Defaults to None.
""" """
if not channel: channel_id = interaction.channel.id if not channel else channel.id
channel = interaction.channel
channel_id = channel.id
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(CategoryGroup) select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id) .where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id)) .where(col(CategoryGroup.id) == int(group_id))
) )
group = session.scalar(stmt) if (group := session.scalar(stmt)) is None:
group.ChannelId = channel_id await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
group.channel_id = channel_id
session.commit() session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True) await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@@ -93,14 +87,17 @@ class SetCog(commands.Cog, name="Set command."):
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(CategoryGroup) select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id) .where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id)) .where(col(CategoryGroup.id) == int(group_id))
) )
group = session.scalar(stmt) if (group := session.scalar(stmt)) is None:
group.PadLength = length await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
group.pad_length = length
session.commit() session.commit()
await interaction.response.send_message( await interaction.response.send_message(
f"Category pad length set to {length} for {group.GroupName}", ephemeral=True f"Category pad length set to {length} for {group.name}", ephemeral=True
) )
@set.subcommand( @set.subcommand(
@@ -121,17 +118,17 @@ class SetCog(commands.Cog, name="Set command."):
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command interaction (nextcord.Interaction): Interaction that invoked the command
channel Optional[nextcord.abc.GuildChannel], optional): Channel to register channel (nextcord.abc.GuildChannel): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None. Is a SlashOption. Limited to text channels, Defaults to None.
""" """
if not channel: channel_id = interaction.channel.id if not channel else channel.id
channel = interaction.channel
with Session(engine) as session: with Session(engine) as session:
channel_id = channel.id stmt = select(Guild).where(col(Guild.id) == interaction.guild_id)
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id) if (guild := session.scalar(stmt)) is None:
guild = session.scalar(stmt) await interaction.response.send_message("Guild was not found.", ephemeral=True)
guild.SubmissionChannel = channel_id return
guild.submission_channel = channel_id
session.commit() session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True) await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@@ -151,16 +148,20 @@ class SetCog(commands.Cog, name="Set command."):
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command interaction (nextcord.Interaction): Interaction that invoked the command
group_id (str): String representation of Group id to set username length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 12. length (int): Number of characters. Is SlashOption. Defaults to 12.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(CategoryGroup) select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id) .where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id)) .where(col(CategoryGroup.id) == int(group_id))
) )
group = session.scalar(stmt) if (group := session.scalar(stmt)) is None:
group.UsernameLength = length await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
group.username_length = length
session.commit() session.commit()
await interaction.response.send_message(f"Username length set to {length}.", ephemeral=True) await interaction.response.send_message(f"Username length set to {length}.", ephemeral=True)
@@ -183,20 +184,21 @@ class SetCog(commands.Cog, name="Set command."):
channel (Optional[nextcord.abc.GuildChannel]): Channel to register channel (Optional[nextcord.abc.GuildChannel]): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None. Is a SlashOption. Limited to text channels, Defaults to None.
""" """
if not channel: channel_id = interaction.channel.id if not channel else channel.id
channel = interaction.channel
channel_id = channel.id
with Session(engine) as session: with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id) stmt = select(Guild).where(col(Guild.id) == interaction.guild_id)
guild = session.scalar(stmt) if (guild := session.scalar(stmt)) is None:
guild.VerificationChannel = channel_id await interaction.response.send_message("Guild was not found.", ephemeral=True)
return
guild.verification_channel = channel_id
session.commit() session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True) await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@group_output_channel.on_autocomplete("group_id") @group_output_channel.on_autocomplete("group_id")
@pad_length.on_autocomplete("group_id") @pad_length.on_autocomplete("group_id")
@username_length.on_autocomplete("group_id") @username_length.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str): async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
""" """
Sends results from autofill_groups back to group_output_channel for autocompletion of group_id. Sends results from autofill_groups back to group_output_channel for autocompletion of group_id.
+38 -28
View File
@@ -6,9 +6,11 @@ import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
from cogs.submission.verification_view import VerificationView from cogs.submission.verification_view import VerificationView
from database.db import Category, CategoryGroup, Guild, Submission, engine from database.db import engine
from database.models import Category, CategoryGroup, Guild, Submission
from main import logger from main import logger
from utilities.utils import autofill_categories, autofill_groups, submit_score from utilities.utils import autofill_categories, autofill_groups, submit_score
@@ -19,8 +21,10 @@ class SubmissionCog(commands.Cog):
def __init__(self) -> None: def __init__(self) -> None:
return return
# TODO(imcompiling): fix too many return statements when reworking command.
# https://gitea.cheezers.org/imcompiling/agon/issues/76
@nextcord.slash_command() @nextcord.slash_command()
async def submit( # noqa: PLR0913 async def submit( # noqa: PLR0911, PLR0913
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", required=True, description="Group to submit score to."), group_id: str = nextcord.SlashOption(name="group", required=True, description="Group to submit score to."),
@@ -56,40 +60,46 @@ class SubmissionCog(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(Category).where(Category.GroupId == int(group_id)).where(Category.CategoryId == int(category_id)) select(Category)
.where(col(Category.group_id) == int(group_id))
.where(col(Category.id) == int(category_id))
) )
if (category := session.scalar(stmt)) is None:
category: Category = session.scalar(stmt) await interaction.response.send_message("Category not found.", ephemeral=True)
return
# check if verification is required. if no picture, deny # check if verification is required. if no picture, deny
if category.IsValidated and proof is None: if category.is_validated and proof is None:
await interaction.response.send_message( await interaction.response.send_message(
"This category required verification. Please attach a picture and re-submit.", ephemeral=True "This category required verification. Please attach a picture and re-submit.", ephemeral=True
) )
return return
# check with regex if score is in correct format # check with regex if score is in correct format
verified_score = await self.verify_score(interaction, score, is_time_record=category.IsTimeRecord)
# check if verify_score was successful. It replies with corrections required itself. # check if verify_score was successful. It replies with corrections required itself.
if verified_score is None: if (
verified_score := await self.verify_score(interaction, score, is_time_record=category.is_time_record)
) is None:
# This response message will be filled out better once command is split up to
# know which type it expects...
await interaction.response.send_message("Format was incorrect.", ephemeral=True)
return return
# Create submission verification request or submit score if verification not needed. # Create submission verification request or submit score if verification not needed.
if not category.IsValidated: if not category.is_validated:
await submit_score(interaction, int(group_id), int(category_id), username, verified_score) await submit_score(interaction, int(group_id), int(category_id), username, verified_score)
await interaction.response.send_message("Score has been submitted!", ephemeral=True) await interaction.response.send_message("Score has been submitted!", ephemeral=True)
return return
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id) stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
verification_channel_id: int = session.scalar(stmt).VerificationChannel
channel = interaction.guild.get_channel(verification_channel_id) if (verification_channel_id := session.scalar(stmt)) is None:
error_response = "No verification channel was registered. Contact server admins."
await interaction.response.send_message(error_response, ephemeral=True)
return
if channel is None: if (channel := interaction.guild.get_channel(verification_channel_id)) is None:
error_response = ( error_response = "Registered Verification channel does not exist. Contact server admins."
"Registered Verification channel does not exist or was never registered. Contact server admins."
)
await interaction.response.send_message(error_response, ephemeral=True) await interaction.response.send_message(error_response, ephemeral=True)
return return
@@ -111,13 +121,13 @@ class SubmissionCog(commands.Cog):
# Save the submission data for later use. # Save the submission data for later use.
submission: Submission = Submission( submission: Submission = Submission(
CategoryId=category_id, category_id=int(category_id),
GroupId=group_id, group_id=int(group_id),
GuildId=interaction.guild_id, guild_id=interaction.guild_id,
MessageId=verification_message.id, message_id=verification_message.id,
Score=verified_score, score=verified_score,
UserId=interaction.user.id, user_id=interaction.user.id,
UserName=username, username=username,
) )
session.add(submission) session.add(submission)
@@ -151,15 +161,15 @@ class SubmissionCog(commands.Cog):
# Get group name from database # Get group name from database
stmt = ( stmt = (
select(CategoryGroup.GroupName) select(col(CategoryGroup.name))
.where(CategoryGroup.GuildId == interaction.guild_id) .where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(CategoryGroup.GroupId == group_id) .where(col(CategoryGroup.id) == group_id)
) )
embed.add_field(name="Group", value=session.scalar(stmt), inline=False) embed.add_field(name="Group", value=session.scalar(stmt), inline=False)
# Get category name from database # Get category name from database
stmt = ( stmt = (
select(Category.CategoryName).where(Category.GroupId == group_id).where(Category.CategoryId == category_id) select(col(Category.name)).where(col(Category.group_id) == group_id).where(col(Category.id) == category_id)
) )
embed.add_field(name="Category", value=session.scalar(stmt), inline=False) embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
embed.add_field(name="Score", value=score, inline=False) embed.add_field(name="Score", value=score, inline=False)
+20 -22
View File
@@ -1,23 +1,20 @@
""" """Commands for updating information/variables held in database."""
Commands for updating information/variables held in database.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import Guild, engine from database.models import Guild
from utilities.utils import autofill_users_in_guild from utilities.utils import autofill_users_in_guild
from .views.update_username_verify import UpdateUsernameVerifyView from .views.update_username_verify import UpdateUsernameVerifyView
class UpdateCog(commands.Cog, name="Update commands."): class UpdateCog(commands.Cog, name="Update commands."):
""" """Class holds commands and views needed for updating things in bot."""
Class holds commands and views needed for updating things in bot.
"""
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
""" """
@@ -29,17 +26,13 @@ class UpdateCog(commands.Cog, name="Update commands."):
self._bot: commands.Bot = bot self._bot: commands.Bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): async def on_ready(self) -> None:
""" """Called when cog is loaded. Adds views required for DataManagement commands."""
Called when cog is loaded. Adds views required for DataManagement commands.
"""
self._bot.add_view(UpdateUsernameVerifyView()) self._bot.add_view(UpdateUsernameVerifyView())
@nextcord.slash_command() @nextcord.slash_command()
async def update(self, interaction: nextcord.Interaction): async def update(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for updating things. Never called."""
Parent slash command for updating things. Never called.
"""
@update.subcommand(description="Request a username update. Admins must approve.") @update.subcommand(description="Request a username update. Admins must approve.")
async def username( async def username(
@@ -47,10 +40,11 @@ class UpdateCog(commands.Cog, name="Update commands."):
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
old_username: str = nextcord.SlashOption(description="Username currently stored in highscores.", required=True), 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), new_username: str = nextcord.SlashOption(description="Username to update to.", required=True),
): ) -> None:
""" """
Creates a request to send to admins for changing a username. Uses Creates a request to send to admins for changing a username.
UpdateUsernameVerifyView to do the work.
Uses UpdateUsernameVerifyView to do the work.
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command interaction (nextcord.Interaction): Interaction that invoked the command
@@ -58,9 +52,13 @@ class UpdateCog(commands.Cog, name="Update commands."):
new_username (str): Username to replace old with. new_username (str): Username to replace old with.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id) stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
verify_channel = interaction.guild.get_channel(session.scalar(stmt)) verify_channel = interaction.guild.get_channel(session.scalar(stmt))
if not isinstance(verify_channel, nextcord.TextChannel):
await interaction.send("Verification channel not set. Cannot process request.", ephemeral=True)
return
embed: nextcord.Embed = nextcord.Embed(title="Username change request.") embed: nextcord.Embed = nextcord.Embed(title="Username change request.")
embed.add_field(name="Old Username", value=old_username) embed.add_field(name="Old Username", value=old_username)
embed.add_field(name="New Username", value=new_username) embed.add_field(name="New Username", value=new_username)
@@ -72,12 +70,12 @@ class UpdateCog(commands.Cog, name="Update commands."):
await interaction.send("Request sent to admins for verification.", ephemeral=True) await interaction.send("Request sent to admins for verification.", ephemeral=True)
@username.on_autocomplete("old_username") @username.on_autocomplete("old_username")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str): async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str) -> None:
""" """
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username. Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args: Args:
interaction (nextcord.Interaction): Interaction with command. interaction (nextcord.Interaction): Interaction with command.
old_username (str): String to run autocompletion against. username (str): String to run autocompletion against.
""" """
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username)) await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+9 -8
View File
@@ -5,9 +5,10 @@ import pandas as pd
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import Category, CategoryGroup, Score, engine from database.models import Category, CategoryGroup, Score
from utilities.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string from utilities.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string
@@ -47,17 +48,17 @@ class ViewCog(commands.Cog, name="View Command"):
# get group, category, and all scores sorted correctly. # get group, category, and all scores sorted correctly.
with Session(engine) as session: with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id)) stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt) group = session.scalar(stmt)
stmt = select(Category).where(Category.CategoryId == int(category_id)) stmt = select(Category).where(col(Category.id) == int(category_id))
category = session.scalar(stmt) category = session.scalar(stmt)
d_f = pd.read_sql_query( d_f = pd.read_sql_query(
sql=session.query(Score).where(Score.CategoryId == category_id).statement, con=engine sql=session.query(Score).where(col(Score.category_id) == category_id).statement, con=engine
) )
scores = d_f.sort_values("Score", ascending=category.IsAscending).reset_index() scores = d_f.sort_values("Score", ascending=category.is_ascending).reset_index()
# get the longest string # get the longest string
longest_str: int = scores.UserName.map(len).max() longest_str: int = scores.UserName.map(len).max()
@@ -68,13 +69,13 @@ class ViewCog(commands.Cog, name="View Command"):
row["UserName"], row["UserName"],
row["Score"], row["Score"],
longest_str, longest_str,
category.IsTimeRecord, category.is_time_record,
i + 1, # type: ignore[reportOperatorIssue] i + 1, # type: ignore[reportOperatorIssue]
MAX_LINE_LENGTH, MAX_LINE_LENGTH,
) )
) )
message_str: str = f"**{group.GroupName} {category.CategoryName} Leaderboard!**\n```" message_str: str = f"**{group.name} {category.name} Leaderboard!**\n```"
if len(str_list) == 0: if len(str_list) == 0:
message_str += "No Submissions!" message_str += "No Submissions!"
else: else:
+1
View File
@@ -0,0 +1 @@
"""Package for views used in commands."""
+19 -16
View File
@@ -1,23 +1,19 @@
""" """Contains view and helper functions for confirming and processing deletes."""
Contains view and helper functions for confirming and processing deletes.
"""
import nextcord import nextcord
from sqlalchemy import delete, select from sqlalchemy import delete, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# pylint: disable=import-error from database.db import engine
from database.db import Category, CategoryGroup, engine from database.models import Category, CategoryGroup
from utilities.utils import send_highscore_message from utilities.utils import send_highscore_message
class DeleteConfirmView(nextcord.ui.Modal): class DeleteConfirmView(nextcord.ui.Modal):
""" """View modal popup for confirming deletions."""
View modal popup for confirming deletions.
"""
def __init__(self, custom_id: str): def __init__(self, custom_id: str):
super().__init__(title="Warning, deletion is perminant.", custom_id=custom_id) super().__init__(title="Warning, deletion is permanent.", custom_id=custom_id)
self.checker = nextcord.ui.TextInput( self.checker = nextcord.ui.TextInput(
label='Type "delete" to confirm deletion.', label='Type "delete" to confirm deletion.',
@@ -27,7 +23,12 @@ class DeleteConfirmView(nextcord.ui.Modal):
) )
self.add_item(self.checker) self.add_item(self.checker)
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction) -> None:
"""Action for confirming deletion.
Args:
interaction (nextcord.Interaction): User interaction object with the modal.
"""
if self.checker.value.lower() != "delete": if self.checker.value.lower() != "delete":
await interaction.response.send_message('Response was not "delete". Canceling delete.') await interaction.response.send_message('Response was not "delete". Canceling delete.')
return return
@@ -35,21 +36,23 @@ class DeleteConfirmView(nextcord.ui.Modal):
del_vals = parse_delete_id(self.custom_id) del_vals = parse_delete_id(self.custom_id)
with Session(engine) as session: with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == del_vals["group_id"]) stmt = select(CategoryGroup).where(CategoryGroup.id == del_vals["group_id"])
group = session.scalar(stmt) group = session.scalar(stmt)
if del_vals["category_id"] is None: if del_vals["category_id"] is None:
# delete entire group # delete entire group
session.delete(group) session.delete(group)
# delete the message related to group # delete the message related to group
channel: nextcord.TextChannel = interaction.guild.get_channel(group.ChannelId) channel = interaction.guild.get_channel(group.channel_id)
message: nextcord.Message = await channel.fetch_message(group.MessageId) if not isinstance(channel, nextcord.TextChannel):
return
message: nextcord.Message = await channel.fetch_message(group.message_id)
await message.delete() await message.delete()
else: else:
stmt = ( stmt = (
delete(Category) delete(Category)
.where(Category.GroupId == del_vals["group_id"]) .where(Category.group_id == del_vals["group_id"])
.where(Category.CategoryId == del_vals["category_id"]) .where(Category.id == del_vals["category_id"])
) )
session.execute(stmt) session.execute(stmt)
# update the message # update the message
@@ -78,7 +81,7 @@ def parse_delete_id(delete_id: str) -> dict:
Returns a dict containing the values of a delete id. Returns a dict containing the values of a delete id.
Args: Args:
id (str): the delete id delete_id (str): the delete id
Returns: Returns:
dict: dict containing values from a delete id. dict: dict containing values from a delete id.
+16 -16
View File
@@ -3,23 +3,22 @@
import nextcord import nextcord
from sqlalchemy import distinct, exc, select from sqlalchemy import distinct, exc, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import Category, CategoryGroup, Score, engine from database.models import Category, CategoryGroup, Score
from utilities.utils import send_highscore_message from utilities.utils import send_highscore_message
class UpdateUsernameVerifyView(nextcord.ui.View): class UpdateUsernameVerifyView(nextcord.ui.View):
"""Defines view for allowing a username update.""" """Defines view for allowing a username update."""
def __init__(self): def __init__(self) -> None:
""" """Default constructor for UpdateUsernameVerifyView. Calls super constructor with no timeout."""
Default constructor for UpdateUsernameVerifyView. Calls super constructor with no timeout.
"""
super().__init__(timeout=None) super().__init__(timeout=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-username-update") @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 async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button for denying username update. Deletes message. Button for denying username update. Deletes message.
@@ -30,10 +29,11 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
await interaction.message.delete() await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-username-update") @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 async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button for approving username update. Tells guild data to update username, then updates Button for approving username update.
message to show it is complete.
Tells guild data to update username, then updates message to show it is complete.
Args: Args:
button (nextcord.ui.Button): unused button (nextcord.ui.Button): unused
@@ -42,20 +42,20 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
with Session(engine) as session: with Session(engine) as session:
try: try:
embed_content = interaction.message.embeds[0].to_dict() embed_content = interaction.message.embeds[0].to_dict()
old_username: str = embed_content["fields"][0]["value"] old_username: str = embed_content["fields"][0]["value"] # type: ignore reportTypedDictNotRequiredAccess
new_username: str = embed_content["fields"][1]["value"] new_username: str = embed_content["fields"][1]["value"] # type: ignore reportTypedDictNotRequiredAccess
stmt = select(distinct(Score.CategoryId)).where(Score.UserName == old_username) stmt = select(distinct(col(Score.category_id))).where(col(Score.username) == old_username)
category_ids: list[int] = list(session.scalars(stmt)) category_ids: list[int] = list(session.scalars(stmt))
stmt = select(Score).where(Score.UserName == old_username) stmt = select(Score).where(col(Score.username) == old_username)
for score in session.scalars(stmt): for score in session.scalars(stmt):
score.UserName = new_username score.UserName = new_username
session.commit() session.commit()
group_ids = select(Category.GroupId).where(Category.CategoryId.in_(category_ids)) group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids)) stmt = select(CategoryGroup).where(col(CategoryGroup.id).in_(group_ids))
for group in session.scalars(stmt): for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild, group) await send_highscore_message(session, interaction.guild, group)
+12 -14
View File
@@ -1,14 +1,13 @@
""" """Defines cog for guild listeners. Contains functions for various guild events."""
Defines cog for guild listeners. Contains functions for various guild events.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import Guild, engine from database.models import Guild
class GuildListener(commands.Cog, name="Guild Listener"): class GuildListener(commands.Cog, name="Guild Listener"):
@@ -18,7 +17,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): async def on_guild_join(self, guild: nextcord.Guild) -> None:
""" """
Listener for Guild Join. Adds guild to database. Listener for Guild Join. Adds guild to database.
@@ -26,12 +25,11 @@ class GuildListener(commands.Cog, name="Guild Listener"):
guild (nextcord.Guild): Guild that was joined. guild (nextcord.Guild): Guild that was joined.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id) stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt)
# just in case check if the guild already exists # just in case check if the guild already exists
if guild_data is None: if (guild_data := session.scalar(stmt)) is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name) new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild) session.add(new_guild)
else: else:
guild_data.GuildName = guild.name guild_data.GuildName = guild.name
@@ -45,7 +43,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
await imcompiling.send(f'Agon has joined server "{guild.name}"!') await imcompiling.send(f'Agon has joined server "{guild.name}"!')
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild): async def on_guild_remove(self, guild: nextcord.Guild) -> None:
""" """
Listens to when the bot has been removed from a guild. Update's the database to record the active flag as false. Listens to when the bot has been removed from a guild. Update's the database to record the active flag as false.
@@ -53,7 +51,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
guild (nextcord.Guild): Guild that was left. guild (nextcord.Guild): Guild that was left.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id) stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt) guild_data = session.scalar(stmt)
guild_data.Active = False guild_data.Active = False
session.commit() session.commit()
@@ -66,6 +64,6 @@ class GuildListener(commands.Cog, name="Guild Listener"):
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
"""Adds function to bot""" """Adds function to bot."""
bot.add_cog(GuildListener(bot)) bot.add_cog(GuildListener(bot))
+3 -5
View File
@@ -1,6 +1,4 @@
""" """Initializing code for all loops."""
Initializing code for all loops.
"""
from nextcord.ext import commands from nextcord.ext import commands
@@ -10,7 +8,7 @@ from .guild_data import GuildData
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
"""Adds function to bot""" """Adds function to bot."""
bot.add_cog(BotPresence(bot)) bot.add_cog(BotPresence(bot))
bot.add_cog(GuildData(bot)) bot.add_cog(GuildData(bot))
+8 -11
View File
@@ -1,6 +1,4 @@
""" """Functions for controlling the bot's presence. Also contains the loop."""
Functions for controlling the bot's presence. Also contains the loop.
"""
import random import random
@@ -11,27 +9,26 @@ from nextcord.ext import commands, tasks
class BotPresence(commands.Cog, name="Bot Presence"): class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions.""" """Class for bot presence functions."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot) -> None:
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument async def on_guild_join(self, guild: nextcord.Guild) -> None: # noqa: ARG002
"""Listener for Guild Join. Updated presence on guild join.""" """Listener for Guild Join. Updated presence on guild join."""
await self.change_presence() await self.change_presence()
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): async def on_ready(self) -> None:
"""Starts the loop when bot is ready.""" """Starts the loop when bot is ready."""
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop. self.periodic_change_presence.start()
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=2) @tasks.loop(minutes=2)
async def periodic_change_presence(self): async def periodic_change_presence(self) -> None:
"""Loops every 2 minutes and changes presence.""" """Loops every 2 minutes and changes presence."""
await self.change_presence() await self.change_presence()
async def change_presence(self): async def change_presence(self) -> None:
"""Randomly chooses a presence and updates bot's presence.""" """Randomly chooses a presence and updates bot's presence."""
statuses = [ statuses = [
"Now with slash commands!", "Now with slash commands!",
@@ -41,7 +38,7 @@ class BotPresence(commands.Cog, name="Bot Presence"):
"That's a lot of damage!", "That's a lot of damage!",
] ]
status = random.choice(statuses) status = random.choice(statuses) # noqa: S311
status += f" | {len(self.bot.guilds)} servers" status += f" | {len(self.bot.guilds)} servers"
+13 -17
View File
@@ -1,48 +1,44 @@
""" """Contains a loop that updates guild data in the database periodically."""
Contains a loop that updates guild data in the database periodically.
"""
from nextcord.ext import commands, tasks from nextcord.ext import commands, tasks
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error from database.db import engine
from database.db import Guild, engine from database.models import Guild
class GuildData(commands.Cog, name="Guild Data"): class GuildData(commands.Cog, name="Guild Data"):
"""Class to contain loop.""" """Class to contain loop."""
def __init__(self, bot: commands.bot) -> None: def __init__(self, bot: commands.Bot) -> None:
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): async def on_ready(self) -> None:
"""Starts the loop when bot is ready.""" """Starts the loop when bot is ready."""
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_update_guild_data is subclassed from tasks.loop. await self.periodic_update_guild_data.start()
await self.periodic_update_guild_data.start() # pylint: disable=no-member
@tasks.loop(hours=24) @tasks.loop(hours=24)
async def periodic_update_guild_data(self): async def periodic_update_guild_data(self) -> None:
"""Loops every day, calls update_guild_data.""" """Loops every day, calls update_guild_data."""
self.update_guild_data() self.update_guild_data()
def update_guild_data(self): def update_guild_data(self) -> None:
""" """Updates all known guild's data in the database."""
Updates all known guild's data in the database.
"""
with Session(engine) as session: with Session(engine) as session:
for guild in self.bot.guilds: for guild in self.bot.guilds:
# update given guild name for guild id # update given guild name for guild id
stmt = select(Guild).where(Guild.GuildId == guild.id) stmt = select(Guild).where(col(Guild.id) == guild.id)
guild_data = session.scalar(stmt) guild_data = session.scalar(stmt)
# just in case checking if the guild doesn't exist. # just in case checking if the guild doesn't exist.
if guild_data is None: if guild_data is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name) new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild) session.add(new_guild)
else: else:
guild_data.GuildName = guild.name guild_data.name = guild.name
session.commit() session.commit()
+5 -7
View File
@@ -1,6 +1,4 @@
""" """Cog for creating submission process."""
Cog for creating submission process.
"""
from nextcord.ext import commands from nextcord.ext import commands
@@ -15,14 +13,14 @@ class SubmissionCog(commands.Cog, name="Submission Cog"):
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): async def on_ready(self) -> None:
"""Called when cog is loaded""" """Called when cog is loaded."""
self.bot.add_view(SubmissionCreateButton(self.bot)) self.bot.add_view(SubmissionCreateButton(self.bot))
self.bot.add_view(VerificationView()) self.bot.add_view(VerificationView())
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
"""Adds function to bot""" """Adds function to bot."""
bot.add_cog(SubmissionCog(bot)) bot.add_cog(SubmissionCog(bot))
+9 -12
View File
@@ -3,10 +3,10 @@
import nextcord import nextcord
from sqlalchemy import delete, select from sqlalchemy import delete, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import col
from database.db import Submission, engine from database.db import engine
from database.models import Submission
# pylint: disable=import-error
from utilities.utils import submit_score from utilities.utils import submit_score
@@ -22,8 +22,8 @@ class VerificationView(nextcord.ui.View):
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
delete(Submission) delete(Submission)
.where(Submission.GuildId == interaction.guild_id) .where(col(Submission.guild_id) == interaction.guild_id)
.where(Submission.MessageId == interaction.message.id) .where(col(Submission.message_id) == interaction.message.id)
) )
session.execute(stmt) session.execute(stmt)
session.commit() session.commit()
@@ -35,23 +35,20 @@ class VerificationView(nextcord.ui.View):
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(Submission) select(Submission)
.where(Submission.GuildId == interaction.guild_id) .where(col(Submission.guild_id) == interaction.guild_id)
.where(Submission.MessageId == interaction.message.id) .where(col(Submission.message_id) == interaction.message.id)
) )
submission = session.scalar(stmt) if (submission := session.scalar(stmt)) is None:
if submission is None:
await interaction.response.send_message("Could not find submission data in database.", ephemeral=True) await interaction.response.send_message("Could not find submission data in database.", ephemeral=True)
await interaction.message.delete() await interaction.message.delete()
return return
await submit_score( await submit_score(
interaction, submission.GroupId, submission.CategoryId, submission.UserName, submission.Score interaction, submission.group_id, submission.category_id, submission.username, submission.score
) )
session.delete(submission) session.delete(submission)
session.commit() session.commit()
content = interaction.message.content content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>." content += f"\nApproved by <@{interaction.user.id}>."
# remove the bot information embed. it should always be the first one.
await interaction.message.edit(content=content, view=None) await interaction.message.edit(content=content, view=None)
+20 -9
View File
@@ -1,19 +1,30 @@
import os
from dotenv import load_dotenv
from sqlmodel import Session, SQLModel, create_engine, select from sqlmodel import Session, SQLModel, create_engine, select
from models import Guild from models import CategoryGroup, Guild
load_dotenv()
SQL_DB = os.getenv("SQL_DB", "")
SQL_HOST = os.getenv("SQL_HOST", "")
SQL_PASS = os.getenv("SQL_PASS", "")
SQL_USER = os.getenv("SQL_USER", "")
url = "sqlite:///database.db" url = "sqlite:///database.db"
# url = f"mysql+pymysql://{SQL_USER}:{SQL_PASS}@{SQL_HOST}/{SQL_DB}"
engine = create_engine(url) engine = create_engine(url)
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)
with Session(engine) as session: with Session(engine) as session:
# guild = Guild(id=1, name="test guild", submission_channel=1, verification_channel=2) guild = Guild(id=1, name="test guild")
# session.add(guild) session.add(guild)
# group1 = CategoryGroup(name="group1", channel_id=1, guild_id=1, message_id=1) group1 = CategoryGroup(name="group1", channel_id=1, guild_id=1, message_id=1)
# group2 = CategoryGroup(name="group2", channel_id=2, guild_id=1, message_id=2) group2 = CategoryGroup(name="group2", channel_id=2, guild_id=1, message_id=2)
# session.add(group1) session.add(group1)
# session.add(group2) session.add(group2)
# session.commit() session.commit()
# anotherone = Guild(id=2, name="test guild 2", submission_channel=1, verification_channel=2) # anotherone = Guild(id=2, name="test guild 2", submission_channel=1, verification_channel=2)
# session.add(anotherone) # session.add(anotherone)
@@ -23,7 +34,7 @@ with Session(engine) as session:
# session.add(group2) # session.add(group2)
# session.commit() # session.commit()
stmt = select(Guild).where(Guild.id == 1) stmt = select(Guild).where(Guild.name == "Dakiny")
guild = session.scalar(stmt) guild = session.scalar(stmt)
print(guild.category_groups) print(guild.category_groups)
+39 -30
View File
@@ -1,6 +1,6 @@
"""File holds all model definitions of SQL tables.""" """File holds all model definitions of SQL tables."""
from sqlalchemy.dialects.mysql import BIGINT, INTEGER, SMALLINT from sqlalchemy.dialects.mysql import BIGINT, BOOLEAN, FLOAT, INTEGER, SMALLINT, VARCHAR
from sqlmodel import Column, Field, ForeignKey, Relationship, SQLModel, UniqueConstraint from sqlmodel import Column, Field, ForeignKey, Relationship, SQLModel, UniqueConstraint
@@ -19,12 +19,16 @@ class Guild(SQLModel, table=True):
__tablename__: str = "Guilds" __tablename__: str = "Guilds"
id: int = Field(sa_column=Column(BIGINT(unsigned=True), primary_key=True, nullable=False)) id: int = Field(sa_column=Column("GuildId", BIGINT(unsigned=True), primary_key=True, nullable=False))
name: str = Field(max_length=95) name: str = Field(sa_column=Column("GuildName", VARCHAR), max_length=95)
# deprecated, not currently used. # deprecated, not currently used.
submission_channel: int = Field(sa_column=Column(BIGINT(unsigned=True))) submission_channel: int | None = Field(
verification_channel: int = Field(sa_column=Column(BIGINT(unsigned=True))) sa_column=Column("SubmissionChannel", BIGINT(unsigned=True), nullable=True), default=None
active: bool = Field(default=True) )
verification_channel: int | None = Field(
sa_column=Column("VerificationChannel", BIGINT(unsigned=True), nullable=True), default=None
)
active: bool = Field(sa_column=Column("Active", BOOLEAN), default=True)
category_groups: list["CategoryGroup"] = Relationship(back_populates="guild", cascade_delete=True) category_groups: list["CategoryGroup"] = Relationship(back_populates="guild", cascade_delete=True)
@@ -49,21 +53,22 @@ class CategoryGroup(SQLModel, table=True):
__tablename__: str = "CategoryGroups" __tablename__: str = "CategoryGroups"
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(sa_column=Column("GroupId", INTEGER, primary_key=True), default=None)
name: str = Field(max_length=50) name: str = Field(sa_column=Column("GroupName", VARCHAR), max_length=50)
channel_id: int = Field(sa_column=Column(BIGINT(unsigned=True))) channel_id: int = Field(sa_column=Column("ChannelId", BIGINT(unsigned=True)))
guild_id: int = Field( guild_id: int = Field(
sa_column=Column( sa_column=Column(
"GuildId",
BIGINT(unsigned=True), BIGINT(unsigned=True),
ForeignKey("Guilds.id", ondelete="CASCADE"), ForeignKey("Guilds.GuildId", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
) )
message_id: int = Field(sa_column=Column(BIGINT(unsigned=True))) message_id: int | None = Field(sa_column=Column("MessageId", BIGINT(unsigned=True)), default=None)
pad_length: int = Field(default=12, sa_column=Column(SMALLINT(unsigned=True))) pad_length: int = Field(default=12, sa_column=Column("PadLength", SMALLINT(unsigned=True)))
username_length: int = Field(default=12, sa_column=Column(SMALLINT(unsigned=True))) username_length: int = Field(default=12, sa_column=Column("UsernameLength", SMALLINT(unsigned=True)))
__table_args__ = (UniqueConstraint("guild_id", "name", name="UniqueGuildCategoryGroup"),) __table_args__ = (UniqueConstraint("GuildId", "GroupName", name="UniqueGuildCategoryGroup"),)
guild: Guild | None = Relationship(back_populates="category_groups") guild: Guild | None = Relationship(back_populates="category_groups")
categories: list["Category"] | None = Relationship(back_populates="category_group", cascade_delete=True) categories: list["Category"] | None = Relationship(back_populates="category_group", cascade_delete=True)
@@ -91,21 +96,22 @@ class Category(SQLModel, table=True):
__tablename__: str = "Categories" __tablename__: str = "Categories"
id: int = Field(default=None, primary_key=True) id: int = Field(sa_column=Column("CategoryId", INTEGER, primary_key=True), default=None)
name: str = Field(max_length=30) name: str = Field(sa_column=Column("CategoryName", VARCHAR), max_length=30)
group_id: int = Field( group_id: int = Field(
sa_column=Column( sa_column=Column(
"GroupId",
INTEGER(unsigned=True), INTEGER(unsigned=True),
ForeignKey("CategoryGroups.id", ondelete="CASCADE"), ForeignKey("CategoryGroups.GroupId", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
) )
size: int = Field(sa_column=Column(SMALLINT(unsigned=True), default=3, nullable=False)) size: int = Field(sa_column=Column("Size", SMALLINT(unsigned=True), nullable=False), default=3)
is_ascending: bool = Field(default=True, nullable=False) is_ascending: bool = Field(sa_column=Column("IsAscending", BOOLEAN, nullable=False), default=True)
is_time_record: bool = Field(default=False, nullable=False) is_time_record: bool = Field(sa_column=Column("IsTimeRecord", BOOLEAN, nullable=False), default=False)
is_validated: bool = Field(default=True, nullable=False) is_validated: bool = Field(sa_column=Column("IsValidated", BOOLEAN, nullable=False), default=True)
__table_args__ = (UniqueConstraint("group_id", "name", name="UniqueCategoryInGroup"),) __table_args__ = (UniqueConstraint("GroupId", "CategoryName", name="UniqueCategoryInGroup"),)
category_group: CategoryGroup | None = Relationship(back_populates="categories") category_group: CategoryGroup | None = Relationship(back_populates="categories")
scores: list["Score"] | None = Relationship(back_populates="category", cascade_delete=True) scores: list["Score"] | None = Relationship(back_populates="category", cascade_delete=True)
@@ -126,12 +132,13 @@ class Score(SQLModel, table=True):
__tablename__: str = "Scores" __tablename__: str = "Scores"
username: str = Field(max_length=30, nullable=False, primary_key=True) username: str = Field(sa_column=Column("UserName", VARCHAR, primary_key=True, nullable=False), max_length=30)
score: float = Field(nullable=False) score: float = Field(sa_column=Column("Score", FLOAT, nullable=False))
category_id: int = Field( category_id: int = Field(
sa_column=Column( sa_column=Column(
"CategoryId",
INTEGER(unsigned=True), INTEGER(unsigned=True),
ForeignKey("Categories.id", ondelete="CASCADE"), ForeignKey("Categories.CategoryId", ondelete="CASCADE"),
nullable=False, nullable=False,
primary_key=True, primary_key=True,
), ),
@@ -158,6 +165,7 @@ class Submission(SQLModel, table=True):
category_id: int = Field( category_id: int = Field(
sa_column=Column( sa_column=Column(
"CategoryId",
INTEGER(unsigned=True), INTEGER(unsigned=True),
nullable=False, nullable=False,
primary_key=True, primary_key=True,
@@ -165,13 +173,14 @@ class Submission(SQLModel, table=True):
) )
group_id: int = Field( group_id: int = Field(
sa_column=Column( sa_column=Column(
"GroupId",
INTEGER(unsigned=True), INTEGER(unsigned=True),
nullable=False, nullable=False,
) )
) )
guild_id: int = Field(sa_column=Column(BIGINT(unsigned=True), nullable=False)) guild_id: int = Field(sa_column=Column("GuildId", BIGINT(unsigned=True), nullable=False))
message_id: int = Field(sa_column=Column(BIGINT(unsigned=True), nullable=False)) message_id: int = Field(sa_column=Column("MessageId", BIGINT(unsigned=True), nullable=False))
# Score is stored as a string until converted during submission. # Score is stored as a string until converted during submission.
score: str = Field(max_length=30, nullable=False) score: str = Field(sa_column=Column("Score", VARCHAR, nullable=False), max_length=30)
user_id: int = Field(sa_column=Column(BIGINT(unsigned=True), nullable=False)) user_id: int = Field(sa_column=Column("UserId", BIGINT(unsigned=True), nullable=False))
username: str = Field(max_length=30, nullable=False, primary_key=True) username: str = Field(sa_column=Column("UserName", VARCHAR, nullable=False, primary_key=True), max_length=30)
+1
View File
@@ -0,0 +1 @@
"""Package of utility functions and environment configuration variables."""
+3 -4
View File
@@ -8,7 +8,6 @@ from sqlalchemy import distinct, select
from sqlalchemy.orm import Session, aliased from sqlalchemy.orm import Session, aliased
from sqlmodel import col from sqlmodel import col
# pylint: disable=import-error
from database.db import engine from database.db import engine
from database.models import Category, CategoryGroup, Guild, Score from database.models import Category, CategoryGroup, Guild, Score
@@ -65,7 +64,7 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
""" """
with Session(engine) as session: with Session(engine) as session:
# Select all groups in guild. # Select all groups in guild.
stmt = select(CategoryGroup).wherecol((CategoryGroup.guild_id) == guild_id) stmt = select(CategoryGroup).where(col(CategoryGroup.guild_id) == guild_id)
# dict has to have key values of same type for discord. # dict has to have key values of same type for discord.
group_dict: dict[str, str] = {group.name: str(group.id) for group in session.scalars(stmt)} group_dict: dict[str, str] = {group.name: str(group.id) for group in session.scalars(stmt)}
@@ -348,7 +347,7 @@ async def send_highscore_message(session: Session, guild: nextcord.Guild, group:
if group.message_id is None: if group.message_id is None:
# message doesn't exist. # message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string) message: nextcord.Message = await channel.send(highscore_string)
group.MessageId = message.id group.message_id = message.id
session.commit() session.commit()
else: else:
message: nextcord.Message = await channel.fetch_message(group.message_id) message: nextcord.Message = await channel.fetch_message(group.message_id)
@@ -391,7 +390,7 @@ async def submit_score(
existing_score = session.scalar(stmt) existing_score = session.scalar(stmt)
if existing_score: if existing_score:
existing_score.Score = score_f existing_score.score = score_f
else: else:
score_obj = Score(username=username, score=score_f, category_id=category_id) score_obj = Score(username=username, score=score_f, category_id=category_id)
session.add(score_obj) session.add(score_obj)