Compare commits

..
Author SHA1 Message Date
imcompiling 02e7a6df3a Comment out troublesome code. I believe that this causes users to have trouble submitting multiple in a row over 5 minutes. 2023-08-29 21:01:13 -04:00
imcompiling 7be869b2b6 Update registry location.
build_develop / build (push) Successful in 1m14s
2023-07-29 18:34:56 -04:00
imcompiling 15c4776918 its async. must await.
build_develop / build (push) Failing after 1m29s
2023-07-25 13:38:48 -04:00
imcompiling d3102b57cc Add new birthday action.
build_develop / build (push) Has been cancelled
2023-07-25 11:19:56 -04:00
imcompiling 9bef20de5b Clarify wording in submission process.
build
2023-04-30 21:54:02 -04:00
imcompiling 9231c04c6a Merge branch '61-guild-active-in-database'
build
2023-03-29 20:14:47 -04:00
imcompiling 36ba4d35e0 Update table definition. Add on_guild_remove listener. 2023-03-29 20:13:39 -04:00
imcompiling b80959866a Merge branch '44-limit-autofill-choices'
build
2023-03-29 19:19:20 -04:00
imcompiling de1eba025a Limit results from autofill functions.
Also define some types.
2023-03-29 19:19:04 -04:00
imcompiling 28e575f364 Remove docker-compose from repo.
build
2023-03-29 16:43:11 -04:00
imcompiling d8368b4b92 Merge branch '62-dm-on-join'
build
2023-03-29 16:42:41 -04:00
imcompiling cc10bb518c Add comment. 2023-03-29 16:42:25 -04:00
imcompiling 90131f6395 Add message to myself on join. Edit adding guild just in case they already exist in database. 2023-03-28 15:06:48 -04:00
imcompiling 1814867c09 Merge branch '58-guild-name-in-database'
build
2023-03-28 13:55:40 -04:00
imcompiling 390bf8a489 rename variable. 2023-03-28 13:55:18 -04:00
imcompiling 74aa78c748 Add loop for updating guild data. 2023-03-28 13:44:03 -04:00
imcompiling 379f280c11 Refactor presence to loops. 2023-03-28 13:02:07 -04:00
imcompiling 7d1e69c007 Don't need this import. 2023-03-28 12:56:50 -04:00
imcompiling b905ce7216 Change tag to v. 2023-03-28 12:51:39 -04:00
imcompiling 4bb7db3f71 Set push for specific tag only, set GIT_TAG env var. 2023-03-28 12:51:39 -04:00
imcompiling b3f48d4603 Correct on push. 2023-03-28 12:51:39 -04:00
imcompiling 90763c73a7 Add release workflow. 2023-03-28 12:51:39 -04:00
imcompiling 635419dc7c Change to operate on the main branch. No need for dev branch. 2023-03-28 12:51:39 -04:00
imcompiling 2356d5edbf Change workflow back to develop branch. 2023-03-28 12:51:39 -04:00
imcompiling 9eed0a1f4e Don't use action due to using local repo. 2023-03-28 12:51:39 -04:00
imcompiling a9a6ab4520 correct github address. 2023-03-28 12:51:39 -04:00
imcompiling e08a764591 Install docker onto image. 2023-03-28 12:51:39 -04:00
imcompiling 8b43f766b3 try https url 2023-03-28 12:51:39 -04:00
imcompiling cb9cad9783 maybe need buildx to set up docker 2023-03-28 12:51:39 -04:00
imcompiling 216becff6e try with docker commands instead of action. 2023-03-28 12:51:39 -04:00
imcompiling 836673198a try localhost for repo. 2023-03-28 12:51:39 -04:00
imcompiling e34739a0b1 Need buildx, don't understand why. 2023-03-28 12:51:39 -04:00
imcompiling 7c592e6acc correct http url, try again without buildx. 2023-03-28 12:51:39 -04:00
imcompiling 0f8637be23 try adding buildx to fix missing token. 2023-03-28 12:51:39 -04:00
imcompiling 3b597aeac7 delete unneeded files. 2023-03-28 12:51:39 -04:00
imcompiling c43b2297a4 Change runs-on, use action to build docker image. 2023-03-28 12:51:39 -04:00
imcompiling 1b337a291d add demo workflow 2023-03-28 12:51:39 -04:00
imcompiling 16b662875f Add build_develop workflow. 2023-03-28 12:51:39 -04:00
imcompiling 307f61934e Merge branch '59-gitea-action'
build
2023-03-23 16:52:15 -04:00
imcompiling 00265f2f58 Change tag to v. 2023-03-23 16:50:39 -04:00
imcompiling 93d558edbc Merge commit '0f2b1aa2e655a66c777a71c3917cd61147c9d754' into 59-gitea-action
build
2023-03-23 16:45:52 -04:00
imcompiling 0f2b1aa2e6 Set push for specific tag only, set GIT_TAG env var.
build
2023-03-23 16:44:08 -04:00
imcompiling 3f6bac99ab Merge branch '59-gitea-action'
build
2023-03-23 09:30:16 -04:00
imcompiling 46e2955642 Correct on push.
build
2023-03-23 09:30:00 -04:00
imcompiling ac9bf6b35f Merge branch 'develop' 2023-03-23 09:19:47 -04:00
imcompiling 869b2ccb82 Merge branch '59-gitea-action' into develop 2023-03-23 09:19:29 -04:00
imcompiling 3ddfdfe9b0 Add release workflow. 2023-03-23 09:16:01 -04:00
imcompiling 3860e74579 Change to operate on the main branch. No need for dev branch. 2023-03-23 09:09:41 -04:00
imcompiling 3b49a6ec2a Change workflow back to develop branch. 2023-03-22 23:25:26 -04:00
imcompiling 2f89588358 Don't use action due to using local repo.
build
2023-03-22 23:14:15 -04:00
imcompiling 0513df944f correct github address.
build
2023-03-22 22:31:42 -04:00
imcompiling ddf319b9d5 Install docker onto image.
build
2023-03-22 22:27:05 -04:00
imcompiling 13f5bc0aab try https url
build
2023-03-22 21:47:28 -04:00
imcompiling e6f3fcb604 maybe need buildx to set up docker
build
2023-03-22 19:35:11 -04:00
imcompiling eaa342d479 try with docker commands instead of action.
build
2023-03-22 19:34:27 -04:00
imcompiling ac11f3e462 try localhost for repo.
build
2023-03-22 19:22:18 -04:00
imcompiling 106b394c44 Need buildx, don't understand why.
build
2023-03-22 19:16:54 -04:00
imcompiling 6c1505685b correct http url, try again without buildx.
build
2023-03-22 19:15:15 -04:00
imcompiling 2e681be690 try adding buildx to fix missing token.
build
2023-03-22 19:12:03 -04:00
imcompiling 5448f2a426 delete unneeded files. 2023-03-22 19:11:22 -04:00
imcompiling 7647c355af Change runs-on, use action to build docker image.
Explore-GitHub-Actions
build
2023-03-22 18:56:20 -04:00
imcompiling a95fca40f9 add demo workflow
Explore-GitHub-Actions
build_develop
2023-03-21 22:06:46 -04:00
imcompiling 148ba253cf Add build_develop workflow.
build_develop
2023-03-21 21:53:04 -04:00
imcompiling 640d481018 Add guild name to database on join. 2023-03-21 21:41:51 -04:00
imcompiling 4c18513661 Merge branch '35-show-leaderboard-command' into develop 2023-03-11 15:12:59 -05:00
imcompiling 37b4d2d62e correct usage of format_score_string. Make format of view message better. 2023-03-11 15:12:36 -05:00
imcompiling 9c762fb346 Add on delete cascade to sql definition. 2023-03-10 17:56:49 -05:00
imcompiling 2c9e9e246a Create view leaderboard command. 2023-03-10 17:37:59 -05:00
imcompiling ab721c1625 create format_score_string function. 2023-03-10 16:27:40 -05:00
imcompiling 2864f342cc need to push tags separately. 2023-03-10 16:01:54 -05:00
imcompiling 92511db446 Merge branch '35-show-leaderboard-command' of https://gitea.cheezers.net/imcompiling/agon into 35-show-leaderboard-command 2023-03-10 12:18:25 -05:00
imcompiling 44c2720f80 Add new view file. 2023-03-10 12:17:58 -05:00
imcompiling 35cebdebce correct develop builder. 2023-03-10 11:57:28 -05:00
imcompiling e4437cc654 Correct the build scripts. 2023-03-10 11:56:45 -05:00
imcompiling fe2c400a2a Add build script. 2023-03-09 16:22:31 -05:00
imcompiling 39875585ef Change sqlalchemy version requirement. 2023-02-12 21:46:25 -05:00
imcompiling 7796ead644 Revert "Make command for admin only."
This reverts commit 8595a12f7f.
2023-02-12 21:37:12 -05:00
imcompiling 531d2f3a0e Merge commit '63e77958fb09c6849d48263994b69fcd92f46277' 2023-02-12 21:30:22 -05:00
imcompiling 8595a12f7f Make command for admin only. 2023-02-12 21:27:54 -05:00
imcompiling f76171ac84 Create simple help command for setting up. 2023-02-12 20:52:28 -05:00
imcompiling 1605c8ad50 Add new view file. 2023-02-12 13:33:05 -05:00
alwayscompiling 63e77958fb Do need to expose port for test bot. 2022-12-10 16:54:49 -05:00
alwayscompiling 0bae0ea5ff Remove custom mysqldb image, just use latest public. Move bot docker to base directory. 2022-12-10 16:12:56 -05:00
alwayscompiling c67e9ae617 Use pymysql for sql connection. 2022-12-10 16:12:21 -05:00
alwayscompiling 7e23ac14ff Create dockerfile for bot. Add bot to compose. 2022-12-10 12:46:12 -05:00
alwayscompiling a0a7e37921 Remove guild join stuff again. 2022-12-09 16:43:56 -05:00
alwayscompiling c01c888503 Merge branch '21-allow-users-to-modify-tablescategories' into develop 2022-12-08 23:17:21 -05:00
alwayscompiling 58dbe547bb Add submit button on group create. 2022-12-08 23:16:56 -05:00
alwayscompiling 3cf4fc6b33 Merge branch '21-allow-users-to-modify-tablescategories' into develop 2022-12-08 23:09:07 -05:00
alwayscompiling 411f8ab275 Add a deletion confirmation. 2022-12-08 22:59:00 -05:00
alwayscompiling 8952c4a352 Remove onjoin message. 2022-12-08 13:44:34 -05:00
alwayscompiling 93248797a0 Fill out rest of group and category command. Fill out doc strings. 2022-12-08 13:41:15 -05:00
alwayscompiling dace72f5cc Correct empty message id handling. 2022-12-08 13:28:13 -05:00
alwayscompiling f468a028d0 Changed send_highscore_message to take guild rather than channel. 2022-12-08 13:02:39 -05:00
alwayscompiling a9b889234a Start filling out create category. 2022-12-07 22:40:47 -05:00
alwayscompiling 88cec73644 Create command skeletons. Update set to conform to min and max values decided in create. 2022-12-07 22:24:38 -05:00
alwayscompiling 91e57c6555 Standardize autocomplete function names. 2022-12-07 21:52:36 -05:00
alwayscompiling 6c5f40f788 Refractor command files into single command files. 2022-12-07 21:46:04 -05:00
alwayscompiling a9920db381 Don't auto add runescape groups on join. 2022-12-07 20:54:49 -05:00
alwayscompiling d04b8c5e32 Squashed commit of the following:
commit 3627419e12
Merge: 2db744d 3446ce9
Author: alwayscompiling <[email protected]>
Date:   Tue Dec 6 22:36:51 2022 -0500

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

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

    Change session usage to on use basis.

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

    Add restart policy.

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

    Renamed docker compose file.
2022-12-06 23:02:01 -05:00
alwayscompiling 3627419e12 Merge branch '45-sql-connection-timing-out' into develop 2022-12-06 22:36:51 -05:00
alwayscompiling 3446ce9ca2 Change session usage to on use basis. 2022-12-06 22:28:53 -05:00
alwayscompiling 582b88e248 Create set command over Settings Management. 2022-12-04 22:42:55 -05:00
alwayscompiling 2db744d170 Add restart policy. 2022-12-04 21:44:54 -05:00
alwayscompiling fa33be1b4a Renamed docker compose file. 2022-12-04 21:25:57 -05:00
29 changed files with 1529 additions and 779 deletions
+26
View File
@@ -0,0 +1,26 @@
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: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/agon:develop .
- name: Push Docker Image
run: docker push gitea.cheezers.org/imcompiling/agon:develop
+30
View File
@@ -0,0 +1,30 @@
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: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Set env
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Build Docker Images
run: docker build -t gitea.cheezers.org/imcompiling/agon:latest -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}} .
- name: Push Latest Image
run: docker push gitea.cheezers.org/imcompiling/agon:latest
- name: Push Tagged Image
run: docker push gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}}
+21
View File
@@ -0,0 +1,21 @@
FROM python:3.8.10-slim
# install dependencies for mysqlclient
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
RUN pip install --upgrade pip
WORKDIR /home
COPY classes classes/
COPY cogs cogs/
COPY utilities utilities/
COPY highscores.py highscores.py
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
# remove build deps that aren't needed to run
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "./highscores.py"]
+16 -12
View File
@@ -6,9 +6,10 @@ from enum import Enum
from nextcord import Embed from nextcord import Embed
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error # pylint: disable=import-error
from utilities.sql_alchemy_connection import Category, CategoryGroup, session from utilities.sql_data_classes import Category, CategoryGroup, engine
class SubmissionState(Enum): class SubmissionState(Enum):
@@ -182,11 +183,13 @@ class Submission:
if state is SubmissionState.CATEGORY: if state is SubmissionState.CATEGORY:
description = "Step 1: Choose your category!" description = "Step 1: Choose your category!"
elif state is SubmissionState.SCORE: elif state is SubmissionState.SCORE:
description = "Step 2: Input your score!" description = "Step 2: Send a message to input your score!"
elif state is SubmissionState.PROOF: elif state is SubmissionState.PROOF:
description = "Step 3: Submit your proof!" description = "Step 3: Send a message with a picture attached to submit your proof!"
elif state is SubmissionState.NAME: elif state is SubmissionState.NAME:
description = "Step 4 (Optional): Change name on submission! (proceed to next if skipping)" description = (
"Step 4 (Optional): Send a message to change name on submission! (proceed to next if skipping)"
)
elif state is SubmissionState.SUBMIT: elif state is SubmissionState.SUBMIT:
description = "Step 5: Submit if everything looks correct!" description = "Step 5: Submit if everything looks correct!"
elif state is SubmissionState.VERIFY: elif state is SubmissionState.VERIFY:
@@ -199,14 +202,15 @@ class Submission:
.where(CategoryGroup.GuildId == self.guild_id) .where(CategoryGroup.GuildId == self.guild_id)
.where(CategoryGroup.GroupId == self.group_id) .where(CategoryGroup.GroupId == self.group_id)
) )
embed.add_field(name="Group", value=session.scalar(stmt), inline=False) with Session(engine) as session:
if self.category_id: embed.add_field(name="Group", value=session.scalar(stmt), inline=False)
stmt = ( if self.category_id:
select(Category.CategoryName) stmt = (
.where(Category.GroupId == self.group_id) select(Category.CategoryName)
.where(Category.CategoryId == self.category_id) .where(Category.GroupId == self.group_id)
) .where(Category.CategoryId == self.category_id)
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=self.score, inline=False)
+14 -7
View File
@@ -4,10 +4,13 @@ Cog loader. Adds all command cogs to the bot.
from nextcord.ext import commands from nextcord.ext import commands
# Command classes from .create import CreateCog
from .data_management import DataManagementCog from .delete import DeleteCog
from .message_management import MessageManagementCog from .help import HelpCog
from .settings_management import SettingsManagementCog 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. # This function will be called when this extension is loaded.
@@ -16,6 +19,10 @@ def setup(bot: commands.Bot):
""" """
Adds all command classes to bot. Adds all command classes to bot.
""" """
bot.add_cog(DataManagementCog(bot)) bot.add_cog(CreateCog(bot))
bot.add_cog(MessageManagementCog(bot)) bot.add_cog(DeleteCog())
bot.add_cog(SettingsManagementCog()) bot.add_cog(HelpCog())
bot.add_cog(RefreshCog(bot))
bot.add_cog(SetCog())
bot.add_cog(UpdateCog(bot))
bot.add_cog(ViewCog())
+172
View File
@@ -0,0 +1,172 @@
"""
Commands for creating items in bot.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from sqlalchemy import exc, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from cogs.submission.submission_views import SubmissionCreateButton
from utilities.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
class CreateCog(commands.Cog, name="Create Cog"):
"""
Class holds commands for creating things.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for CreateCog class.
"""
self._bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def create(self, interaction: nextcord.Interaction):
"""
Parent slash command for creating things. Never called.
"""
# need all these arguments because discord command.
# pylint: disable=too-many-arguments
@create.subcommand(description="Create a new Category. Must not exist already in Group.")
async def category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True),
category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True),
is_ascending: bool = nextcord.SlashOption(description="If higher value is better score.", required=True),
is_time_record: bool = nextcord.SlashOption(
description="If the score should be shown in time format. Cannot be changed.", required=True
),
is_validated: bool = nextcord.SlashOption(
description="If scores need to be validated before posted.", required=True
),
size: int = nextcord.SlashOption(
description="How many scores to post in Category. Default is 3.", required=False, default=3, min_value=1
),
) -> None:
"""
Creates a new category to be placed into group_id group. Fails if the category name already exists within the
group. Then calls send_highscore_message().
Args:
interaction (nextcord.Interaction): Interaction for the command.
group_id (str): Id of group to add category to. Is SlashOption.
category_name (str): Name of new category. Is SlashOption.
is_ascending (bool): If the scores are sorted ascending or not. Is SlashOption.
is_time_record (bool): If the scores are a time record or not. Is SlashOption.
is_validated (bool): If the scores should be validated or not. Is SlashOption.
size (int): How many sores to show in the category message. Is SlashOption.
"""
new_category = Category(
CategoryName=category_name,
GroupId=int(group_id),
Size=size,
IsAscending=is_ascending,
IsTimeRecord=is_time_record,
IsValidated=is_validated,
)
# Open connection to engine and add new category.
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
try:
session.add(new_category)
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'"{category_name}" has been added to "{group.GroupName}"!', ephemeral=True
)
except exc.IntegrityError:
# Category name already exists in group
session.rollback()
await interaction.response.send_message(
f'"{category_name}" already exists in "{group.GroupName}". Please rename.', ephemeral=True
)
# need all these arguments because discord command.
# pylint: disable=too-many-arguments
@create.subcommand(description="Create a new Category Group. Must not exist already for Guild.")
async def group(
self,
interaction: nextcord.Interaction,
group_name: str = nextcord.SlashOption(description="Name of new Group.", required=True),
pad_length: int = nextcord.SlashOption(
description="Number of characters wide a category is printed. Default is 26.",
required=False,
default=26,
min_value=7,
max_value=MAX_LINE_LENGTH,
),
username_length: int = nextcord.SlashOption(
description="Number of characters of a username to display. Default is 12.",
required=False,
default=12,
min_value=1,
max_value=30,
),
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to post Group in. If left empty, defaults to current channel.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Creates a new category group to be placed into the database with the guild's id. Fails if the category group
name already exists within the guild. Then calls send_highscore_message().
Args:
interaction (nextcord.Interaction): Interaction for the command.
group_name (str): Name of new Group. Is SlashOption.
pad_length (int): Number of characters wide a category is printed. Is SlashOption.
username_length (int): Number of characters of a username to display. Is SlashOption.
channel (Optional[nextcord.abc.GuildChannel]): Channel to post group in. If None, defaults to current
channel. Is SlashOption.
"""
# if no channel provided, use current channel.
if channel is None:
channel = interaction.channel
new_group = CategoryGroup(
GroupName=group_name,
ChannelId=channel.id,
GuildId=interaction.guild_id,
PadLength=pad_length,
UsernameLength=username_length,
)
with Session(engine) as session:
try:
session.add(new_group)
session.commit()
message: nextcord.Message = await send_highscore_message(session, interaction.guild, new_group)
await message.edit(view=SubmissionCreateButton(self._bot))
await interaction.response.send_message(f'"{group_name}" has been created!', ephemeral=True)
except exc.IntegrityError:
# Category Group already exists in guild
session.rollback()
await interaction.response.send_message(
f'"{group_name}" already exists in "{interaction.guild.name}". Please rename.', ephemeral=True
)
@category.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
-241
View File
@@ -1,241 +0,0 @@
"""
Commands for managing the tables/categories/scores for a Guild.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from sqlalchemy import distinct, select
# pylint: disable=import-error
from utilities.sql_alchemy_connection import Category, CategoryGroup, Guild, Score, session
from utilities.utils import (
autofill_categories,
autofill_groups,
autofill_users_in_group_category,
autofill_users_in_guild,
send_highscore_message,
)
from .views.update_username_verify import UpdateUsernameVerifyView
class DataManagementCog(commands.Cog, name="Table Management"):
"""
Class holds commands for managing the data storage of the highscores.
"""
def __init__(self, bot: commands.Bot):
"""
Default constructor for DataManagement class. Sets internal bot variable.
"""
self._bot: commands.Bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""
Called when cog is loaded. Adds views required for DataManagement commands.
"""
self._bot.add_view(UpdateUsernameVerifyView())
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def delete(self, interaction: nextcord.Interaction):
"""
Parent slash command for deleting things. Never called.
"""
@delete.subcommand()
async def user(self, interaction: nextcord.Interaction):
"""
Subcommand parent for deleting user information. Never called.
"""
@user.subcommand(
description="Removes all of a user's scores. Warning: data is destroyed, use with case.",
)
async def from_all(
self,
interaction: nextcord.Interaction,
user_name: str = nextcord.SlashOption(
description="User to remove scores for. Note: This is not always a user's discord name.", required=True
),
):
"""
Searches all of guild's scores data for any score from the user. Removes. Updates table
message is user was removed from that table.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): User to remove from highscores
"""
stmt = select(distinct(Score.CategoryId)).where(Score.UserName == user_name)
category_ids: "list[int]" = list(session.scalars(stmt))
stmt = select(Score).where(Score.UserName == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
group_ids = select(Category.GroupId).where(Category.CategoryId.in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids))
for group in session.scalars(stmt):
await send_highscore_message(interaction.guild.get_channel(group.ChannelId), group)
await interaction.send(f'User "{user_name}" was removed from all tables.', ephemeral=True)
@user.subcommand(description="Deletes a user's scores from a category.")
async def from_category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to delete user from.", required=True
),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
):
"""
Removes a user from a category. Uses autofill functions to fill out group_id, category_id, and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to remove user from. Is SlashOption.
category_id (str): String representation of Category id to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
stmt = select(Category.CategoryName).where(Category.CategoryId == int(category_id))
category_name = session.scalar(stmt)
stmt = select(Score).where(Score.CategoryId == int(category_id)).where(Score.UserName == user_name)
# there should only be one because category id and username are a composite primary key in score
session.delete(session.scalar(stmt))
session.commit()
await send_highscore_message(interaction.guild.get_channel(group.ChannelId), group)
await interaction.response.send_message(f'User "{user_name}" removed from "{category_name}".', ephemeral=True)
@user.subcommand(description="Deletes a user's scores from entire group.")
async def from_group(
self,
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),
):
"""
Removes user from a goup. Uses autofill functions to fill out group_id and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
category_ids = select(Category.CategoryId).where(Category.GroupId == int(group_id))
stmt = select(Score).where(Score.CategoryId.in_(category_ids)).where(Score.UserName == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
await send_highscore_message(interaction.guild.get_channel(group.ChannelId), group)
await interaction.response.send_message(
f'User "{user_name}" removed from all categories in {group.GroupName}.', ephemeral=True
)
@from_category.on_autocomplete("group_id")
@from_group.on_autocomplete("group_id")
async def autocomplete_group(self, interaction: nextcord.Interaction, group_id: str):
"""
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))
@from_category.on_autocomplete("category_id")
async def autocomplete_category(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)
)
@from_category.on_autocomplete("user_name")
@from_group.on_autocomplete("user_name")
async def autocomplete_user(
self, interaction: nextcord.Interaction, user_name: str, group_id: str, category_id: Optional[str] = None
):
"""
Autocomplete functions for user names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): String to be autocompleted with User name.
group_id (str): String representation of Group id to find category from.
category_id (Optional[str]): String representation of Category id to find User from, can be None.
"""
await interaction.response.send_autocomplete(
autofill_users_in_group_category(
interaction.guild_id, int(group_id), category_id if category_id else 0, user_name
)
)
@nextcord.slash_command(description="Request a username update. Admins must approve.")
async def update_username(
self,
interaction: nextcord.Interaction,
old_username: str = nextcord.SlashOption(description="Username currently stored in highscores.", required=True),
new_username: str = nextcord.SlashOption(description="Username to update to.", required=True),
):
"""
Creates a request to send to admins for changing a username. Uses
UpdateUsernameVerifyView to do the work.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
old_username (str): Username to replace.
new_username (str): Username to replace old with.
"""
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verify_channel = interaction.guild.get_channel(session.scalar(stmt))
embed: nextcord.Embed = nextcord.Embed(title="Username change request.")
embed.add_field(name="Old Username", value=old_username)
embed.add_field(name="New Username", value=new_username)
await verify_channel.send(
content=f"Username change request from <@{interaction.user.id}>.",
embed=embed,
view=UpdateUsernameVerifyView(),
)
await interaction.send("Request sent to admins for verification.", ephemeral=True)
@update_username.on_autocomplete("old_username")
@from_all.on_autocomplete("user_name")
async def username_autofill(self, interaction: nextcord.Interaction, username: str):
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args:
interaction (nextcord.Interaction): Interaction with command.
old_username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+297
View File
@@ -0,0 +1,297 @@
"""
Commands for deleting groups/categories/scores in database and messages in discord..
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import (
autofill_categories,
autofill_groups,
autofill_users_in_group_category,
autofill_users_in_guild,
send_highscore_message,
)
from .views.delete_confirm import DeleteConfirmView, create_delete_id
class DeleteCog(commands.Cog, name="Table Management"):
"""
Class holds commands for deleting.
"""
def __init__(self) -> None:
"""
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.
"""
@delete.subcommand()
async def all(self, interaction: nextcord.Interaction):
"""
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(
self,
interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to clear of messages.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Deletes all group messages from channel. Does not change data.
Does not delete highscores channel id. Uses current channel if not supplied a channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (Optional[nextcord.abc.GuildChannel]): Channel to clear of messages.
Is a SlashOption. Limited to text channels. Defaults to None.
"""
if not channel:
channel = interaction.channel
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.ChannelId == channel.id)
)
# TODO test when nothing in channel
groups = session.scalars(stmt)
await interaction.response.defer(ephemeral=True)
for group in groups:
try:
table_message: nextcord.Message = await channel.fetch_message(group.MessageId)
await table_message.delete()
except nextcord.NotFound:
# silently ignore that messages are not found. They got deleted already.
pass
await interaction.send("Table messages deleted.", ephemeral=True)
@all.subcommand(
description="Removes all of a user's scores. Warning: data is destroyed, use with case.",
)
async def users(
self,
interaction: nextcord.Interaction,
user_name: str = nextcord.SlashOption(
description="User to remove scores for. Note: This is not always a user's discord name.", required=True
),
):
"""
Searches all of guild's scores data for any score from the user. Removes. Updates table
message is user was removed from that table.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): User to remove from highscores
"""
with Session(engine) as session:
stmt = select(distinct(Score.CategoryId)).where(Score.UserName == user_name)
category_ids: "list[int]" = list(session.scalars(stmt))
stmt = select(Score).where(Score.UserName == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
group_ids = select(Category.GroupId).where(Category.CategoryId.in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids))
for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild, group)
await interaction.send(f'User "{user_name}" was removed from all tables.', ephemeral=True)
@delete.subcommand(description="Delete a category from a group. Deletion is perminant.")
async def category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete from.", required=True),
category_id: str = nextcord.SlashOption(name="category", description="Category to delete.", required=True),
) -> None:
"""
Calls the delete confirmation view for the given group and category.
Args:
interaction (nextcord.Interaction): Interaction of command.
group_id (str): Group to delete from. Is SlashOption.
category_id (str): Category to delete. Is SlashOption.
"""
await interaction.response.send_modal(
DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id)))
)
@delete.subcommand(description="Delete a group. Deletion is perminant.")
async def group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete.", required=True),
) -> None:
"""
Calls the delete confirmation view for the given group.
Args:
interaction (nextcord.Interaction): Interaction of command.
group_id (str): Group to delete from. Is SlashOption.
"""
await interaction.response.send_modal(DeleteConfirmView(custom_id=create_delete_id(int(group_id))))
@delete.subcommand()
async def user(self, interaction: nextcord.Interaction):
"""
Subcommand parent for deleting user information. Never called.
"""
@user.subcommand(description="Deletes a user's scores from a category.")
async def from_category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to delete user from.", required=True
),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
):
"""
Removes a user from a category. Uses autofill functions to fill out group_id, category_id, and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to remove user from. Is SlashOption.
category_id (str): String representation of Category id to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
stmt = select(Category.CategoryName).where(Category.CategoryId == int(category_id))
category_name = session.scalar(stmt)
stmt = select(Score).where(Score.CategoryId == int(category_id)).where(Score.UserName == user_name)
# there should only be one because category id and username are a composite primary key in score
session.delete(session.scalar(stmt))
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from "{category_name}".', ephemeral=True
)
@user.subcommand(description="Deletes a user's scores from entire group.")
async def from_group(
self,
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),
):
"""
Removes user from a goup. Uses autofill functions to fill out group_id and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt)
category_ids = select(Category.CategoryId).where(Category.GroupId == int(group_id))
stmt = select(Score).where(Score.CategoryId.in_(category_ids)).where(Score.UserName == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from all categories in {group.GroupName}.', ephemeral=True
)
@from_category.on_autocomplete("group_id")
@from_group.on_autocomplete("group_id")
@category.on_autocomplete("group_id")
@group.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Autocomplete function for table names.
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))
@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):
"""
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)
)
@from_category.on_autocomplete("user_name")
@from_group.on_autocomplete("user_name")
async def user_autocomplete(
self, interaction: nextcord.Interaction, user_name: str, group_id: str, category_id: Optional[str] = None
):
"""
Autocomplete functions for user names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): String to be autocompleted with User name.
group_id (str): String representation of Group id to find category from.
category_id (Optional[str]): String representation of Category id to find User from, can be None.
"""
await interaction.response.send_autocomplete(
autofill_users_in_group_category(
interaction.guild_id, int(group_id), category_id if category_id else 0, user_name
)
)
@users.on_autocomplete("user_name")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str):
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args:
interaction (nextcord.Interaction): Interaction with command.
old_username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+47
View File
@@ -0,0 +1,47 @@
"""
Commands for helping a user.
"""
import nextcord
from nextcord.ext import commands
class HelpCog(commands.Cog, name="Help commands."):
"""
Class holds commands for getting help.
"""
def __init__(self) -> None:
"""
Default constructor for HelpCog class. Does nothing.
"""
@nextcord.slash_command()
async def help(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
"""
@help.subcommand(description="Describes how to get started with Agon.")
async def setup(self, interaction: nextcord.Interaction):
"""
Sends a message describing how to get started with Agon.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
# I hate this but don't want indents in the string.
# pylint: disable=line-too-long
help_str: str = """Welcome to Agon! Three simple steps are needed to get started.
1.) Set a channel to recieve submission verifications with "/set verify_channel."
This channel is used to allow moderators to check if a submission is real/valid.
2.) Set a channel for submissions to be posted in with "/set submission_channel."
This channel will be where all user submission messages are sent.
3.) Set up your highscore tables! Use "/create group" to first create a group message in the channel you would like and then use "/create category" to create a category in that group message!"""
# pylint: enable=line-too-long
print(help_str)
await interaction.response.send_message(content=help_str)
-152
View File
@@ -1,152 +0,0 @@
"""
Commands for managing/displaying the highscores messages.
"""
from typing import Optional
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
# pylint: disable=import-error
from cogs.submission.submission_views import SubmissionCreateButton
from utilities.sql_alchemy_connection import CategoryGroup, session
from utilities.utils import autofill_groups, send_highscore_message
class MessageManagementCog(commands.Cog, name="Message Management"):
"""
Class holds commands for managing the messages that display highscores.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for MessageManagement class
Args:
bot (commands.Bot): Bot object. Required to register submission button view.
"""
self.bot = bot
@nextcord.slash_command(
description="Clears highscores channel of all table messages. Does not change data.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def delete_channel_messages(
self,
interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
description="Channel to clear of messages.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Deletes all group messages from channel. Does not change data.
Does not delete highscores channel id. Uses current channel if not supplied a channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (Optional[nextcord.abc.GuildChannel]): Channel to clear of messages.
Is a SlashOption. Limited to text channels. Defaults to None.
"""
if not channel:
channel = interaction.channel
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.ChannelId == channel.id)
)
# TODO test when nothing in channel
groups = session.scalars(stmt)
await interaction.response.defer(ephemeral=True)
for group in groups:
try:
table_message: nextcord.Message = await channel.fetch_message(group.MessageId)
await table_message.delete()
except nextcord.NotFound:
# silently ignore that messages are not found. They got deleted already.
pass
await interaction.send("Table messages deleted.", ephemeral=True)
@nextcord.slash_command(
description="Triggers post/update of highscores to registered highscore channel.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def post_highscore_groups(self, interaction: nextcord.Interaction) -> None:
"""
A command which posts the highscore in the registered channel.
Sends warning if no channel has been registered.
Either creates new messages for each table if one doesn't exist
or edits the existing message.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == interaction.guild_id)
# awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True)
for group in session.scalars(stmt):
channel = interaction.guild.get_channel(group.ChannelId)
# Error checking for registered highscore channel
error_response = "Registered Highscores channel does not exist or was never registered. "
error_response += 'Use "/register" to register channels.'
assert channel is not None, await interaction.response(error_response, ephemeral=True)
message: nextcord.Message = await send_highscore_message(channel, group)
await message.edit(view=SubmissionCreateButton(self.bot))
await interaction.send(f"Posted all groups in {interaction.guild.name}'s highscores.", ephemeral=True)
@nextcord.slash_command(
description="Manually refresh a table. Use when changing settings or data manually changed.",
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def refresh_group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Name of Group.", required=True),
) -> None:
"""
Refreshes a table given by user.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): Str representation of Group id to refresh. Is SlashOption.
"""
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
channel = interaction.guild.get_channel(group.ChannelId)
message: nextcord.Message = await send_highscore_message(channel, group)
await message.edit(view=SubmissionCreateButton(self.bot))
await interaction.response.send_message(f"Updated table {group.GroupName}!", ephemeral=True)
@refresh_group.on_autocomplete("group_id")
async def refresh_table_autofill(self, interaction: nextcord.Interaction, group_id: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
+104
View File
@@ -0,0 +1,104 @@
"""
Commands for refreshing highscore messages.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from cogs.submission.submission_views import SubmissionCreateButton
from utilities.sql_data_classes import CategoryGroup, engine
from utilities.utils import autofill_groups, send_highscore_message
class RefreshCog(commands.Cog, name="Refresh Cog"):
"""
Class holds commands for refreshing.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for RefreshCog class
Args:
bot (commands.Bot): Bot object. Required to register submission button view.
"""
self._bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def refresh(self, interaction: nextcord.Interaction):
"""
Parent slash command for refreshing things. Never called.
"""
@refresh.subcommand(description="Manually refresh a table. Use when changing settings or data manually changed.")
async def group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Name of Group.", required=True),
) -> None:
"""
Refreshes a table given by user.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): Str representation of Group id to refresh. Is SlashOption.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
message: nextcord.Message = await send_highscore_message(session, interaction.guild, group)
await message.edit(view=SubmissionCreateButton(self._bot))
await interaction.response.send_message(f"Updated table {group.GroupName}!", ephemeral=True)
@refresh.subcommand()
async def all(self, interaction: nextcord.Interaction):
"""
Subcommand parent for refreshing all things. Never called.
"""
@all.subcommand(description="Triggers post/update of highscores to registered highscore channels.")
async def groups(self, interaction: nextcord.Interaction) -> None:
"""
A command which posts the highscore in the registered channel.
Sends warning if no channel has been registered.
Either creates new messages for each table if one doesn't exist
or edits the existing message.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == interaction.guild_id)
# awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True)
for group in session.scalars(stmt):
message: nextcord.Message = await send_highscore_message(session, interaction.guild, group)
await message.edit(view=SubmissionCreateButton(self._bot))
await interaction.send(f"Posted all groups in {interaction.guild.name}'s highscores.", ephemeral=True)
@group.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
"""
Collects list of table names and compares to input table name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@@ -1,5 +1,5 @@
""" """
Commands for managing the Guild's settings. Set command heirarchy.
""" """
from typing import Optional from typing import Optional
@@ -7,29 +7,30 @@ from typing import Optional
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
# pylint: disable=import-error # pylint: disable=import-error
from utilities.sql_alchemy_connection import CategoryGroup, Guild, session from utilities.sql_data_classes import CategoryGroup, Guild, engine
from utilities.utils import autofill_groups from utilities.utils import MAX_LINE_LENGTH, autofill_groups
class SettingsManagementCog(commands.Cog, name="Settings Management"): class SetCog(commands.Cog, name="Set command."):
""" """
Class holds commands for managing Guild's settings. Class holds commands for setting variables in guild's database.
""" """
def __init__(self) -> None: def __init__(self) -> None:
""" """
Default constructor for SettingsManagement class. Does nothing. Default constructor for SetCog class. Does nothing.
""" """
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True)) @nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def register(self, interaction: nextcord.Interaction): async def set(self, interaction: nextcord.Interaction):
""" """
Parent slash command, never called. Parent slash command for setting settings. Never called.
""" """
@register.subcommand(description="Registers the channel for a Category Group's messages.") @set.subcommand(description="Registers the channel for a Category Group's messages.")
async def group_output_channel( async def group_output_channel(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
@@ -47,26 +48,67 @@ class SettingsManagementCog(commands.Cog, name="Settings Management"):
Command function to register a CategoryGroup's output channel. Command function to register a CategoryGroup's output channel.
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 register channel to. Is SlashOption. group_id (str): String representation of Group id to register channel to. Is SlashOption.
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: if not channel:
channel = interaction.channel channel = interaction.channel
channel_id = channel.id channel_id = channel.id
stmt = ( with Session(engine) as session:
select(CategoryGroup) stmt = (
.where(CategoryGroup.GuildId == interaction.guild_id) select(CategoryGroup)
.where(CategoryGroup.GroupId == int(group_id)) .where(CategoryGroup.GuildId == interaction.guild_id)
) .where(CategoryGroup.GroupId == int(group_id))
group = session.scalar(stmt) )
group.ChannelId = channel_id group = session.scalar(stmt)
session.commit() group.ChannelId = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True) await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@register.subcommand(description="Registers the channel for submission messages.") @set.subcommand(
async def submission( description="Set character length for a category. Can cut off username/score. Affects all categories."
)
async def pad_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Category Group to assign pad length to.", required=True
),
length: int = nextcord.SlashOption(
description="Number of characters alloted to a category.",
required=True,
default=26,
min_value=7,
max_value=MAX_LINE_LENGTH,
),
) -> None:
"""
Changes guild's category pad length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
group_id (str): String representation of Group id to set pad length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 26.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.PadLength = length
session.commit()
await interaction.response.send_message(
f"Category pad length set to {length} for {group.GroupName}", ephemeral=True
)
@set.subcommand(description="Registers the channel for submission messages.")
async def submission_channel(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption( channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
@@ -84,17 +126,50 @@ class SettingsManagementCog(commands.Cog, name="Settings Management"):
channel Optional[nextcord.abc.GuildChannel], optional): Channel to register channel Optional[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: if not channel:
channel = interaction.channel channel = interaction.channel
channel_id = channel.id
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id) with Session(engine) as session:
guild = session.scalar(stmt) channel_id = channel.id
guild.SubmissionChannel = channel_id stmt = select(Guild).where(Guild.GuildId == interaction.guild_id)
session.commit() guild = session.scalar(stmt)
guild.SubmissionChannel = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True) await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@register.subcommand(description="Registers the channel for verification messages.") @set.subcommand(description="Set username length for a Group of Categories.")
async def verify( async def username_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to change username length of.", required=True
),
length: int = nextcord.SlashOption(
description="Number of characters.", required=True, default=12, min_value=1, max_value=30
),
) -> None:
"""
Changes guild's username length limit.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
length (int): Number of characters. Is SlashOption. Defaults to 12.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.UsernameLength = length
session.commit()
await interaction.response.send_message(f"Username length set to {length}.", ephemeral=True)
@set.subcommand(description="Registers the channel for verification messages.")
async def verify_channel(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption( channel: Optional[nextcord.abc.GuildChannel] = nextcord.SlashOption(
@@ -112,84 +187,21 @@ class SettingsManagementCog(commands.Cog, name="Settings Management"):
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: if not channel:
channel = interaction.channel channel = interaction.channel
channel_id = channel.id channel_id = channel.id
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id) with Session(engine) as session:
guild = session.scalar(stmt) stmt = select(Guild).where(Guild.GuildId == interaction.guild_id)
guild.VerificationChannel = channel_id guild = session.scalar(stmt)
session.commit() guild.VerificationChannel = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True) await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def set(self, interaction: nextcord.Interaction):
"""
Parent slash command for setting settings. Never called.
"""
@set.subcommand(
description="Set character length for a category. Can cut off username/score. Affects all categories."
)
async def category_pad_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Category Group to assign pad length to.", required=True
),
length: int = nextcord.SlashOption(description="Number of characters.", required=True, default=26),
) -> None:
"""
Changes guild's category pad length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
group_id (str): String representation of Group id to set pad length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 26.
"""
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.PadLength = length
session.commit()
await interaction.response.send_message(
f"Category pad length set to {length} for {group.GroupName}", ephemeral=True
)
@set.subcommand(description="Set username length for a Group of Categories.")
async def username_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to change username length of.", required=True
),
length: int = nextcord.SlashOption(description="Number of characters.", required=True, default=12),
) -> None:
"""
Changes guild's username length limit.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
length (int): Number of characters. Is SlashOption. Defaults to 12.
"""
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.UsernameLength = length
session.commit()
await interaction.response.send_message(f"Username length set to {length}.", ephemeral=True)
@group_output_channel.on_autocomplete("group_id") @group_output_channel.on_autocomplete("group_id")
@category_pad_length.on_autocomplete("group_id") @pad_length.on_autocomplete("group_id")
@username_length.on_autocomplete("group_id") @username_length.on_autocomplete("group_id")
async def group_id_autocomplete(self, interaction: nextcord.Interaction, group_id: str): async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str):
""" """
Sends results from autofill_groups back to group_output_channel for autocompletion of group_id. Sends results from autofill_groups back to group_output_channel for autocompletion of group_id.
+84
View File
@@ -0,0 +1,84 @@
"""
Commands for updating information/variables held in database.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Guild, engine
from utilities.utils import autofill_users_in_guild
from .views.update_username_verify import UpdateUsernameVerifyView
class UpdateCog(commands.Cog, name="Update commands."):
"""
Class holds commands and views needed for updating things in bot.
"""
def __init__(self, bot: commands.Bot):
"""
Default constructor for Update class. Sets internal bot variable.
Args:
bot (commands.Bot): Bot object. Required to register update username button view.
"""
self._bot: commands.Bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""
Called when cog is loaded. Adds views required for DataManagement commands.
"""
self._bot.add_view(UpdateUsernameVerifyView())
@nextcord.slash_command()
async def update(self, interaction: nextcord.Interaction):
"""
Parent slash command for updating things. Never called.
"""
@update.subcommand(description="Request a username update. Admins must approve.")
async def username(
self,
interaction: nextcord.Interaction,
old_username: str = nextcord.SlashOption(description="Username currently stored in highscores.", required=True),
new_username: str = nextcord.SlashOption(description="Username to update to.", required=True),
):
"""
Creates a request to send to admins for changing a username. Uses
UpdateUsernameVerifyView to do the work.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
old_username (str): Username to replace.
new_username (str): Username to replace old with.
"""
with Session(engine) as session:
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verify_channel = interaction.guild.get_channel(session.scalar(stmt))
embed: nextcord.Embed = nextcord.Embed(title="Username change request.")
embed.add_field(name="Old Username", value=old_username)
embed.add_field(name="New Username", value=new_username)
await verify_channel.send(
content=f"Username change request from <@{interaction.user.id}>.",
embed=embed,
view=UpdateUsernameVerifyView(),
)
await interaction.send("Request sent to admins for verification.", ephemeral=True)
@username.on_autocomplete("old_username")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str):
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args:
interaction (nextcord.Interaction): Interaction with command.
old_username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+112
View File
@@ -0,0 +1,112 @@
"""
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)
)
+97
View File
@@ -0,0 +1,97 @@
"""
Contains view and helper functions for confirming and processing deletes.
"""
from typing import Optional
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import send_highscore_message
class DeleteConfirmView(nextcord.ui.Modal):
"""
View modal popup for confirming deletions.
"""
def __init__(self, custom_id: str):
super().__init__(title="Warning, deletion is perminant.", custom_id=custom_id)
self.checker = nextcord.ui.TextInput(
label='Type "delete" to confirm deletion.',
required=True,
placeholder="delete",
style=nextcord.TextInputStyle.short,
)
self.add_item(self.checker)
async def callback(self, interaction: nextcord.Interaction):
if self.checker.value.lower() != "delete":
await interaction.response.send_message('Response was not "delete". Canceling delete.')
return
del_vals = parse_delete_id(self.custom_id)
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == del_vals["group_id"])
group = session.scalar(stmt)
if del_vals["category_id"] is None:
# delete entire group
session.delete(group)
# delete the message related to group
channel: nextcord.TextChannel = interaction.guild.get_channel(group.ChannelId)
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.delete()
else:
stmt = (
delete(Category)
.where(Category.GroupId == del_vals["group_id"])
.where(Category.CategoryId == del_vals["category_id"])
)
session.execute(stmt)
# update the message
await send_highscore_message(session, interaction.guild, group)
session.commit()
await interaction.response.send_message("Deletion complete.", ephemeral=True)
def create_delete_id(group_id: int, category_id: Optional[int] = None) -> str:
"""
Creates a delete id string to tell the view what to delete.
Args:
group_id (int): group id to delete
category_id (Optional[int]): category id to delete. Can be None.
Returns:
str: string representation of the delete id.
"""
return f"delete-{group_id}-{category_id}"
def parse_delete_id(delete_id: str) -> dict:
"""
Returns a dict containing the values of a delete id.
Args:
id (str): the delete id
Returns:
dict: dict containing values from a delete id.
"""
vals = {}
group_id = delete_id.split("-")[1]
category_id = delete_id.split("-")[2]
vals["group_id"] = group_id
if category_id == "None":
vals["category_id"] = None
else:
vals["category_id"] = category_id
return vals
+26 -24
View File
@@ -2,9 +2,10 @@
import nextcord import nextcord
from sqlalchemy import distinct, exc, select from sqlalchemy import distinct, exc, select
from sqlalchemy.orm import Session
# pylint: disable=import-error # pylint: disable=import-error
from utilities.sql_alchemy_connection import Category, CategoryGroup, Score, session from utilities.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import send_highscore_message from utilities.utils import send_highscore_message
@@ -43,32 +44,33 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
interaction (nextcord.Interaction): Interaction with the button interaction (nextcord.Interaction): Interaction with the button
""" """
try: with Session(engine) as session:
embed_content = interaction.message.embeds[0].to_dict() try:
old_username: str = embed_content["fields"][0]["value"] embed_content = interaction.message.embeds[0].to_dict()
new_username: str = embed_content["fields"][1]["value"] old_username: str = embed_content["fields"][0]["value"]
new_username: str = embed_content["fields"][1]["value"]
stmt = select(distinct(Score.CategoryId)).where(Score.UserName == old_username) stmt = select(distinct(Score.CategoryId)).where(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(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(Category.GroupId).where(Category.CategoryId.in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids)) stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_ids))
for group in session.scalars(stmt): for group in session.scalars(stmt):
await send_highscore_message(interaction.guild.get_channel(group.ChannelId), group) await send_highscore_message(session, interaction.guild, group)
content = interaction.message.content content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>." content += f"\nApproved by <@{interaction.user.id}>."
await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None) await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None)
except exc.DataError: except exc.DataError:
session.rollback() session.rollback()
content = interaction.message.content content = interaction.message.content
content += "\nError, UserName is too long." content += "\nError, UserName is too long."
await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None) await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None)
await interaction.response.send_message(content="User must resubmit with shorter name.", ephemeral=True) await interaction.response.send_message(content="User must resubmit with shorter name.", ephemeral=True)
+46 -13
View File
@@ -4,10 +4,11 @@ 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.orm import Session
# pylint: disable=import-error # pylint: disable=import-error
from utilities.sql_alchemy_connection import Guild, session from utilities.sql_data_classes import Guild, engine
from utilities.utils import add_preset_configuration
class GuildListener(commands.Cog, name="Guild Listener"): class GuildListener(commands.Cog, name="Guild Listener"):
@@ -18,19 +19,51 @@ 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. Creates skeleton for new guild joined and saves it.""" """
new_guild = Guild(GuildId=guild.id) Listener for Guild Join. Adds guild to database.
session.add(new_guild)
session.commit()
# This add config will be removed eventually. Config adding will have to be controlled by commands.
add_preset_configuration(guild.id, "runescape")
welcome_message: str = "Hello, I keep track of Highscores for you!" Args:
welcome_message += " Currently only supporting RuneScape boss highscores," guild (nextcord.Guild): Guild that was joined.
welcome_message += " but will be more dynamic support in future." """
welcome_message += ' For assistance on getting started, use "?register help".'
await guild.system_channel.send(content=welcome_message) 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.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. # This function will be called when this extension is loaded.
+16
View File
@@ -0,0 +1,16 @@
"""
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))
@@ -1,5 +1,5 @@
""" """
Functions for controlling the bot's presence. Functions for controlling the bot's presence. Also contains the loop.
""" """
import random import random
@@ -46,10 +46,3 @@ class BotPresence(commands.Cog, name="Bot Presence"):
status += f" | {len(self.bot.guilds)} servers" status += f" | {len(self.bot.guilds)} servers"
await self.bot.change_presence(activity=nextcord.Game(status)) 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))
+48
View File
@@ -0,0 +1,48 @@
"""
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()
+75 -55
View File
@@ -3,16 +3,17 @@ Defines cog for on message listener.
""" """
import re import re
from datetime import datetime
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 aliased 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_alchemy_connection import Category, CategoryGroup, Guild, session from utilities.sql_data_classes import Category, Guild, engine
class SubmissionListener(commands.Cog, name="Message Listener"): class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -28,71 +29,74 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
if message.author.id == self.bot.user.id: if message.author.id == self.bot.user.id:
return return
await birthday_action(message)
guild_id = message.guild.id guild_id = message.guild.id
if submission_objects.has_user(message.author.id): if submission_objects.has_user(message.author.id):
submission = submission_objects.get_user_submission(message.author.id) submission = submission_objects.get_user_submission(message.author.id)
message_id = submission.message_id message_id = submission.message_id
stmt = select(Guild.SubmissionChannel).where(Guild.GuildId == guild_id)
submission_channel_id = session.scalar(stmt)
submission_message: nextcord.Message = (
await self.bot.get_guild(guild_id).get_channel(submission_channel_id).fetch_message(message_id)
)
# need check if correct user and correct channel with Session(engine) as session:
if message.channel.id == submission_channel_id: stmt = select(Guild.SubmissionChannel).where(Guild.GuildId == guild_id)
state = submission.submission_state submission_channel_id = session.scalar(stmt)
if state == SubmissionState.SCORE and message.content: submission_message: nextcord.Message = (
await self.bot.get_guild(guild_id).get_channel(submission_channel_id).fetch_message(message_id)
)
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_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:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_id)
# check with regex if score matches correctly # check with regex if score matches correctly
is_time_record = session.scalar(stmt) is_time_record = session.scalar(stmt)
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content) 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) int_regex = re.search("^[0-9]+$", message.content)
if time_regex is None and is_time_record: if time_regex is None and is_time_record:
embeds = submission_message.embeds[0] embeds = submission_message.embeds[0]
embeds.add_field( embeds.add_field(
name="Error", name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.", 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 int_regex is None and not is_time_record: elif state == SubmissionState.PROOF and len(message.attachments) > 0:
embeds = submission_message.embeds[0] await submission_message.edit(
embeds.add_field(name="Error", value="Number record must only be number.") files=[await attch.to_file() for attch in message.attachments],
else: )
score = message.content elif state == SubmissionState.NAME and message.content:
if is_time_record: submission.username = message.content
# 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() embeds = submission.get_embeds()
await submission_message.edit( await submission_message.edit(
embeds=embeds, 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,
)
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.
@@ -100,3 +104,19 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
"""Adds function to bot""" """Adds function to bot"""
bot.add_cog(SubmissionListener(bot)) bot.add_cog(SubmissionListener(bot))
async def birthday_action(message: nextcord.Message) -> None:
"""
Does the birthday thing, currently for chris only.
Args:
message (nextcord.Message): The message to check for birthday thing.
"""
today = datetime.today()
birthday = datetime(today.year, 7, 31)
chris = 642330132958281750
if today.date() == birthday.date():
if message.author.id == chris or chris in message.raw_mentions:
await message.add_reaction("🎂")
+26 -21
View File
@@ -5,12 +5,12 @@ from typing import Optional
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 aliased 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 utilities.sql_alchemy_connection import Category, Guild, CategoryGroup, session
from highscores import submission_objects from highscores import submission_objects
from utilities.sql_data_classes import Category, CategoryGroup, Guild, engine
from .verification_view import VerificationView from .verification_view import VerificationView
@@ -18,12 +18,14 @@ from .verification_view import VerificationView
def category_select_options(group_id: int) -> "list[nextcord.SelectOption]": def category_select_options(group_id: int) -> "list[nextcord.SelectOption]":
"""Returns list of categories for a given boss.""" """Returns list of categories for a given boss."""
options: "list[nextcord.SelectOption]" = [] options: "list[nextcord.SelectOption]" = []
subq = select(CategoryGroup).where(CategoryGroup.GroupId == group_id).subquery()
cat_group_alias = aliased(CategoryGroup, subq) with Session(engine) as session:
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId) subq = select(CategoryGroup).where(CategoryGroup.GroupId == group_id).subquery()
categories_list = session.scalars(stmt) cat_group_alias = aliased(CategoryGroup, subq)
for category in categories_list: stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId)
options.append(nextcord.SelectOption(label=category.CategoryName, value=str(category.CategoryId))) categories_list = session.scalars(stmt)
for category in categories_list:
options.append(nextcord.SelectOption(label=category.CategoryName, value=str(category.CategoryId)))
return options return options
@@ -110,8 +112,9 @@ class SubmitButton(nextcord.ui.Button):
self._bot = bot self._bot = bot
async def callback(self, interaction: nextcord.Interaction): async def callback(self, interaction: nextcord.Interaction):
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id) with Session(engine) as session:
verification_channel_id = session.scalar(stmt) stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
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) submission = submission_objects.get_user_submission(interaction.user.id)
@@ -148,20 +151,22 @@ class SubmissionCreateButton(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 score submission.""" """Button for score submission."""
# Don't allow a user to start a new submission if one already exists # Don't allow a user to start a new submission if one already exists
if submission_objects.has_user(interaction.user.id): if submission_objects.has_user(interaction.user.id):
await interaction.send("Please finish in progress submission first.", ephemeral=True) await interaction.send("Please finish in progress submission first.", ephemeral=True)
return return
stmt = ( with Session(engine) as session:
select(CategoryGroup.GroupId) stmt = (
.where(CategoryGroup.GuildId == interaction.guild_id) select(CategoryGroup.GroupId)
.where(CategoryGroup.MessageId == interaction.message.id) .where(CategoryGroup.GuildId == interaction.guild_id)
) .where(CategoryGroup.MessageId == interaction.message.id)
group_id = session.scalar(stmt) )
group_id = session.scalar(stmt)
stmt = select(Guild.SubmissionChannel).where(Guild.GuildId == interaction.guild_id) stmt = select(Guild.SubmissionChannel).where(Guild.GuildId == interaction.guild_id)
channel = interaction.guild.get_channel(session.scalar(stmt)) channel = interaction.guild.get_channel(session.scalar(stmt))
# 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...
@@ -226,6 +231,6 @@ class SubmissionView(nextcord.ui.View):
return check return check
async def on_timeout(self) -> None: # async def on_timeout(self) -> None:
# remove user from submission objects after time out. Allows user to start new submission. # # remove user from submission objects after time out. Allows user to start new submission.
submission_objects.remove_user_submission(self._submission_user) # submission_objects.remove_user_submission(self._submission_user)
-3
View File
@@ -1,3 +0,0 @@
FROM mysql:latest
COPY highscores_database.sql /docker-entrypoint-initdb.d/
+2 -1
View File
@@ -9,7 +9,8 @@ import sys
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
import utilities.config as config # pylint: disable=import-error
from utilities import config
from classes.submission import SubmissionMap from classes.submission import SubmissionMap
logger = logging.getLogger("nextcord") logger = logging.getLogger("nextcord")
-15
View File
@@ -1,15 +0,0 @@
version: "3"
services:
sqldb:
image: "highscores_db"
container_name: "Highscores-DataBase"
volumes:
- highscores_data:/var/lib/mysql
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=${SQL_PASS}
- MYSQL_ROOT_USERNAME=${SQL_USER}
volumes:
highscores_data:
@@ -3,8 +3,10 @@ use highscores;
CREATE TABLE Guilds( CREATE TABLE Guilds(
GuildId bigint unsigned not null unique, GuildId bigint unsigned not null unique,
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)
); );
@@ -17,7 +19,7 @@ CREATE TABLE CategoryGroups(
PadLength smallint not null DEFAULT 26, PadLength smallint not null DEFAULT 26,
UsernameLength smallint DEFAULT 12, UsernameLength smallint DEFAULT 12,
CONSTRAINT UniqueGuildCategoryGroup UNIQUE (GuildId, GroupName), CONSTRAINT UniqueGuildCategoryGroup UNIQUE (GuildId, GroupName),
FOREIGN KEY (GuildId) REFERENCES Guilds(GuildId), FOREIGN KEY (GuildId) REFERENCES Guilds(GuildId) ON DELETE CASCADE,
PRIMARY KEY (GroupId) PRIMARY KEY (GroupId)
); );
@@ -30,7 +32,7 @@ CREATE TABLE Categories(
IsTimeRecord boolean not null DEFAULT false, IsTimeRecord boolean not null DEFAULT false,
IsValidated boolean not null DEFAULT true, IsValidated boolean not null DEFAULT true,
CONSTRAINT UniqueCategoryInGroup UNIQUE (GroupId, CategoryName), CONSTRAINT UniqueCategoryInGroup UNIQUE (GroupId, CategoryName),
FOREIGN KEY (GroupId) REFERENCES CategoryGroups(GroupId), FOREIGN KEY (GroupId) REFERENCES CategoryGroups(GroupId) ON DELETE CASCADE,
PRIMARY Key (CategoryId) PRIMARY Key (CategoryId)
); );
@@ -38,6 +40,6 @@ CREATE TABLE Scores(
UserName varchar(30) not null, UserName varchar(30) not null,
Score float not null, -- Maybe should have time and int scores?? timestamp type? Score float not null, -- Maybe should have time and int scores?? timestamp type?
CategoryId int unsigned not null, CategoryId int unsigned not null,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId), FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE,
PRIMARY KEY (CategoryId, UserName) PRIMARY KEY (CategoryId, UserName)
); );
+3 -2
View File
@@ -1,5 +1,6 @@
nextcord>=2.3.2 nextcord>=2.3.2
python-dotenv>=0.20.0 python-dotenv>=0.20.0
pandas>=1.5.2 pandas>=1.5.2
sqlalchemy>=1.4.44 sqlalchemy<2.0
mysqlclient>-2.1.1 pymysql>=1.0.2
cryptography>=2.8
@@ -3,17 +3,15 @@ Simple file to set up the sqlalchemy connection to database.
""" """
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
import utilities.config as config # pylint: disable=import-error
from utilities import config
# connect with sql alchemy and autoload schema # connect with sql alchemy and autoload schema
url = f"mysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}" url = f"mysql+pymysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
engine = create_engine(url) engine = create_engine(url)
connection = engine.connect()
Base = automap_base() Base = automap_base()
Base.prepare(autoload_with=engine) Base.prepare(autoload_with=engine)
session = Session(engine)
# define the classes # define the classes
Guild = Base.classes.Guilds Guild = Base.classes.Guilds
+148 -119
View File
@@ -8,10 +8,10 @@ import time
import nextcord import nextcord
import pandas as pd import pandas as pd
from sqlalchemy import distinct, select from sqlalchemy import distinct, select
from sqlalchemy.orm import aliased from sqlalchemy.orm import Session, aliased
# pylint: disable=import-error # pylint: disable=import-error
from utilities.sql_alchemy_connection import Category, CategoryGroup, Guild, Score, engine, session from utilities.sql_data_classes import Category, CategoryGroup, Guild, Score, engine
MAX_LINE_LENGTH = 142 MAX_LINE_LENGTH = 142
@@ -30,27 +30,28 @@ def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "d
dict[str, str]: List of all category names that start with category_begin. dict[str, str]: List of all category names that start with category_begin.
""" """
# search database for all category names with Session(engine) as session:
subq = ( # search database for all category names
select(CategoryGroup) subq = (
.where(CategoryGroup.GuildId == guild_id) select(CategoryGroup)
.where(CategoryGroup.GroupId == group_id) .where(CategoryGroup.GuildId == guild_id)
.subquery() .where(CategoryGroup.GroupId == group_id)
) .subquery()
cat_group_alias = aliased(CategoryGroup, subq) )
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId) cat_group_alias = aliased(CategoryGroup, subq)
categories_dict: dict = {cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)} 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)}
# if string empty, return all category names # if string empty, return all category names
if not category_begin: if not category_begin:
return categories_dict return dict(list(category_begin.items())[:25])
# else find all categories that start with category_begin # else find all categories that start with category_begin
# TODO is it better to have sql handle the starts with or python? # TODO is it better to have sql handle the starts with or python?
categories: dict = { categories: "dict[str, str]" = {
cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower() cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower()
} }
return categories return dict(list(categories.items())[:25])
def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]": def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
@@ -65,22 +66,23 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
dict[str, str]: Dict of group names and ids where group name start with group_begin. Key is name, value is id. dict[str, str]: Dict of group names and ids where group name start with group_begin. Key is name, value is id.
""" """
# Select all groups in guild. with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id) # Select all groups in guild.
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == 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 = {group.GroupName: str(group.GroupId) for group in session.scalars(stmt)} group_dict: "dict[str, str]" = {group.GroupName: str(group.GroupId) for group in session.scalars(stmt)}
# if string empty, return all group names # if string empty, return all group names
if not group_begin: if not group_begin:
return group_dict return dict(list(group_dict.items())[:25])
# else find all groups that start with group_begin # else find all groups that start with group_begin
groups: dict = { groups: "dict[str, str]" = {
group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower() group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower()
} }
return groups return dict(list(groups.items())[:25])
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]": def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]":
@@ -98,41 +100,42 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
list[str]: List of users that start with user_begin. list[str]: List of users that start with user_begin.
""" """
# get all categories in the group with Session(engine) as session:
subq = ( # get all categories in the group
select(CategoryGroup)
.where(CategoryGroup.GuildId == guild_id)
.where(CategoryGroup.GroupId == group_id)
.subquery()
)
category_group = aliased(CategoryGroup, subq)
# If no category id, get all users in group.
if category_id == 0:
subq = select(Category.CategoryId).where(Category.GroupId == category_group.GroupId)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq))
else:
# get all users in category_id
subq = ( subq = (
select(Category) select(CategoryGroup)
.where(Category.GroupId == category_group.GroupId) .where(CategoryGroup.GuildId == guild_id)
.where(Category.CategoryId == category_id) .where(CategoryGroup.GroupId == group_id)
.subquery() .subquery()
) )
category = aliased(Category, subq) category_group = aliased(CategoryGroup, subq)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId == category.CategoryId)
users_list: "list[str]" = list(session.scalars(stmt)) # If no category id, get all users in group.
if category_id == 0:
subq = select(Category.CategoryId).where(Category.GroupId == category_group.GroupId)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq))
else:
# get all users in category_id
subq = (
select(Category)
.where(Category.GroupId == category_group.GroupId)
.where(Category.CategoryId == category_id)
.subquery()
)
category = aliased(Category, subq)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId == category.CategoryId)
users_list: "list[str]" = list(session.scalars(stmt))
# if string empty, return all users # if string empty, return all users
if not user_begin: if not user_begin:
return users_list return users_list[:25]
# else find all users that start with user_begin # else find all users that start with user_begin
users = [user for user in users_list if user_begin.lower() in user.lower()] users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()]
return users return users[:25]
def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]": def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
@@ -146,33 +149,37 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
Returns: Returns:
list[str]: All users with submissions in the guild. list[str]: All users with submissions in the guild.
""" """
group_ids = select(CategoryGroup.GroupId).where(CategoryGroup.GuildId == guild_id)
category_ids = select(Category.CategoryId).where(Category.GroupId.in_(group_ids)) with Session(engine) as session:
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(category_ids)) group_ids = select(CategoryGroup.GroupId).where(CategoryGroup.GuildId == guild_id)
users_list: "list[str]" = list(session.scalars(stmt)) category_ids = select(Category.CategoryId).where(Category.GroupId.in_(group_ids))
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(category_ids))
users_list: "list[str]" = list(session.scalars(stmt))
# if string empty, return all users # if string empty, return all users
if not user_begin: if not user_begin:
return users_list return users_list[:25]
# else find all users that start with user_begin # else find all users that start with user_begin
users = [user for user in users_list if user_begin.lower() in user.lower()] users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()]
return users return users[:25]
def format_highscore_message(group: CategoryGroup) -> str: def format_highscore_message(session: Session, group: CategoryGroup) -> str:
""" """
Creates a formatted string for a boss's highscore discord message. Creates a formatted string for a boss's highscore discord message.
This formatted string is in discord's code block to ensure monospace text. This formatted string is in discord's code block to ensure monospace text.
Loops through each category to build the message one place at a time. Loops through each category to build the message one place at a time.
Args: Args:
group (group): group object that contains the contents of the message. session (Session): the sqlalchemy session used for this function call.
group (CategoryGroup): group object that contains the contents of the message.
Returns: Returns:
str: Formatted string for discord message str: Formatted string for discord message
""" """
ret_string = f"**{group.GroupName}**\n```" ret_string = f"**{group.GroupName}**\n```"
stmt = select(Guild).where(Guild.GuildId == group.GuildId) stmt = select(Guild).where(Guild.GuildId == group.GuildId)
@@ -224,20 +231,12 @@ def format_highscore_message(group: CategoryGroup) -> str:
# add each score to list # add each score to list
for ind in range(max_cat_size): # need to subtract to deal with category name string for ind in range(max_cat_size): # need to subtract to deal with category name string
if ind in processed_group.index: if ind in processed_group.index:
user_length = group.UsernameLength username = processed_group["UserName"][ind]
username = processed_group["UserName"][ind][:user_length].ljust(user_length, " ") score = processed_group["Score"][ind]
# get score, pad to correct length str_list[ind + 1] += format_score_string(
if category.IsTimeRecord: username, score, group.UsernameLength, category.IsTimeRecord, ind + 1, pad_length
# 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: else:
# if not enough entries, pad out # if not enough entries, pad out
if ind + 1 <= category.Size: if ind + 1 <= category.Size:
@@ -255,7 +254,6 @@ def format_highscore_message(group: CategoryGroup) -> str:
# need to use index to allow for modifying the list # need to use index to allow for modifying the list
for ind, line in enumerate(str_list): for ind, line in enumerate(str_list):
if len(line) > MAX_LINE_LENGTH: if len(line) > MAX_LINE_LENGTH:
split_loc = int(MAX_LINE_LENGTH / pad_length) * pad_length split_loc = int(MAX_LINE_LENGTH / pad_length) * pad_length
new_line = line[split_loc:] new_line = line[split_loc:]
str_list[ind] = line[:split_loc] str_list[ind] = line[:split_loc]
@@ -276,6 +274,38 @@ def format_highscore_message(group: CategoryGroup) -> str:
return ret_string + "```" 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: def get_time_str(time_in: float) -> str:
""" """
Formats a float value into the desired string format for a timestamp. Formats a float value into the desired string format for a timestamp.
@@ -291,28 +321,31 @@ 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))
async def send_highscore_message(channel: nextcord.TextChannel, group: CategoryGroup) -> nextcord.Message: async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message:
""" """
Function takes care of checking if a message exists for a group. Function takes care of checking if a message exists for a group.
Creates and sends/edits the message. Creates and sends/edits the message.
Args: Args:
channel (nextcord.TextChannel): Discord channel object where message is sent session (Session): The sqlalchemy session being used for this function call.
group (CategoryGroup): group to post to the message guild (nextcord.Guild): the nextcord.Guild object requesting this message.
group (CategoryGroup): group to post to the message.
Returns: Returns:
nextcord.Message: Message object that was posted with group contents nextcord.Message: Message object that was posted with group contents.
""" """
highscore_string = format_highscore_message(group)
try: highscore_string = format_highscore_message(session, group)
message: nextcord.Message = await channel.fetch_message(group.MessageId) channel = guild.get_channel(group.ChannelId)
await message.edit(content=highscore_string)
except nextcord.NotFound: if group.MessageId 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.MessageId = message.id
session.commit() session.commit()
else:
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.edit(content=highscore_string)
return message return message
@@ -332,37 +365,31 @@ async def submit_score(
score (str): Score to submit. Need to convert to correct type. score (str): Score to submit. Need to convert to correct type.
""" """
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == category_id) with Session(engine) as session:
is_time_record = session.scalar(stmt) stmt = select(Category.IsTimeRecord).where(Category.CategoryId == category_id)
is_time_record = session.scalar(stmt)
if is_time_record: if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0]) minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1]) seconds = float(score.split(":", maxsplit=1)[1])
score = (minutes * 60) + seconds score = (minutes * 60) + seconds
else: else:
score = float(score) 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)
if existing_score: if existing_score:
existing_score.Score = score existing_score.Score = score
else: else:
score_obj = Score(UserName=username, Score=score, CategoryId=category_id) score_obj = Score(UserName=username, Score=score, CategoryId=category_id)
session.add(score_obj) session.add(score_obj)
session.commit() session.commit()
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == group_id) stmt = select(CategoryGroup).where(CategoryGroup.GroupId == group_id)
group = session.scalar(stmt) group = session.scalar(stmt)
# edit message await send_highscore_message(session, interaction.guild, group)
channel = interaction.guild.get_channel(group.ChannelId)
error_response = "Registered Highscores channel does not exist or was never registered."
error_response += 'Register with "/register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(channel, group)
def add_preset_configuration(guild_id: int, config_str: str) -> None: def add_preset_configuration(guild_id: int, config_str: str) -> None:
@@ -374,19 +401,21 @@ def add_preset_configuration(guild_id: int, config_str: str) -> None:
guild_id (int): Guild id to add config to guild_id (int): Guild id to add config to
config_str (str): Config name to add config_str (str): Config name to add
""" """
with open(f"example_config/{config_str}.json", encoding="utf-8", mode="r") as file:
config = json.load(file)
for group_name, categorys in config["tables"].items(): with Session(engine) as session:
group_obj = CategoryGroup(GroupName=group_name, GuildId=guild_id) with open(f"example_config/{config_str}.json", encoding="utf-8", mode="r") as file:
session.add(group_obj) config = json.load(file)
for category_name in categorys:
cat_obj = Category(
CategoryName=category_name,
Size=config["table_size"],
IsTimeRecord=config["categories"][category_name]["is_time_record"],
IsAscending=config["categories"][category_name]["ascending"],
)
session.add(cat_obj)
session.commit() for group_name, categorys in config["tables"].items():
group_obj = CategoryGroup(GroupName=group_name, GuildId=guild_id)
session.add(group_obj)
for category_name in categorys:
cat_obj = Category(
CategoryName=category_name,
Size=config["table_size"],
IsTimeRecord=config["categories"][category_name]["is_time_record"],
IsAscending=config["categories"][category_name]["ascending"],
)
session.add(cat_obj)
session.commit()