Compare commits

..
Author SHA1 Message Date
imcompiling 50a3c5fa6b Rename main file to agon. 2023-02-12 22:17:21 -05:00
17 changed files with 325 additions and 482 deletions
-20
View File
@@ -1,20 +0,0 @@
name: build_develop
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Build Docker Image
run: docker build -t viker.cheezers.net:5000/agon:develop .
- name: Push Docker Image
run: docker push viker.cheezers.net:5000/agon:develop
-24
View File
@@ -1,24 +0,0 @@
name: build_release
on:
push:
tags: ["v[0-9].[0-9].[0-9]"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Set env
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Build Docker Images
run: docker build -t viker.cheezers.net:5000/agon:latest -t viker.cheezers.net:5000/agon:${{env.GIT_TAG}} .
- name: Push Latest Image
run: docker push viker.cheezers.net:5000/agon:latest
- name: Push Tagged Image
run: docker push viker.cheezers.net:5000/agon:${{env.GIT_TAG}}
+1 -1
View File
@@ -1,5 +1,5 @@
"""
main file for highscores bot
main file for agon
"""
import logging
+154 -15
View File
@@ -2,7 +2,6 @@
Contains classes and enum for tracking data on submissions.
"""
from dataclasses import dataclass, field
from enum import Enum
from nextcord import Embed
@@ -11,10 +10,8 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error
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):
"""
Enum for which state a submission is in
@@ -28,23 +25,158 @@ class SubmissionState(Enum):
VERIFY = 6
@dataclass
# TODO remake into a data class
class Submission:
"""
Class for an individual submission. Contains submission information such as
guild id, message id, table, category, username, score and submission state.
"""
category_id: int = field(init=False, default=0)
group_id: int
guild_id: int
is_time_record: bool = field(init=False, default=False)
message_id: int = field(init=False, default=0)
score: float = field(init=False, default=0.0)
submission_state: SubmissionState = field(init=False, default=SubmissionState.CATEGORY)
username: str
def __init__(self, guild_id: int, group_id: int, username: str) -> None:
"""
Default constructor for Submission. Just need to know guild_id, table, and username.
Score and category are defaulted at empty, message id is 0, and submission state
is SubmissionState.CATEGORY.
"""
self._category_id: int = 0
self._guild_id: int = guild_id
self._message_id: int = 0
self._score: str = ""
self._submission_state: SubmissionState = SubmissionState.CATEGORY
self._group_id: int = group_id
self._username: str = username
def get_embed(self) -> Embed:
@property
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."""
state: SubmissionState = self.submission_state
@@ -78,9 +210,16 @@ class Submission:
)
embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
if self.score:
embed.add_field(name="Score", value=score_float_to_str(self.score, self.is_time_record), inline=False)
embed.add_field(name="Score", value=self.score, inline=False)
return embed
embeds = [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:
-2
View File
@@ -10,7 +10,6 @@ from .help import HelpCog
from .refresh import RefreshCog
from .set import SetCog
from .update import UpdateCog
from .view import ViewCog
# This function will be called when this extension is loaded.
@@ -25,4 +24,3 @@ def setup(bot: commands.Bot):
bot.add_cog(RefreshCog(bot))
bot.add_cog(SetCog())
bot.add_cog(UpdateCog(bot))
bot.add_cog(ViewCog())
-112
View File
@@ -1,112 +0,0 @@
"""
View command heirarchy.
"""
import nextcord
import pandas as pd
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string
class ViewCog(commands.Cog, name="View Command"):
"""
Class holds commands for viewing data.
"""
def __init__(self) -> None:
"""
Default constructor for ViewCog class. Does nothing.
"""
@nextcord.slash_command()
async def view(self, interaction: nextcord.Interaction):
"""
Parent slash command for view. Never called.
"""
@view.subcommand(description="Lists all scores for a given category.")
async def leaderboard(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to show leaderboard from.", required=True
),
category_id: str = nextcord.SlashOption(
name="category", description="Category to show leaderboard.", required=True
),
):
"""
Gathers all users that have a submission in the group/category combo and returns a message with them
in ranked order.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to search from. Is SlashOption.
category_id (str): String representation of Category id to show leaderboard. Is SlashOption.
"""
str_list: "list[str]" = []
# get group, category, and all scores sorted correctly.
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
stmt = select(Category).where(Category.CategoryId == int(category_id))
category = session.scalar(stmt)
d_f = pd.read_sql_query(
sql=session.query(Score).where(Score.CategoryId == category_id).statement, con=engine
)
scores = d_f.sort_values("Score", ascending=category.IsAscending).reset_index()
# get the longest string
longest_str: int = scores.UserName.map(len).max()
for i, row in scores.iterrows():
str_list.append(
format_score_string(
row["UserName"], row["Score"], longest_str, category.IsTimeRecord, i + 1, MAX_LINE_LENGTH
)
)
message_str: str = f"**{group.GroupName} {category.CategoryName} Leaderboard!**\n```"
if len(str_list) == 0:
message_str += "No Submissions!"
else:
for line in str_list:
message_str += f"{line}\n"
message_str += "```"
await interaction.response.send_message(content=message_str)
@leaderboard.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
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))
@leaderboard.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str):
"""
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)
)
+4 -42
View File
@@ -4,11 +4,11 @@ Defines cog for guild listeners. Contains functions for various guild events.
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Guild, engine
from utilities.utils import add_preset_configuration
class GuildListener(commands.Cog, name="Guild Listener"):
@@ -19,52 +19,14 @@ class GuildListener(commands.Cog, name="Guild Listener"):
@commands.Cog.listener()
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.
"""
new_guild = Guild(GuildId=guild.id)
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id)
guild_data = session.scalar(stmt)
# just in case check if the guild already exists
if guild_data is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name)
session.add(new_guild)
else:
guild_data.GuildName = guild.name
guild_data.Active = True
session.add(new_guild)
session.commit()
# let imcompiling#2272 know about a new server joining!
# 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 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.
# It is necessary to add these functions to the bot.
-16
View File
@@ -1,16 +0,0 @@
"""
Initializing code for all loops.
"""
from nextcord.ext import commands
from .bot_presence import BotPresence
from .guild_data import GuildData
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(BotPresence(bot))
bot.add_cog(GuildData(bot))
-48
View File
@@ -1,48 +0,0 @@
"""
Contains a loop that updates guild data in the database periodically.
"""
from nextcord.ext import commands, tasks
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Guild, engine
class GuildData(commands.Cog, name="Guild Data"):
"""Class to contain loop."""
def __init__(self, bot: commands.bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is 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() # pylint: disable=no-member
@tasks.loop(hours=24)
async def periodic_update_guild_data(self):
"""Loops every day, calls update_guild_data."""
self.update_guild_data()
def update_guild_data(self):
"""
Updates all known guild's data in the database.
"""
with Session(engine) as session:
for guild in self.bot.guilds:
# update given guild name for guild id
stmt = select(Guild).where(Guild.GuildId == guild.id)
guild_data = session.scalar(stmt)
# just in case checking if the guild doesn't exist.
if guild_data is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name)
session.add(new_guild)
else:
guild_data.GuildName = guild.name
session.commit()
+52 -36
View File
@@ -11,9 +11,8 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error
from classes.submission import SubmissionState
from highscores import submission_objects
from utilities.sql_data_classes import Guild, engine
from utilities.utils import score_str_to_float
from agon import submission_objects
from utilities.sql_data_classes import Category, Guild, engine
class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -42,42 +41,59 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
await self.bot.get_guild(guild_id).get_channel(submission_channel_id).fetch_message(message_id)
)
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission.submission_state
if state == SubmissionState.SCORE and message.content:
# check with regex if score matches correctly
is_time_record = submission.is_time_record
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission.submission_state
if state == SubmissionState.SCORE and message.content:
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)
if time_regex is None and is_time_record:
embed = submission_message.embeds[0]
embed.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
# check with regex if score matches correctly
is_time_record = session.scalar(stmt)
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)
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.
@@ -1,5 +1,5 @@
"""
Functions for controlling the bot's presence. Also contains the loop.
Functions for controlling the bot's presence.
"""
import random
@@ -46,3 +46,10 @@ class BotPresence(commands.Cog, name="Bot Presence"):
status += f" | {len(self.bot.guilds)} servers"
await self.bot.change_presence(activity=nextcord.Game(status))
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(BotPresence(bot))
+16 -34
View File
@@ -9,8 +9,8 @@ from sqlalchemy.orm import Session, aliased
# pylint: disable=import-error
from classes.submission import Submission, SubmissionState
from highscores import submission_objects
from utilities.sql_data_classes import Category, CategoryGroup, Guild, Submissions, engine
from agon import submission_objects
from utilities.sql_data_classes import Category, CategoryGroup, Guild, engine
from .verification_view import VerificationView
@@ -30,7 +30,7 @@ def category_select_options(group_id: int) -> "list[nextcord.SelectOption]":
return options
# Items used in submission process ########################
############ Items used in submission process ############
class SubmissionDropdown(nextcord.ui.Select):
@@ -47,14 +47,9 @@ class SubmissionDropdown(nextcord.ui.Select):
async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id)
submission.category_id = self.values[0]
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()
embeds = submission.get_embeds()
await interaction.message.edit(
embed=embed,
embeds=embeds,
)
@@ -105,8 +100,8 @@ class NextButton(nextcord.ui.Button):
else:
state = SubmissionState(state.value + 1)
submission.submission_state = state
embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
embeds = submission.get_embeds()
await interaction.edit(embeds=embeds, view=SubmissionView(self._bot, interaction.user.id))
class SubmitButton(nextcord.ui.Button):
@@ -121,40 +116,27 @@ class SubmitButton(nextcord.ui.Button):
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verification_channel_id = session.scalar(stmt)
channel = interaction.guild.get_channel(verification_channel_id)
active_submission = submission_objects.get_user_submission(interaction.user.id)
submission = submission_objects.get_user_submission(interaction.user.id)
error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins."
assert channel is not None, await channel.send(error_response)
active_submission.submission_state = SubmissionState.VERIFY
submission.submission_state = SubmissionState.VERIFY
embed = active_submission.get_embed()
embeds = submission.get_embeds()
verification_message = await channel.send(
await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(),
embed=embed,
embeds=embeds,
content=f"Submission from <@{interaction.user.id}>.",
)
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)
with Session(engine) as session:
session.add(new_submission)
session.commit()
# Submission Views ########################
############ Submission Views ############
class SubmissionCreateButton(nextcord.ui.View):
@@ -189,11 +171,11 @@ class SubmissionCreateButton(nextcord.ui.View):
# a user should only have 1 submission active at once.
# split username on pipe. Decided to store whole username...
username: str = interaction.user.display_name.split("|")[0]
submission: Submission = Submission(group_id, interaction.guild_id, username)
submission: Submission = Submission(interaction.guild_id, group_id, username)
submission_objects.add_submission(interaction.user.id, submission)
embed = submission.get_embed()
embeds = submission.get_embeds()
submission_message = await channel.send(
f"<@{interaction.user.id}>",
@@ -201,7 +183,7 @@ class SubmissionCreateButton(nextcord.ui.View):
self._bot,
interaction.user.id,
),
embed=embed,
embeds=embeds,
)
# update the message_id once its created
+17 -22
View File
@@ -1,10 +1,6 @@
"""View for verification buttons"""
import nextcord
from sqlalchemy import select
from sqlalchemy.orm import Session
from utilities.sql_data_classes import Submissions, engine
# pylint: disable=import-error
from utilities.utils import submit_score
@@ -21,34 +17,33 @@ class VerificationView(nextcord.ui.View):
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""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()
@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
with Session(engine) as session:
stmt = select(Submissions).where(Submissions.VerificationMessageId == interaction.message.id)
submission = session.scalar(stmt)
"""Button for approving score submission."""
for embed in interaction.message.embeds:
content = embed.to_dict()
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:
await submit_score(
interaction, submission.GroupId, submission.CategoryId, submission.UserName, submission.Score
)
await submit_score(interaction, group_id, category_id, user, score)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
await interaction.message.edit(content=content, view=None)
# TODO send message to let user know submission was accepted. Need access to bot here.
# remove the bot information embed. it should always be the first one.
await interaction.message.edit(content=content, view=None, embed=interaction.message.embeds[0])
except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None)
+29
View File
@@ -0,0 +1,29 @@
version: "3"
services:
sqldb:
restart: unless-stopped
image: mysql:latest
container_name: "Highscores-DataBase"
volumes:
- highscores_data:/var/lib/mysql
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=${SQL_PASS}
- MYSQL_ROOT_USERNAME=${SQL_USER}
bot:
restart: unless-stopped
build: .
container_name: "Highscores-Bot"
depends_on:
- sqldb
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN}
- SQL_DB=${SQL_DB}
- SQL_HOST=${SQL_HOST}
- SQL_PASS=${SQL_PASS}
- SQL_USER=${SQL_USER}
volumes:
highscores_data:
+3 -16
View File
@@ -3,10 +3,8 @@ use highscores;
CREATE TABLE Guilds(
GuildId bigint unsigned not null unique,
GuildName varchar(95) not null,
SubmissionChannel bigint unsigned,
VerificationChannel bigint unsigned,
Active boolean DEFAULT true,
PRIMARY KEY (GuildId)
);
@@ -19,7 +17,7 @@ CREATE TABLE CategoryGroups(
PadLength smallint not null DEFAULT 26,
UsernameLength smallint DEFAULT 12,
CONSTRAINT UniqueGuildCategoryGroup UNIQUE (GuildId, GroupName),
FOREIGN KEY (GuildId) REFERENCES Guilds(GuildId) ON DELETE CASCADE,
FOREIGN KEY (GuildId) REFERENCES Guilds(GuildId),
PRIMARY KEY (GroupId)
);
@@ -32,7 +30,7 @@ CREATE TABLE Categories(
IsTimeRecord boolean not null DEFAULT false,
IsValidated boolean not null DEFAULT true,
CONSTRAINT UniqueCategoryInGroup UNIQUE (GroupId, CategoryName),
FOREIGN KEY (GroupId) REFERENCES CategoryGroups(GroupId) ON DELETE CASCADE,
FOREIGN KEY (GroupId) REFERENCES CategoryGroups(GroupId),
PRIMARY Key (CategoryId)
);
@@ -40,17 +38,6 @@ CREATE TABLE Scores(
UserName varchar(30) not null,
Score float not null, -- Maybe should have time and int scores?? timestamp type?
CategoryId int unsigned not null,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId),
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)
);
+2 -3
View File
@@ -14,8 +14,7 @@ Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
Category = Base.classes.Categories
CategoryGroup = Base.classes.CategoryGroups
Guild = Base.classes.Guilds
CategoryGroup = Base.classes.CategoryGroups
Category = Base.classes.Categories
Score = Base.classes.Scores
Submissions = Base.classes.Submissions
+39 -90
View File
@@ -40,18 +40,18 @@ def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "d
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId)
categories_dict: "dict[str, str]" = {cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)}
categories_dict: dict = {cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)}
# if string empty, return all category names
if not category_begin:
return dict(list(category_begin.items())[:25])
return categories_dict
# else find all categories that start with category_begin
# TODO is it better to have sql handle the starts with or python?
categories: "dict[str, str]" = {
categories: dict = {
cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower()
}
return dict(list(categories.items())[:25])
return categories
def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
@@ -71,18 +71,18 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id)
# dict has to have key values of same type for discord.
group_dict: "dict[str, str]" = {group.GroupName: str(group.GroupId) for group in session.scalars(stmt)}
group_dict: dict = {group.GroupName: str(group.GroupId) for group in session.scalars(stmt)}
# if string empty, return all group names
if not group_begin:
return dict(list(group_dict.items())[:25])
return group_dict
# else find all groups that start with group_begin
groups: "dict[str, str]" = {
groups: dict = {
group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower()
}
return dict(list(groups.items())[:25])
return groups
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]":
@@ -130,12 +130,12 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
# if string empty, return all users
if not user_begin:
return users_list[:25]
return users_list
# else find all users that start with user_begin
users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()]
users = [user for user in users_list if user_begin.lower() in user.lower()]
return users[:25]
return users
def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
@@ -158,12 +158,12 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
# if string empty, return all users
if not user_begin:
return users_list[:25]
return users_list
# else find all users that start with user_begin
users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()]
users = [user for user in users_list if user_begin.lower() in user.lower()]
return users[:25]
return users
def format_highscore_message(session: Session, group: CategoryGroup) -> str:
@@ -231,12 +231,20 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
# add each score to list
for ind in range(max_cat_size): # need to subtract to deal with category name string
if ind in processed_group.index:
username = processed_group["UserName"][ind]
score = processed_group["Score"][ind]
user_length = group.UsernameLength
username = processed_group["UserName"][ind][:user_length].ljust(user_length, " ")
str_list[ind + 1] += format_score_string(
username, score, group.UsernameLength, category.IsTimeRecord, ind + 1, pad_length
)
# get score, pad to correct length
if category.IsTimeRecord:
# MM:SS.ms is 7 characters long
# if there are single digit minutes, add a space for formatting.
score = get_time_str(processed_group["Score"][ind])
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
else:
score = str(int(processed_group["Score"][ind])).rjust(7, " ")
str_list[ind + 1] += f"{ind + 1}: {username} - {score}".ljust(pad_length, " ")
else:
# if not enough entries, pad out
if ind + 1 <= category.Size:
@@ -254,6 +262,7 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
# need to use index to allow for modifying the list
for ind, line in enumerate(str_list):
if len(line) > MAX_LINE_LENGTH:
split_loc = int(MAX_LINE_LENGTH / pad_length) * pad_length
new_line = line[split_loc:]
str_list[ind] = line[:split_loc]
@@ -274,38 +283,6 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
return ret_string + "```"
def format_score_string(
username: str, score: float, username_length: int, is_time_record: bool, rank: int, pad_length: int
) -> str:
"""
Builds the string to show a user's score.
Args:
username (str): username.
score (int): score in int form.
username_length (float): limit to how long username is allowed to be.
is_time_record (bool): if the score represents an number or a time record.
rank (int): the user's rank.
pad_length (int): limit to how long the string can be.
Returns:
str: string to show user's score.
"""
username = username[:username_length].ljust(username_length, " ")
# get score, pad to correct length
if is_time_record:
# MM:SS.ms is 7 characters long
# if there are single digit minutes, add a space for formatting.
score = get_time_str(score)
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
else:
score = str(int(score)).rjust(7, " ")
return f"{rank}: {username} - {score}".ljust(pad_length, " ")
def get_time_str(time_in: float) -> str:
"""
Formats a float value into the desired string format for a timestamp.
@@ -321,44 +298,6 @@ def get_time_str(time_in: float) -> str:
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:
"""
Function takes care of checking if a message exists for a group.
@@ -389,7 +328,7 @@ async def send_highscore_message(session: Session, guild: nextcord.Guild, group:
async def submit_score(
interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: float
interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: str
) -> None:
"""
Adds passed score to group. Then updates group message by calling send_highscore_message.
@@ -404,6 +343,16 @@ async def submit_score(
"""
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)
existing_score = session.scalar(stmt)