Compare commits

...
Author SHA1 Message Date
imcompiling cdb54658ee Added new class for submission command. 2025-02-17 17:57:59 -05:00
imcompiling 4ce37ee3ea Add submissions table. 2025-02-17 17:40:03 -05:00
imcompiling c62b87c7f7 Apply fixed for ruff smells. 2025-02-17 16:39:10 -05:00
imcompiling ba04858328 Remove pylint stuff. 2025-02-17 16:08:29 -05:00
imcompiling a12fca6220 Address ruff smells in create.py. 2025-02-17 15:03:07 -05:00
imcompiling 136702654e Ran ruff check --fix. 2025-02-17 14:56:33 -05:00
imcompiling 23adad403a Merge branch '66-update-build-process'
build_develop / build (push) Successful in 56s
2025-02-17 14:50:36 -05:00
imcompiling 3a72d9f0e7 Update renovatebot to not automatically merge minor dockerfile changes. 2025-02-17 14:45:08 -05:00
imcompiling da0acb2827 Merge pull request 'Update actions/checkout action to v4' (#72) from renovate/actions-checkout-4.x into main
build_develop / build (push) Successful in 48s
Reviewed-on: #72
2025-02-16 11:25:55 -05:00
renovate-bot 0baffe96f3 Update actions/checkout action to v4 2025-02-16 05:01:09 +00:00
11 changed files with 288 additions and 71 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Docker
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Docker
+16 -27
View File
@@ -1,48 +1,37 @@
"""
Commands for creating items in bot.
"""
"""Commands for creating items in bot."""
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 database_setup.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.
"""
"""Class holds commands for creating things."""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for CreateCog class.
"""
"""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.
"""
async def create(self, interaction: nextcord.Interaction) -> None:
"""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(
async def category( # noqa: PLR0913 need all these arguments because discord command.
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 lower value is a better score.", required=True),
is_time_record: bool = nextcord.SlashOption(
is_ascending: bool = nextcord.SlashOption(description="If lower value is a better score.", required=True), # noqa: FBT001
is_time_record: bool = nextcord.SlashOption( # noqa: FBT001
description="If the score should be shown in time format. Cannot be changed.", required=True
),
is_validated: bool = nextcord.SlashOption(
is_validated: bool = nextcord.SlashOption( # noqa: FBT001
description="If scores need to be validated before posted.", required=True
),
size: int = nextcord.SlashOption(
@@ -50,8 +39,9 @@ class CreateCog(commands.Cog, name="Create Cog"):
),
) -> 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().
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.
@@ -89,8 +79,6 @@ class CreateCog(commands.Cog, name="Create Cog"):
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,
@@ -118,8 +106,9 @@ class CreateCog(commands.Cog, name="Create Cog"):
),
) -> 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().
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.
@@ -156,7 +145,7 @@ class CreateCog(commands.Cog, name="Create Cog"):
)
@category.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:
"""
Collects list of table names and compares to input table name for autocomplete assistance.
+22 -32
View File
@@ -1,6 +1,4 @@
"""
Commands for deleting groups/categories/scores in database and messages in discord..
"""
"""Commands for deleting groups/categories/scores in database and messages in discord."""
import nextcord
from nextcord.ext import commands
@@ -21,26 +19,18 @@ from .views.delete_confirm import DeleteConfirmView, create_delete_id
class DeleteCog(commands.Cog, name="Table Management"):
"""
Class holds commands for deleting.
"""
"""Class holds commands for deleting."""
def __init__(self) -> None:
"""
Default constructor for DeleteCog class.
"""
"""Default constructor for DeleteCog class."""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def delete(self, interaction: nextcord.Interaction):
"""
Parent slash command for deleting things. Never called.
"""
async def delete(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for deleting things. Never called."""
@delete.subcommand()
async def all(self, interaction: nextcord.Interaction):
"""
Subcommand parent for deleting all of a thing. Never called.
"""
async def all(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent for deleting all of a thing. Never called."""
@all.subcommand(description="Clears highscores channel of all table messages. Does not change data.")
async def channel_messages(
@@ -55,6 +45,7 @@ class DeleteCog(commands.Cog, name="Table Management"):
) -> 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:
@@ -95,10 +86,11 @@ class DeleteCog(commands.Cog, name="Table Management"):
user_name: str = nextcord.SlashOption(
description="User to remove scores for. Note: This is not always a user's discord name.", required=True
),
):
) -> None:
"""
Searches all of guild's scores data for any score from the user. Removes. Updates table
message is user was removed from that table.
Searches all of guild's scores data for any score from the user and removes.
Updates table message is user was removed from that table.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
@@ -157,10 +149,8 @@ class DeleteCog(commands.Cog, name="Table Management"):
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.
"""
async def user(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent for deleting user information. Never called."""
@user.subcommand(description="Deletes a user's scores from a category.")
async def from_category(
@@ -171,7 +161,7 @@ class DeleteCog(commands.Cog, name="Table Management"):
name="category", description="Category to delete user from.", required=True
),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
):
) -> None:
"""
Removes a user from a category. Uses autofill functions to fill out group_id, category_id, and user_name.
@@ -205,9 +195,9 @@ class DeleteCog(commands.Cog, name="Table Management"):
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
):
) -> None:
"""
Removes user from a goup. Uses autofill functions to fill out group_id and user_name.
Removes user from a group. Uses autofill functions to fill out group_id and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
@@ -233,7 +223,7 @@ class DeleteCog(commands.Cog, name="Table Management"):
@from_group.on_autocomplete("group_id")
@category.on_autocomplete("group_id")
@group.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:
"""
Autocomplete function for table names.
@@ -245,7 +235,7 @@ class DeleteCog(commands.Cog, name="Table Management"):
@from_category.on_autocomplete("category_id")
@category.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str):
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
@@ -262,7 +252,7 @@ class DeleteCog(commands.Cog, name="Table Management"):
@from_group.on_autocomplete("user_name")
async def user_autocomplete(
self, interaction: nextcord.Interaction, user_name: str, group_id: str, category_id: str | None = None
):
) -> None:
"""
Autocomplete functions for user names.
@@ -279,12 +269,12 @@ class DeleteCog(commands.Cog, name="Table Management"):
)
@users.on_autocomplete("user_name")
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.
Args:
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))
+218
View File
@@ -0,0 +1,218 @@
"""Contains command for submitting a score."""
import re
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from cogs.submission.verification_view import VerificationView
from database_setup.sql_data_classes import Category, CategoryGroup, Guild, Submission, engine
from utilities.utils import autofill_categories, autofill_groups, submit_score
class SubmissionCog(commands.Cog):
"""Class holds command for submitting a score."""
def __init__(self) -> None:
return
@nextcord.slash_command()
async def submit(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="Group", required=True, description="Group to submit score to."),
category_id: str = nextcord.SlashOption(
name="Category", required=True, description="Category to submit score to."
),
score: str = nextcord.SlashOption(required=True, description="Score to submit"),
username: str = nextcord.SlashOption(
required=False, description="Modified username if submitting under not your discord name."
),
) -> None:
"""
Submit a highscore.
Sets the username if one isn't passed in. Checks if a submission needs verified. Ensures that the score is
following the correct format. Then either creates the verification request or submits score if verification
isn't required.
Args:
interaction (nextcord.Interaction): Interaction of the command.
group_id (str): Is SlashOption. Group that the score category belongs in.
category_id (str): Is SlashOption. Category to submit the score in.
score (str): Is SlashOption. The score.
username (str, optional): Is SlashOption. Username to associate with the score.
"""
# fill out username if not passed in
if username is None:
username = interaction.user.display_name
with Session(engine) as session:
stmt = (
select(Category).where(Category.GroupId == int(group_id)).where(Category.CategoryId == int(category_id))
)
category: Category = session.scalar(stmt)
# check if verification is required. if no picture, deny
if category.IsValidated and not interaction.message.attachments > 0:
await interaction.response.send_message(
"This category required verification. Please attach a picture and re-submit.", ephemeral=True
)
return
# check with regex if score is in correct format
score = await self.verify_score(interaction, score, is_time_record=category.IsTimeRecord)
# check if verify_score was successful. It replies with corrections required itself.
if score is None:
return
# Create submission verification request or submit score if verification not needed.
if not category.IsValidated:
await submit_score(interaction, group_id, category_id, username, score)
await interaction.response.send_message("Score has been submitted!", ephemeral=True)
return
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verification_channel_id = session.scalar(stmt)
channel = interaction.guild.get_channel(verification_channel_id)
if channel is None:
error_response = (
"Registered Verification channel does not exist or was never registered. Contact server admins."
)
await interaction.response.send_message(error_response, ephemeral=True)
# Send the verification message to admins
verification_message = await channel.send(
files=[await attach.to_file() for attach in interaction.message.attachments],
view=VerificationView(),
embeds=self.create_embed(interaction, group_id, category_id, score, username, session),
content=f"Submission from <@{interaction.user.id}>.",
)
await interaction.response.send_message(content="Score submitted!", ephemeral=True)
# Save the submission data for later use.
submission: Submission = Submission(
CategoryId=category_id,
GroupId=group_id,
GuildId=interaction.guild_id,
MessageId=verification_message.id,
Score=score,
UserName=username,
)
session.add(submission)
session.commit()
def create_embed( # noqa: PLR0913
self,
interaction: nextcord.Interaction,
group_id: str,
category_id: str,
score: str,
username: str,
session: Session,
) -> nextcord.Embed:
"""
Creates an embed containing submission data for human verification.
Args:
interaction (nextcord.Interaction): Discord interaction of the command.
group_id (str): Used to get group name.
category_id (str): Used to get category name.
score (str): Score to set.
username (str): Username to set.
session (Session): Open connection to database.
Returns:
nextcord.Embed: Embed containing human readable submission data.
"""
embed = nextcord.Embed(title="Highscores Submission", description="Review fields and accept.")
embed.add_field(name="User Name", value=username, inline=False)
# Get group name from database
stmt = (
select(CategoryGroup.GroupName)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == group_id)
)
embed.add_field(name="Group", value=session.scalar(stmt), inline=False)
# Get category name from database
stmt = (
select(Category.CategoryName).where(Category.GroupId == group_id).where(Category.CategoryId == category_id)
)
embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
embed.add_field(name="Score", value=score, inline=False)
return embed
async def verify_score(self, interaction: nextcord.Interaction, score: str, *, is_time_record: bool) -> str | None:
"""
Uses regex to verify that scores are in the correct format.
Handles responding to the interaction if the format is incorrect. Also standardizes the time record.
Args:
interaction (nextcord.Interaction): Discord interaction of the command.
score (str): String representation of the score being verified.
is_time_record (bool): Whether the score needs to be verified/formatted as a time record.
Returns:
str | None: String of corrected score. None if formatted incorrectly.
"""
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", score)
int_regex = re.search("^[0-9]+$", score)
if time_regex is None and is_time_record:
await interaction.response.send_message(
"Time record must follow format MM:SS.ms, SS.ms, or SS.", ephemeral=True
)
return None
if int_regex is None and not is_time_record:
await interaction.response.send_message("Number record must only be number.", ephemeral=True)
return None
if is_time_record:
# standardize time score format.
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# score is already fine.
pass
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
# need to add leading colon and 0
score = "0:" + score
elif re.search("^[0-9](\\.[0-9])?$", score):
# need to add 0 in seconds and leading colon and 0
score = "0:0" + score
elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove
score = score.lstrip("0")
return score
@submit.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Autocomplete function for table names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String to be autocompleted with Group name.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@submit.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_id (str): String to be autocompleted with Category name.
group_id (str): Group to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, int(group_id), category_id)
)
+1 -1
View File
@@ -12,8 +12,8 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error
from classes.submission import SubmissionState
from main import submission_objects
from database_setup.sql_data_classes import Category, Guild, engine
from main import submission_objects
class SubmissionListener(commands.Cog, name="Message Listener"):
+1 -1
View File
@@ -7,8 +7,8 @@ from sqlalchemy.orm import Session, aliased
# pylint: disable=import-error
from classes.submission import Submission, SubmissionState
from main import submission_objects
from database_setup.sql_data_classes import Category, CategoryGroup, Guild, engine
from main import submission_objects
from .verification_view import VerificationView
+5 -5
View File
@@ -1,4 +1,4 @@
"""View for verification buttons"""
"""View for verification buttons."""
import nextcord
@@ -7,18 +7,18 @@ from utilities.utils import submit_score
class VerificationView(nextcord.ui.View):
"""Defines views for verifying score submissions"""
"""Defines views for verifying score submissions."""
def __init__(self):
def __init__(self) -> None:
super().__init__(timeout=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission")
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 score submission."""
await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission")
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 score submission."""
for embed in interaction.message.embeds:
content = embed.to_dict()
+3 -2
View File
@@ -11,7 +11,8 @@ Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
Guild = Base.classes.Guilds
CategoryGroup = Base.classes.CategoryGroups
Category = Base.classes.Categories
CategoryGroup = Base.classes.CategoryGroups
Guild = Base.classes.Guilds
Score = Base.classes.Scores
Submission = Base.classes.Submissions
+9
View File
@@ -42,4 +42,13 @@ CREATE TABLE Scores(
CategoryId int unsigned not null,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE,
PRIMARY KEY (CategoryId, UserName)
);
CREATE TABLE Submissions(
CategoryId int unsigned not null,
GroupId int unsigned not null,
GuildId int unsigned not null,
MessageId int unsigned not null,
Score varchar(30) not null, -- Score is stored as a string until converted during submission
UserName varchar(30) not null
);
+11 -1
View File
@@ -3,6 +3,10 @@
"lockFileMaintenance": {
"automerge": true
},
"major": {
"automerge": false,
"automergeType": "pr"
},
"minor": {
"automerge": true,
"automergeType": "branch"
@@ -15,5 +19,11 @@
"automerge": true,
"automergeType": "branch"
},
"ignoreTests": true
"ignoreTests": true,
"dockerfile": {
"minor": {
"automerge": false,
"automergeType": "pr"
}
}
}