Compare commits

...
8 changed files with 186 additions and 252 deletions
+15 -154
View File
@@ -2,6 +2,7 @@
Contains classes and enum for tracking data on submissions. Contains classes and enum for tracking data on submissions.
""" """
from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from nextcord import Embed from nextcord import Embed
@@ -10,8 +11,10 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error # pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, engine from utilities.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import score_float_to_str
# TODO use the fancy enums
class SubmissionState(Enum): class SubmissionState(Enum):
""" """
Enum for which state a submission is in Enum for which state a submission is in
@@ -25,158 +28,23 @@ class SubmissionState(Enum):
VERIFY = 6 VERIFY = 6
# TODO remake into a data class @dataclass
class Submission: class Submission:
""" """
Class for an individual submission. Contains submission information such as Class for an individual submission. Contains submission information such as
guild id, message id, table, category, username, score and submission state. guild id, message id, table, category, username, score and submission state.
""" """
def __init__(self, guild_id: int, group_id: int, username: str) -> None: category_id: int = field(init=False, default=0)
""" group_id: int
Default constructor for Submission. Just need to know guild_id, table, and username. guild_id: int
Score and category are defaulted at empty, message id is 0, and submission state is_time_record: bool = field(init=False, default=False)
is SubmissionState.CATEGORY. message_id: int = field(init=False, default=0)
""" score: float = field(init=False, default=0.0)
self._category_id: int = 0 submission_state: SubmissionState = field(init=False, default=SubmissionState.CATEGORY)
self._guild_id: int = guild_id username: str
self._message_id: int = 0
self._score: str = ""
self._submission_state: SubmissionState = SubmissionState.CATEGORY
self._group_id: int = group_id
self._username: str = username
@property def get_embed(self) -> Embed:
def category_id(self) -> int:
"""
Getter for Submission's Category id.
Returns:
int: Category id.
"""
return self._category_id
@category_id.setter
def category_id(self, category: int) -> None:
"""
Setter for Submission's Category id.
Args:
category (int): Category id
"""
self._category_id = category
@property
def guild_id(self) -> int:
"""
Getter for guild id Submission is happening in. Will not change.
Returns:
int: guild id
"""
return self._guild_id
@property
def message_id(self) -> int:
"""
Getter for message id that Submission has a view on.
Returns:
int: message id
"""
return self._message_id
@message_id.setter
def message_id(self, m_id: int) -> None:
"""
Setter for message id that Submission has a view on.
Args:
m_id (int): message id
"""
self._message_id = m_id
@property
def score(self) -> str:
"""
Getter for score.
Returns:
str: score
"""
return self._score
@score.setter
def score(self, score: str) -> None:
"""
Setter for score.
Args:
score (str): score
"""
self._score = score
@property
def submission_state(self) -> SubmissionState:
"""
Getter for state Submission is in.
Returns:
SubmissionState: State the Submission is in
"""
return self._submission_state
@submission_state.setter
def submission_state(self, state: SubmissionState) -> None:
"""
Setter for state Submission is in.
Args:
state (SubmissionState): State to change to
"""
self._submission_state = state
@property
def group_id(self) -> str:
"""
Getter for group Submission is for.
Returns:
str: group id
"""
return self._group_id
@group_id.setter
def group_id(self, table: str) -> None:
"""
Getter for table Submission is for.
Args:
table (str): group id
"""
self._group_id = table
@property
def username(self) -> str:
"""
Getter for Submission owner's username.
Returns:
str: Submission owner's username
"""
return self._username
@username.setter
def username(self, username: str) -> None:
"""
Setter for Submission owner's username.
Args:
username (str): Submission owner's username
"""
self._username = username
def get_embeds(self) -> "list[Embed]":
"""Creates an embed based off of the submission state and fields filled out.""" """Creates an embed based off of the submission state and fields filled out."""
state: SubmissionState = self.submission_state state: SubmissionState = self.submission_state
@@ -210,16 +78,9 @@ class Submission:
) )
embed.add_field(name="Category", value=session.scalar(stmt), inline=False) embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
if self.score: if self.score:
embed.add_field(name="Score", value=self.score, inline=False) embed.add_field(name="Score", value=score_float_to_str(self.score, self.is_time_record), inline=False)
embeds = [embed] return embed
if state is SubmissionState.VERIFY:
embed = Embed(title="Bot Information", description="Please disregard.")
embed.add_field(name="GroupId", value=self.group_id)
embed.add_field(name="CategoryId", value=self.category_id)
embeds.append(embed)
return embeds
class SubmissionMap: class SubmissionMap:
+27 -1
View File
@@ -19,7 +19,12 @@ class GuildListener(commands.Cog, name="Guild Listener"):
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): async def on_guild_join(self, guild: nextcord.Guild):
"""Listener for Guild Join. Adds guild to database.""" """
Listener for Guild Join. Adds guild to database.
Args:
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(Guild.GuildId == guild.id)
@@ -31,6 +36,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
session.add(new_guild) session.add(new_guild)
else: else:
guild_data.GuildName = guild.name guild_data.GuildName = guild.name
guild_data.Active = True
session.commit() session.commit()
@@ -39,6 +45,26 @@ class GuildListener(commands.Cog, name="Guild Listener"):
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849) imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
await imcompiling.send(f'Agon has joined server "{guild.name}"!') await imcompiling.send(f'Agon has joined server "{guild.name}"!')
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild):
"""
Listens to when the bot has been removed from a guild. Update's the database to record the active flag as false.
Args:
guild (nextcord.Guild): Guild that was left.
"""
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id)
guild_data = session.scalar(stmt)
guild_data.Active = False
session.commit()
# let imcompiling#2272 know about leaving a server.
# staying with fetch due to intents not being set up correctly. Maybe change to get eventually.
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
await imcompiling.send(f'Agon has been removed from "{guild.name}".')
# 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.
+35 -52
View File
@@ -12,7 +12,8 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error # pylint: disable=import-error
from classes.submission import SubmissionState from classes.submission import SubmissionState
from highscores import submission_objects from highscores import submission_objects
from utilities.sql_data_classes import Category, Guild, engine from utilities.sql_data_classes import Guild, engine
from utilities.utils import score_str_to_float
class SubmissionListener(commands.Cog, name="Message Listener"): class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -41,60 +42,42 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
await self.bot.get_guild(guild_id).get_channel(submission_channel_id).fetch_message(message_id) await self.bot.get_guild(guild_id).get_channel(submission_channel_id).fetch_message(message_id)
) )
# need check if correct user and correct channel # need check if correct user and correct channel
if message.channel.id == submission_channel_id: if message.channel.id == submission_channel_id:
state = submission.submission_state state = submission.submission_state
if state == SubmissionState.SCORE and message.content: if state == SubmissionState.SCORE and message.content:
# check with regex if score matches correctly
is_time_record = submission.is_time_record
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_id) time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content)
int_regex = re.search("^[0-9]+$", message.content)
# check with regex if score matches correctly if time_regex is None and is_time_record:
is_time_record = session.scalar(stmt) embed = submission_message.embeds[0]
embed.add_field(
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content) name="Error",
int_regex = re.search("^[0-9]+$", message.content) value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
if time_regex is None and is_time_record:
embeds = submission_message.embeds[0]
embeds.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
)
elif int_regex is None and not is_time_record:
embeds = submission_message.embeds[0]
embeds.add_field(name="Error", value="Number record must only be number.")
else:
score = message.content
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")
submission.score = score
embeds = submission.get_embeds()
await submission_message.edit(
embeds=embeds,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission.username = message.content
embeds = submission.get_embeds()
await submission_message.edit(
embeds=embeds,
) )
elif int_regex is None and not is_time_record:
embed = submission_message.embeds[0]
embed.add_field(name="Error", value="Number record must only be number.")
else:
submission.score = score_str_to_float(message.content, is_time_record)
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission.username = message.content
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
await message.delete() await message.delete()
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
+33 -15
View File
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session, aliased
# pylint: disable=import-error # pylint: disable=import-error
from classes.submission import Submission, SubmissionState from classes.submission import Submission, SubmissionState
from highscores import submission_objects from highscores import submission_objects
from utilities.sql_data_classes import Category, CategoryGroup, Guild, engine from utilities.sql_data_classes import Category, CategoryGroup, Guild, Submissions, engine
from .verification_view import VerificationView from .verification_view import VerificationView
@@ -30,7 +30,7 @@ def category_select_options(group_id: int) -> "list[nextcord.SelectOption]":
return options return options
############ Items used in submission process ############ # Items used in submission process ########################
class SubmissionDropdown(nextcord.ui.Select): class SubmissionDropdown(nextcord.ui.Select):
@@ -47,9 +47,14 @@ class SubmissionDropdown(nextcord.ui.Select):
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id) submission = submission_objects.get_user_submission(interaction.user.id)
submission.category_id = self.values[0] submission.category_id = self.values[0]
embeds = submission.get_embeds()
with Session(engine) as session:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_id)
submission.is_time_record = session.scalar(stmt)
embed = submission.get_embed()
await interaction.message.edit( await interaction.message.edit(
embeds=embeds, embed=embed,
) )
@@ -100,8 +105,8 @@ class NextButton(nextcord.ui.Button):
else: else:
state = SubmissionState(state.value + 1) state = SubmissionState(state.value + 1)
submission.submission_state = state submission.submission_state = state
embeds = submission.get_embeds() embed = submission.get_embed()
await interaction.edit(embeds=embeds, view=SubmissionView(self._bot, interaction.user.id)) await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class SubmitButton(nextcord.ui.Button): class SubmitButton(nextcord.ui.Button):
@@ -116,27 +121,40 @@ class SubmitButton(nextcord.ui.Button):
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id) stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verification_channel_id = session.scalar(stmt) verification_channel_id = session.scalar(stmt)
channel = interaction.guild.get_channel(verification_channel_id) channel = interaction.guild.get_channel(verification_channel_id)
submission = submission_objects.get_user_submission(interaction.user.id) active_submission = submission_objects.get_user_submission(interaction.user.id)
error_response = "Registered Verification channel does not exist or was never registered. \ error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins." Contact server admins."
assert channel is not None, await channel.send(error_response) assert channel is not None, await channel.send(error_response)
submission.submission_state = SubmissionState.VERIFY active_submission.submission_state = SubmissionState.VERIFY
embeds = submission.get_embeds() embed = active_submission.get_embed()
await channel.send( verification_message = await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments], files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(), view=VerificationView(),
embeds=embeds, embed=embed,
content=f"Submission from <@{interaction.user.id}>.", content=f"Submission from <@{interaction.user.id}>.",
) )
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None) await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
new_submission = Submissions(
CategoryId=active_submission.category_id,
GroupId=active_submission.group_id,
IsTimeRecord=active_submission.is_time_record,
Score=active_submission.score,
SubmissionMessageId=interaction.message.id,
UserName=active_submission.username,
VerificationMessageId=verification_message.id,
)
submission_objects.remove_user_submission(interaction.user.id) submission_objects.remove_user_submission(interaction.user.id)
with Session(engine) as session:
session.add(new_submission)
session.commit()
############ Submission Views ############ # Submission Views ########################
class SubmissionCreateButton(nextcord.ui.View): class SubmissionCreateButton(nextcord.ui.View):
@@ -171,11 +189,11 @@ class SubmissionCreateButton(nextcord.ui.View):
# a user should only have 1 submission active at once. # a user should only have 1 submission active at once.
# split username on pipe. Decided to store whole username... # split username on pipe. Decided to store whole username...
username: str = interaction.user.display_name.split("|")[0] username: str = interaction.user.display_name.split("|")[0]
submission: Submission = Submission(interaction.guild_id, group_id, username) submission: Submission = Submission(group_id, interaction.guild_id, username)
submission_objects.add_submission(interaction.user.id, submission) submission_objects.add_submission(interaction.user.id, submission)
embeds = submission.get_embeds() embed = submission.get_embed()
submission_message = await channel.send( submission_message = await channel.send(
f"<@{interaction.user.id}>", f"<@{interaction.user.id}>",
@@ -183,7 +201,7 @@ class SubmissionCreateButton(nextcord.ui.View):
self._bot, self._bot,
interaction.user.id, interaction.user.id,
), ),
embeds=embeds, embed=embed,
) )
# update the message_id once its created # update the message_id once its created
+22 -17
View File
@@ -1,6 +1,10 @@
"""View for verification buttons""" """View for verification buttons"""
import nextcord import nextcord
from sqlalchemy import select
from sqlalchemy.orm import Session
from utilities.sql_data_classes import Submissions, engine
# pylint: disable=import-error # pylint: disable=import-error
from utilities.utils import submit_score from utilities.utils import submit_score
@@ -17,33 +21,34 @@ class VerificationView(nextcord.ui.View):
self, button: nextcord.ui.Button, interaction: nextcord.Interaction self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Button for denying score submission.""" """Button for denying score submission."""
with Session(engine) as session:
stmt = select(Submissions).where(Submissions.VerificationMessageId == interaction.message.id)
submission = session.scalar(stmt)
# let user know submission was denied
# TODO need to access bot here
# delete the submission
session.delete(stmt)
session.commit()
await interaction.message.delete() await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission") @nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission")
async def approve_button( async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Button for approving score submission.""" with Session(engine) as session:
for embed in interaction.message.embeds: stmt = select(Submissions).where(Submissions.VerificationMessageId == interaction.message.id)
content = embed.to_dict() submission = session.scalar(stmt)
for field in content["fields"]:
if field["name"] == "GroupId":
group_id = int(field["value"])
elif field["name"] == "CategoryId":
category_id = int(field["value"])
elif field["name"] == "Score":
score = field["value"]
elif field["name"] == "User Name":
user = field["value"]
try: try:
await submit_score(
await submit_score(interaction, group_id, category_id, user, score) interaction, submission.GroupId, submission.CategoryId, submission.UserName, submission.Score
)
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, embed=interaction.message.embeds[0]) # TODO send message to let user know submission was accepted. Need access to bot here.
except UnboundLocalError: except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.") await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None) await interaction.message.edit(view=None)
+12
View File
@@ -6,6 +6,7 @@ CREATE TABLE Guilds(
GuildName varchar(95) not null, GuildName varchar(95) not null,
SubmissionChannel bigint unsigned, SubmissionChannel bigint unsigned,
VerificationChannel bigint unsigned, VerificationChannel bigint unsigned,
Active boolean DEFAULT true,
PRIMARY KEY (GuildId) PRIMARY KEY (GuildId)
); );
@@ -41,4 +42,15 @@ CREATE TABLE Scores(
CategoryId int unsigned not null, CategoryId int unsigned not null,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE, FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE,
PRIMARY KEY (CategoryId, UserName) PRIMARY KEY (CategoryId, UserName)
);
CREATE TABLE Submissions(
CategoryId int unsigned not null,
GroupId int unsigned not null,
IsTimeRecord boolean not null,
Score float not null,
SubmissionMessageId bigint unsigned not null,
UserName varchar(30) not null,
VerificationMessageId bigint unsigned not null,
PRIMARY KEY (CategoryId, GroupId, UserName)
); );
+3 -2
View File
@@ -14,7 +14,8 @@ Base = automap_base()
Base.prepare(autoload_with=engine) Base.prepare(autoload_with=engine)
# define the classes # define the classes
Guild = Base.classes.Guilds
CategoryGroup = Base.classes.CategoryGroups
Category = Base.classes.Categories Category = Base.classes.Categories
CategoryGroup = Base.classes.CategoryGroups
Guild = Base.classes.Guilds
Score = Base.classes.Scores Score = Base.classes.Scores
Submissions = Base.classes.Submissions
+39 -11
View File
@@ -321,6 +321,44 @@ def get_time_str(time_in: float) -> str:
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in)) return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
def score_float_to_str(score: float, is_time_record: bool) -> str:
"""
Converts a float representation of a score into a string. Easier to transport the score as one data type.
Calls get_time_str if the score is a time record, otherwise just converts the string.
Args:
score (float): float representation of the score.
is_time_record (bool): true if the score is a time record.
Returns:
str: string representation of the score.
"""
if is_time_record:
return get_time_str(score)
else:
return str(score)
def score_str_to_float(score: str, is_time_record: bool) -> float:
"""
Converts a string representation of score into a float. Easier to transport the score as one data type.
Operates differently depending on if the score is a time record or an int.
Args:
score (str): string representation of the score.
is_time_record (bool): true if the score is a time record.
Returns:
float: float representation of the score.
"""
if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1])
return (minutes * 60) + seconds
else:
return float(score)
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message: async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message:
""" """
Function takes care of checking if a message exists for a group. Function takes care of checking if a message exists for a group.
@@ -351,7 +389,7 @@ async def send_highscore_message(session: Session, guild: nextcord.Guild, group:
async def submit_score( async def submit_score(
interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: str interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: float
) -> None: ) -> None:
""" """
Adds passed score to group. Then updates group message by calling send_highscore_message. Adds passed score to group. Then updates group message by calling send_highscore_message.
@@ -366,16 +404,6 @@ async def submit_score(
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == category_id)
is_time_record = session.scalar(stmt)
if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1])
score = (minutes * 60) + seconds
else:
score = float(score)
stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id) stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id)
existing_score = session.scalar(stmt) existing_score = session.scalar(stmt)