Compare commits

...
26 Commits
Author SHA1 Message Date
imcompiling 0b5330f7ca Merge commit '4e64eac17669e5e9b4e4626b3e2f9501b55c7aac'
build_latest / build (push) Successful in 16s
build_release / build (push) Successful in 16s
2024-08-01 22:07:55 -04:00
imcompiling 4e64eac176 Handle case when there are no non-discord members to view points for.
build_develop / build (push) Successful in 1m25s
2024-08-01 22:07:28 -04:00
imcompiling c90ddf42c3 Merge branch 'main' of https://gitea.cheezers.org/imcompiling/dakiny-bot
build_latest / build (push) Successful in 1m25s
build_develop / build (push) Successful in 16s
2024-08-01 21:50:02 -04:00
imcompiling 8eb2457ae9 Merge commit '2bdb7cb17b44a80286d5bbf689b1f2e5a88dcda0'
build_release / build (push) Successful in 16s
2024-08-01 21:49:21 -04:00
imcompiling 2bdb7cb17b Add the record point command.
build_develop / build (push) Successful in 1m28s
2024-08-01 21:48:33 -04:00
imcompiling 07906aaea1 Spell check and ruff fixes. 2024-08-01 21:46:16 -04:00
imcompiling 9109ac53b3 Use KANDY const instead of int.
build_latest / build (push) Successful in 1m44s
2024-07-13 12:54:55 -04:00
imcompiling 47a33128be Merge commit 'fdb04149aabea373e1f9b9570da0e5e66f6ac9da'
build_latest / build (push) Successful in 22s
build_release / build (push) Successful in 16s
build_develop / build (push) Successful in 1m57s
2024-05-31 15:21:06 -04:00
imcompiling fdb04149aa Define the /time at command.
build_develop / build (push) Successful in 1m32s
2024-05-31 15:20:59 -04:00
imcompiling 4805f6cd5d Add a constant for guild ids. By setting guild ids in commands, commands are added immediately.
Added the /time gametime command.
2024-05-31 14:28:22 -04:00
imcompiling ff85c8d485 Bugfix in record command. Missed moving a positional argument.
build_latest / build (push) Successful in 1m36s
build_release / build (push) Successful in 17s
build_develop / build (push) Successful in 24s
2024-05-26 20:06:43 -04:00
imcompiling ca0d27a929 Update dockerfile image. Need python3.12
build_latest / build (push) Successful in 1m47s
build_release / build (push) Successful in 23s
2024-05-24 14:19:17 -04:00
imcompiling 8ebb689ee7 Merge branch '63-bad-sorting'
build_latest / build (push) Failing after 31s
build_release / build (push) Failing after 29s
2024-05-24 14:11:44 -04:00
imcompiling ecc81c5f35 Sort the council member info.
build_develop / build (push) Failing after 31s
2024-05-24 14:03:23 -04:00
imcompiling 96c1fcb17c Sort all members by lowercase display name. 2024-05-24 13:58:25 -04:00
imcompiling 840e17e03a Adjust the presence because I can. Easier to understand.
build_develop / build (push) Failing after 29s
2024-05-24 13:56:39 -04:00
imcompiling f744d0d387 Merge branch '52-update-packages'
build_latest / build (push) Failing after 30s
build_release / build (push) Failing after 28s
2024-05-24 13:39:56 -04:00
imcompiling 9c71d24fe3 Unneeded file.
build_develop / build (push) Failing after 36s
2024-05-24 13:36:56 -04:00
imcompiling cdc335936c update dependencies
build_develop / build (push) Failing after 30s
2024-05-22 23:35:40 -04:00
imcompiling 303f0aa804 Update python, sqlalchemy, pandas, and nextcord. 2024-05-22 23:16:22 -04:00
imcompiling bc743cbb1a Merge branch 'fix-ruff-lint-smells'
build_latest / build (push) Successful in 18s
build_release / build (push) Successful in 19s
build_develop / build (push) Successful in 16s
2024-05-22 23:01:08 -04:00
imcompiling ed0895056d remove quotes from type annotation.
build_develop / build (push) Successful in 1m23s
2024-05-22 22:48:54 -04:00
imcompiling 985e253954 edits to the pyproject.
build_develop / build (push) Successful in 1m23s
2024-05-22 21:59:12 -04:00
imcompiling 9c175f3e52 Fix issues found while live testing.
build_develop / build (push) Successful in 1m35s
2024-05-22 21:15:37 -04:00
imcompiling b9621a67c9 Finish up smells in the commands directory.
Add a discord max message length constant.
Move boolean argument to not be positional.
2024-05-22 20:48:44 -04:00
imcompiling 9331e7ba4f aiofiles is async. Need to use it properly. 2024-05-22 20:06:50 -04:00
19 changed files with 433 additions and 196 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"cSpell.words": [
"aiofiles",
"blurple",
"CLANLEADER",
"Dakiny",
"DISCORDMANAGER",
"highcouncil"
]
}
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11.9-slim FROM python:3.12.3-slim
# install dependencies # install dependencies
RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3 RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3
+1 -1
View File
@@ -143,7 +143,7 @@ class ElectionCycle:
async def check_date(self) -> None: async def check_date(self) -> None:
"""Function checks current date to see if something needs to be run.""" """Function checks current date to see if something needs to be run."""
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC)
# check all dates if action is needed # check all dates if action is needed
if today.date() == self.election_start.date(): if today.date() == self.election_start.date():
+2 -2
View File
@@ -141,7 +141,7 @@ class VoteHighcouncilView(nextcord.ui.View):
) )
for nominee in sorted( for nominee in sorted(
session.scalars(stmt), session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name, key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
) )
] ]
options.append( options.append(
@@ -213,7 +213,7 @@ class VoteHighcouncilView(nextcord.ui.View):
) )
for nominee in sorted( for nominee in sorted(
session.scalars(stmt), session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name, key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
) )
] ]
options.append( options.append(
+1 -1
View File
@@ -130,7 +130,7 @@ class VoteView(nextcord.ui.View):
) )
for nominee in sorted( for nominee in sorted(
session.scalars(stmt), session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name, key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
) )
] ]
options.append( options.append(
+4 -2
View File
@@ -6,8 +6,9 @@ from cogs.commands.record import RecordCog
from cogs.commands.refresh import RefreshCog from cogs.commands.refresh import RefreshCog
from cogs.commands.remove import RemoveCog from cogs.commands.remove import RemoveCog
from cogs.commands.request import RequestCog from cogs.commands.request import RequestCog
from cogs.commands.set import setCog from cogs.commands.set import SetCog
from cogs.commands.test import TestCog from cogs.commands.test import TestCog
from cogs.commands.time import TimeCog
from cogs.commands.view import ViewCog from cogs.commands.view import ViewCog
@@ -19,6 +20,7 @@ def setup(bot: commands.Bot) -> None:
bot.add_cog(RefreshCog(bot)) bot.add_cog(RefreshCog(bot))
bot.add_cog(RemoveCog()) bot.add_cog(RemoveCog())
bot.add_cog(RequestCog(bot)) bot.add_cog(RequestCog(bot))
bot.add_cog(SetCog())
bot.add_cog(TestCog(bot)) bot.add_cog(TestCog(bot))
bot.add_cog(TimeCog())
bot.add_cog(ViewCog()) bot.add_cog(ViewCog())
bot.add_cog(setCog())
+102 -9
View File
@@ -5,7 +5,7 @@ from discord import TextInputStyle
from nextcord.ext import commands from nextcord.ext import commands
from config import DakinyConfig from config import DakinyConfig
from utils import record_event_points from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, record_event_points
class RecordCog(commands.Cog, name="Record Commands"): class RecordCog(commands.Cog, name="Record Commands"):
@@ -16,7 +16,7 @@ class RecordCog(commands.Cog, name="Record Commands"):
super().__init__() super().__init__()
self.bot: commands.Bot = bot self.bot: commands.Bot = bot
@nextcord.slash_command() @nextcord.slash_command(guild_ids=GUILDS)
async def record(self, interaction: nextcord.Interaction) -> None: async def record(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for record. Never called.""" """Parent slash command for record. Never called."""
@@ -64,6 +64,86 @@ class RecordCog(commands.Cog, name="Record Commands"):
embed=embed, embed=embed,
) )
@record.subcommand(description="Record a miscellaneous point for a clan member.")
async def point( # noqa: PLR0913
self,
interaction: nextcord.Interaction,
activity: str = nextcord.SlashOption(
description="The type of activity to receive points for.",
required=True,
choices=[
"Clan chat activity",
"Teaching or sharing in-game tips",
"Recruiting efforts",
"Successful recruitment",
],
),
justification: str = nextcord.SlashOption(
required=True,
description="Reason to give the recipient a point.",
max_length=int(MAX_DISCORD_MESSAGE_LENGTH / 2),
),
recipient: nextcord.Member = nextcord.SlashOption(
required=False,
description="Discord user to give point to.",
),
non_discord_recipient: str = nextcord.SlashOption(
required=False, description="Non Discord user to give point to.", max_length=32
),
) -> None:
"""
Gives a point to the provided recipient. Sends recording message to event participation channel.
Only one recipient should be given point/provided to the command. Fails if not exactly one recipient
is given.
Args:
interaction (nextcord.Interaction): interaction with this command.
activity (str): Choice list of activities to give point for.
justification (str): Reason to give point.
recipient (nextcord.Member): Discord member to give point to.
non_discord_recipient (str): Non Discord member to give point to.
"""
# role check
if interaction.guild.get_role(self.bot.config.params.clan_member_role_id) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{self.bot.config.params.clan_member_role_id}> role to record points.",
ephemeral=True,
)
return
# pylint or whatever seems to think that there are lines of unreachable code. That is false.
if recipient and non_discord_recipient:
await interaction.response.send_message("Please only provide 1 recipient.", ephemeral=True)
return
elif recipient is not None: # noqa: RET505
record_event_points(
self.bot.config,
[str(recipient.id)],
self.bot.config.params.event_participate_points,
is_member=True,
)
recipient_str = f"<@{recipient.id}>"
elif non_discord_recipient is not None:
record_event_points(
self.bot.config,
[non_discord_recipient],
self.bot.config.params.event_participate_points,
is_member=False,
)
recipient_str = f'"{non_discord_recipient}"'
else:
await interaction.response.send_message("You must provide a recipient.", ephemeral=True)
return
event_channel: nextcord.TextChannel = interaction.guild.get_channel(
self.bot.config.params.event_post_channel_id
)
msg = f"<@{interaction.user.id}> has given {recipient_str} points for the {activity} activity. Justification: {justification}." # noqa: E501
await event_channel.send(msg)
await interaction.response.send_message("Point has been added.", ephemeral=True)
class EventView(nextcord.ui.View): class EventView(nextcord.ui.View):
""" """
@@ -109,8 +189,8 @@ class EventView(nextcord.ui.View):
record_event_points( record_event_points(
self.config, self.config,
discord_users_list, discord_users_list,
True, # noqa: FBT003
self.config.params.event_participate_points, self.config.params.event_participate_points,
is_member=True,
) )
non_discord_users = interaction.message.embeds[0].fields[1].value non_discord_users = interaction.message.embeds[0].fields[1].value
@@ -119,11 +199,11 @@ class EventView(nextcord.ui.View):
record_event_points( record_event_points(
self.config, self.config,
non_discord_users_list, non_discord_users_list,
False,
self.config.params.event_participate_points, self.config.params.event_participate_points,
is_member=False,
) )
record_event_points(self.config, [self.host_id], True, self.config.params.event_host_points) record_event_points(self.config, [self.host_id], self.config.params.event_host_points, is_member=True)
event_channel: nextcord.TextChannel = interaction.guild.get_channel(self.config.params.event_post_channel_id) event_channel: nextcord.TextChannel = interaction.guild.get_channel(self.config.params.event_post_channel_id)
await event_channel.send(embed=interaction.message.embeds[0]) await event_channel.send(embed=interaction.message.embeds[0])
@@ -134,12 +214,13 @@ class EventView(nextcord.ui.View):
class ParticipantModal(nextcord.ui.Modal): class ParticipantModal(nextcord.ui.Modal):
""" """
This modal contains all the text edits for an event. You can change the name, description, and add the usernames This modal contains all the text edits for an event.
of participants who are not in the discord.
You can change the name, description, and add the usernames of participants who are not in the discord.
""" """
# TODO turn this embed into a dict # consider turning this embed into a dict
def __init__(self, embed): def __init__(self, embed: nextcord.Embed):
super().__init__( super().__init__(
"Edit the event.", "Edit the event.",
timeout=5 * 60, timeout=5 * 60,
@@ -170,6 +251,12 @@ class ParticipantModal(nextcord.ui.Modal):
self.add_item(self.participants) self.add_item(self.participants)
async def callback(self, interaction: nextcord.Interaction) -> None: async def callback(self, interaction: nextcord.Interaction) -> None:
"""
Edits the interaction message to contain the Non-Discord participants.
Args:
interaction (nextcord.Interaction): Interaction of the callback.
"""
self.embed.title = self.name.value self.embed.title = self.name.value
self.embed.description = self.description.value self.embed.description = self.description.value
self.embed.set_field_at(index=1, name="Non-Discord Participants", value=self.participants.value) self.embed.set_field_at(index=1, name="Non-Discord Participants", value=self.participants.value)
@@ -189,6 +276,12 @@ class ParticipantSelect(nextcord.ui.UserSelect):
) )
async def callback(self, interaction: nextcord.Interaction) -> None: async def callback(self, interaction: nextcord.Interaction) -> None:
"""
Edits the interaction message to contain the Discord participants.
Args:
interaction (nextcord.Interaction): Interaction of the callback.
"""
embed: nextcord.Embed = interaction.message.embeds[0] embed: nextcord.Embed = interaction.message.embeds[0]
participants: str = "\n".join([f"<@{participant.id}>" for participant in self.values.members]) participants: str = "\n".join([f"<@{participant.id}>" for participant in self.values.members])
embed.set_field_at(0, name="Participants", value=participants) embed.set_field_at(0, name="Participants", value=participants)
+14 -17
View File
@@ -1,8 +1,7 @@
""" """Contained commands and views for refresh commands."""
Contained commands and views for refresh commands.
"""
import os import os
from pathlib import Path
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
@@ -11,22 +10,16 @@ from utils import MESSAGES_DIR, edit_clan_council_info, update_clan_messages
class RefreshCog(commands.Cog, name="Refresh Commands"): class RefreshCog(commands.Cog, name="Refresh Commands"):
""" """Class holds refresh commands."""
Class holds refresh commands.
"""
def __init__(self, bot) -> None: def __init__(self, bot: commands.Bot) -> None:
""" """Default constructor for RefreshCog class. Does nothing."""
Default constructor for RefreshCog class. Does nothing.
"""
super().__init__() super().__init__()
self.bot: commands.Bot = bot self.bot: commands.Bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True)) @nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def refresh(self, interaction: nextcord.Interaction): async def refresh(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for refresh. Never called."""
Parent slash command for refresh. Never called.
"""
@refresh.subcommand(description="Force refresh all of the bot managed messages.") @refresh.subcommand(description="Force refresh all of the bot managed messages.")
async def messages( async def messages(
@@ -36,8 +29,10 @@ class RefreshCog(commands.Cog, name="Refresh Commands"):
description="Channel to refresh messages in.", description="Channel to refresh messages in.",
required=False, required=False,
), ),
): ) -> None:
""" """
Updates all messages.
Calls update_clan_messages() to update all messages. Calls update_clan_messages() to update all messages.
Args: Args:
@@ -48,7 +43,7 @@ class RefreshCog(commands.Cog, name="Refresh Commands"):
if channel is None: if channel is None:
channel_updates = [ channel_updates = [
dir_name for dir_name in os.listdir(MESSAGES_DIR) if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}") dir_name for dir_name in os.listdir(MESSAGES_DIR) if Path(f"{MESSAGES_DIR}/{dir_name}").is_dir()
] ]
else: else:
channel_updates = [channel.name] channel_updates = [channel.name]
@@ -57,8 +52,10 @@ class RefreshCog(commands.Cog, name="Refresh Commands"):
await interaction.followup.send("Messages have been refreshed.", ephemeral=True) await interaction.followup.send("Messages have been refreshed.", ephemeral=True)
@refresh.subcommand(description="Updates the current council info message.") @refresh.subcommand(description="Updates the current council info message.")
async def council_info(self, interaction: nextcord.Interaction): async def council_info(self, interaction: nextcord.Interaction) -> None:
""" """
Edits the clan council info message.
Calls edit_clan_council_info(). Calls edit_clan_council_info().
Args: Args:
+11 -18
View File
@@ -1,6 +1,4 @@
""" """Commands for removing information in the database."""
Commands for removing information in the database.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
@@ -11,35 +9,29 @@ from database_setup.sql_data_classes import Presences, engine
class RemoveCog(commands.Cog, name="Remove Commands"): class RemoveCog(commands.Cog, name="Remove Commands"):
""" """Holds the view commands."""
Holds the view commands.
"""
def __init__(self) -> None: def __init__(self) -> None:
""" """Default constructor for RemoveCog class. Does nothing."""
Default constructor for RemoveCog class. Does nothing.
"""
@nextcord.slash_command( @nextcord.slash_command(
default_member_permissions=nextcord.Permissions(administrator=True), default_member_permissions=nextcord.Permissions(administrator=True),
) )
async def remove(self, interaction: nextcord.Interaction): async def remove(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for set. Never called."""
Parent slash command for set. Never called.
"""
@remove.subcommand(description="Remove a presence from the database.") @remove.subcommand(description="Remove a presence from the database.")
async def presence( async def presence(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
presence_str: str = nextcord.SlashOption(description="The presence to remove", required=True), presence_str: str = nextcord.SlashOption(description="The presence to remove", required=True),
): ) -> None:
""" """
Deletes the given presence id from the database. Deletes the given presence id from the database.
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command. interaction (nextcord.Interaction): Interaction that invoked the command.
presence (str): Is SlashOption. The presence to remove. Is the ID of the presence. presence_str (str): Is SlashOption. The presence to remove. Is the ID of the presence.
""" """
presence_id = int(presence_str) presence_id = int(presence_str)
with Session(engine) as session: with Session(engine) as session:
@@ -50,9 +42,10 @@ class RemoveCog(commands.Cog, name="Remove Commands"):
await interaction.response.send_message("Presence deleted.", ephemeral=True) await interaction.response.send_message("Presence deleted.", ephemeral=True)
@presence.on_autocomplete("presence_str") @presence.on_autocomplete("presence_str")
async def presence_autocomplete(self, interaction: nextcord.Interaction, presence_str: str): async def presence_autocomplete(self, interaction: nextcord.Interaction, presence_str: str) -> None:
""" """
Collects all presences that start with the presence string. Collects all presences that start with the presence string.
Sends autocomplete dict {Presence Name, Presence Id} Sends autocomplete dict {Presence Name, Presence Id}
Args: Args:
@@ -61,14 +54,14 @@ class RemoveCog(commands.Cog, name="Remove Commands"):
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(Presences) stmt = select(Presences)
presences_dict: "dict[str,str]" = {pres.Presence: str(pres.Id) for pres in session.scalars(stmt)} presences_dict: dict[str,str] = {pres.Presence: str(pres.Id) for pres in session.scalars(stmt)}
# if empty compare string # if empty compare string
if not presence_str: if not presence_str:
await interaction.response.send_autocomplete(dict(list(presences_dict.items())[:25])) await interaction.response.send_autocomplete(dict(list(presences_dict.items())[:25]))
return return
autofilled_presences: "dict[str,str]" = { autofilled_presences: dict[str,str] = {
pres: pres_id for pres, pres_id in presences_dict.items() if presence_str.lower() in pres.lower() pres: pres_id for pres, pres_id in presences_dict.items() if presence_str.lower() in pres.lower()
} }
await interaction.response.send_autocomplete(autofilled_presences[:25]) await interaction.response.send_autocomplete(autofilled_presences[:25])
+26 -30
View File
@@ -1,31 +1,27 @@
""" """Contains commands and views for request commands."""
Contains commands and views for request commands.
""" from typing import TYPE_CHECKING
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import Presences, engine from database_setup.sql_data_classes import Presences, engine
if TYPE_CHECKING:
from config import DakinyConfig
class RequestCog(commands.Cog, name="Request Commands"): class RequestCog(commands.Cog, name="Request Commands"):
""" """Class holds request commands."""
Class holds request commands.
"""
def __init__(self, bot) -> None: def __init__(self, bot: commands.Bot) -> None:
""" """Default constructor for RequestCog class. Does nothing."""
Default constructor for RequestCog class. Does nothing.
"""
self.bot: commands.Bot = bot self.bot: commands.Bot = bot
@nextcord.slash_command() @nextcord.slash_command()
async def request(self, interaction: nextcord.Interaction): async def request(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for request. Never called."""
Parent slash command for request. Never called.
"""
@request.subcommand(description="Request to add a presence to the bot's presence list.") @request.subcommand(description="Request to add a presence to the bot's presence list.")
async def add_presence( async def add_presence(
@@ -37,7 +33,7 @@ class RequestCog(commands.Cog, name="Request Commands"):
max_length=100, max_length=100,
description="The presence you want to add.", description="The presence you want to add.",
), ),
): ) -> None:
""" """
Sends a message to admins to request to add a presence. Sends a message to admins to request to add a presence.
@@ -61,13 +57,13 @@ class RequestCog(commands.Cog, name="Request Commands"):
max_length=32, max_length=32,
description="Your requested new nickname.", description="Your requested new nickname.",
), ),
): ) -> None:
""" """
Sends a message to admins to request a nickname change. Sends a message to admins to request a nickname change.
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command. interaction (nextcord.Interaction): Interaction that invoked the command.
new_username(str): The username to change to. new_nickname(str): The username to change to.
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."' content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."'
@@ -77,9 +73,7 @@ class RequestCog(commands.Cog, name="Request Commands"):
class UsernameRequest(nextcord.ui.View): class UsernameRequest(nextcord.ui.View):
""" """View provides approve and deny buttons and action for a username change request."""
View provides approve and deny buttons and action for a username change request.
"""
def __init__( def __init__(
self, self,
@@ -91,9 +85,11 @@ class UsernameRequest(nextcord.ui.View):
self.new_nickname = new_nickname self.new_nickname = new_nickname
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green) @nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Approve button for a nickname change request. Changes the target's nickname to self.new_nickname. Approve button for a nickname change request.
Changes the target's nickname to self.new_nickname.
Catches an error if the target as a higher role than the bot. Catches an error if the target as a higher role than the bot.
Removes the view from the interaction message. Removes the view from the interaction message.
@@ -124,7 +120,7 @@ class UsernameRequest(nextcord.ui.View):
) )
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red) @nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Deny button for a nickname request. Just removes the view and logs who denied the request. Deny button for a nickname request. Just removes the view and logs who denied the request.
@@ -137,9 +133,7 @@ class UsernameRequest(nextcord.ui.View):
class PresenceChange(nextcord.ui.View): class PresenceChange(nextcord.ui.View):
""" """View provides approve and deny buttons and action for a username change request."""
View provides approve and deny buttons and action for a username change request.
"""
def __init__( def __init__(
self, self,
@@ -149,9 +143,11 @@ class PresenceChange(nextcord.ui.View):
self.presence = presence self.presence = presence
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green) @nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Approve button for a presence add request. Adds the presence to the database. Approve button for a presence add request.
Adds the presence to the database.
Removes the view from the interaction message. Removes the view from the interaction message.
Args: Args:
@@ -166,7 +162,7 @@ class PresenceChange(nextcord.ui.View):
await interaction.response.edit_message(content=content, view=None) await interaction.response.edit_message(content=content, view=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red) @nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Deny button for a presence add request. Just removes the view and logs who denied the request. Deny button for a presence add request. Just removes the view and logs who denied the request.
+9 -17
View File
@@ -1,6 +1,4 @@
""" """Contains commands and views for set commands."""
Contains commands and views for set commands.
"""
from datetime import UTC, datetime from datetime import UTC, datetime
@@ -14,21 +12,15 @@ from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list from main import birthday_list
class setCog(commands.Cog, name="Set Commands"): class SetCog(commands.Cog, name="Set Commands"):
""" """Class holds set commands."""
Class holds set commands.
"""
def __init__(self) -> None: def __init__(self) -> None:
""" """Default constructor for SetCog class. Does nothing."""
Default constructor for SetCog class. Does nothing.
"""
@nextcord.slash_command() @nextcord.slash_command()
async def set(self, interaction: nextcord.Interaction): async def set(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for set. Never called."""
Parent slash command for set. Never called.
"""
@set.subcommand(description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself.") @set.subcommand(description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself.")
async def birthday( async def birthday(
@@ -36,7 +28,7 @@ class setCog(commands.Cog, name="Set Commands"):
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."), month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."),
day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."), day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."),
): ) -> None:
""" """
Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list. Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list.
@@ -57,9 +49,9 @@ class setCog(commands.Cog, name="Set Commands"):
return return
# check if valid day # check if valid day
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC)
try: try:
birthday = datetime(today.year, month, day) birthday = datetime(today.year, month, day, tzinfo=UTC)
except ValueError: except ValueError:
await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True) await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True)
return return
+61 -39
View File
@@ -1,51 +1,48 @@
""" """Commands for testing."""
Commands for testing.
""" from typing import TYPE_CHECKING
import nextcord import nextcord
from nextcord.ext import application_checks, commands from nextcord.ext import application_checks, commands
from classes.election_cycle import ElectionCycle from classes.election_cycle import ElectionCycle
from config import DakinyConfig from utils import GUILDS, get_current_election_cycle
from utils import get_current_election_cycle
if TYPE_CHECKING:
from config import DakinyConfig
def is_me(): ME = 206602713511886849
"""
predicate that makes sure only I can run the test commands.
"""
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849 def is_me(): # noqa: ANN201
"""Predicate that makes sure only I can run the test commands."""
def predicate(interaction: nextcord.Interaction) -> bool:
return interaction.user.id == ME
return application_checks.check(predicate) return application_checks.check(predicate)
class TestCog(commands.Cog, name="Test commands."): class TestCog(commands.Cog, name="Test commands."):
""" """Class holds commands for testing."""
Class holds commands for testing.
"""
def __init__(self, bot) -> None: def __init__(self, bot: commands.Bot) -> None:
""" """Default constructor for HelpCog class. Does nothing."""
Default constructor for HelpCog class. Does nothing. self.bot: commands.Bot = bot
"""
self.bot = bot
@nextcord.slash_command(guild_ids=[1000247361639030804]) @nextcord.slash_command(guild_ids=GUILDS)
async def test(self, interaction: nextcord.Interaction): async def test(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for help. Never called."""
Parent slash command for help. Never called.
"""
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def election_start(self, interaction: nextcord.Interaction): async def election_start(self, interaction: nextcord.Interaction) -> None:
""" """
idc Force starts election.
Args: Args:
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): Interaction with the message.
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
@@ -54,15 +51,16 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.election_start_action() await cycle.election_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def voting_start(self, interaction: nextcord.Interaction): async def voting_start(self, interaction: nextcord.Interaction) -> None:
""" """
tests the voting start action Force starts voting.
Args: Args:
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): Interaction with the message.
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
@@ -71,15 +69,16 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.voting_start_action() await cycle.voting_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def voting_end(self, interaction: nextcord.Interaction): async def voting_end(self, interaction: nextcord.Interaction) -> None:
""" """
tests the voting start action Force ends voting.
Args: Args:
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): Interaction with the message.
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
@@ -88,15 +87,16 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.voting_end_action() await cycle.voting_end_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def highcouncil_vote(self, interaction: nextcord.Interaction): async def highcouncil_vote(self, interaction: nextcord.Interaction) -> None:
""" """
tests the voting start action Force starts highcouncil voting.
Args: Args:
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): Interaction with the message.
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
@@ -105,10 +105,17 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.highcouncil_election_start_action() await cycle.highcouncil_election_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def election_reminder(self, interaction: nextcord.Interaction): async def election_reminder(self, interaction: nextcord.Interaction) -> None:
"""
Forces election reminder.
Args:
interaction (nextcord.Interaction): Interaction with the message.
"""
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_election_cycle(config), get_current_election_cycle(config),
@@ -116,10 +123,17 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.election_reminder() await cycle.election_reminder()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def daily(self, interaction: nextcord.Interaction): async def daily(self, interaction: nextcord.Interaction) -> None:
"""
Forces daily function.
Args:
interaction (nextcord.Interaction): Interaction with the message.
"""
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_election_cycle(config), get_current_election_cycle(config),
@@ -127,10 +141,17 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.check_date() await cycle.check_date()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def term_start(self, interaction: nextcord.Interaction): async def term_start(self, interaction: nextcord.Interaction) -> None:
"""
Forces term start.
Args:
interaction (nextcord.Interaction): Interaction with the message.
"""
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_election_cycle(config), get_current_election_cycle(config),
@@ -138,3 +159,4 @@ class TestCog(commands.Cog, name="Test commands."):
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
await cycle.term_start_action() await cycle.term_start_action()
await interaction.send("Test command sent.", ephemeral=True)
+109
View File
@@ -0,0 +1,109 @@
"""Contains commands for time commands."""
from datetime import MAXYEAR, MINYEAR, UTC, datetime
import nextcord
from nextcord.ext import commands
from utils import GUILDS
class TimeCog(commands.Cog, name="Time Commands"):
"""Class holds time commands."""
@nextcord.slash_command(guild_ids=GUILDS)
async def time(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command. Never called."""
@time.subcommand(
description="Get a discord timestamp of the given game time. If date information is left blank, uses current day." # noqa: E501
)
async def at( # noqa: PLR0913
self,
interaction: nextcord.Interaction,
hour: int = nextcord.SlashOption(
description="The hour you want a timestamp of. Given in gametime/UTC.",
min_value=0,
max_value=23,
required=True,
),
minute: int = nextcord.SlashOption(
description="The minute you want a timestamp of. Given in gametime/UTC.",
min_value=0,
max_value=59,
required=True,
),
year: int = nextcord.SlashOption(
description="Year to make timestamp of.", required=False, min_value=MINYEAR, max_value=MAXYEAR
),
month: int = nextcord.SlashOption(
description="Month to make timestamp of.", required=False, min_value=1, max_value=12
),
day: int = nextcord.SlashOption(
description="Day to make timestamp of.", required=False, min_value=1, max_value=31
),
ephemeral: bool = nextcord.SlashOption( # noqa: FBT001
description="Whether the replying message will be public or not.", required=False, default=True
),
) -> None:
"""
Replies with a discord timestamp of given UTC time.
Args:
interaction (nextcord.Interaction): Interaction with the command.
hour (int): The hour in UTC to make the timestamp of..
minute (int): The minute in UTC to make the timestamp of..
year (int): The year to make the timestamp of.
month (int): The month to make the timestamp of.
day (int): The day to make the timestamp of.
ephemeral (bool): Whether the response is ephemeral or not.
"""
gametime_today: datetime = datetime.now(tz=UTC).replace(hour=hour, minute=minute)
if year is not None:
gametime_today = gametime_today.replace(year=year)
if month is not None and day is not None:
try:
gametime_today = gametime_today.replace(month=month, day=day)
except ValueError:
await interaction.response.send_message(f"Day {day} is out of range for month {month}.", ephemeral=True)
return
else:
if month is not None:
try:
gametime_today = gametime_today.replace(month=month)
except ValueError:
await interaction.response.send_message(
f"Today ({gametime_today.day}) is out of range for month {month}.", ephemeral=True
)
return
if day is not None:
try:
gametime_today = gametime_today.replace(day=day)
except ValueError:
await interaction.response.send_message(
f"Day {day} is out of range for this month {gametime_today.month}.", ephemeral=True
)
return
await interaction.response.send_message(content=f"<t:{int(gametime_today.timestamp())}:t>", ephemeral=ephemeral)
@time.subcommand(description="Get the current time in game.")
async def gametime(
self,
interaction: nextcord.Interaction,
ephemeral: bool = nextcord.SlashOption( # noqa: FBT001
description="Whether the replying message will be public or not.", required=False, default=True
),
) -> None:
"""
Replies with the current time in game.
Args:
interaction (nextcord.Interaction): Interaction with the command.
ephemeral: (bool): Whether the response is ephemeral or not.
"""
gametime = datetime.now(tz=UTC)
await interaction.response.send_message(content=f"{gametime.hour}:{gametime.minute}", ephemeral=ephemeral)
+41 -30
View File
@@ -1,6 +1,4 @@
""" """Commands for viewing information in the database."""
Commands for viewing information in the database.
"""
import nextcord import nextcord
import pandas as pd import pandas as pd
@@ -14,24 +12,18 @@ from database_setup.sql_data_classes import (
Votes, Votes,
engine, engine,
) )
from utils import VoteType from utils import MAX_DISCORD_MESSAGE_LENGTH, VoteType
class ViewCog(commands.Cog, name="View Commands"): class ViewCog(commands.Cog, name="View Commands"):
""" """Holds the view commands."""
Holds the view commands.
"""
def __init__(self) -> None: def __init__(self) -> None:
""" """Default constructor for ViewCog class. Does nothing."""
Default constructor for ViewCog class. Does nothing.
"""
@nextcord.slash_command() @nextcord.slash_command()
async def view(self, interaction: nextcord.Interaction): async def view(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for set. Never called."""
Parent slash command for set. Never called.
"""
@view.subcommand(description="Show event points.") @view.subcommand(description="Show event points.")
async def event_points( async def event_points(
@@ -41,40 +33,55 @@ class ViewCog(commands.Cog, name="View Commands"):
description="The quarter to view", description="The quarter to view",
required=False, required=False,
), ),
): ) -> None:
"""
View the event points.
Builds a message with the event points for the quarter that matches with quarter_name.
Args:
interaction (nextcord.Interaction): Interaction with the message.
quarter_name (str, optional): Same of quarter to autofill with. Is SlashOption.
"""
# just in case # just in case
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
with Session(engine) as session: with Session(engine) as session:
if quarter_name: if quarter_name:
df = pd.read_sql_query( ppdf = pd.read_sql_query(
sql=session.query(ParticipationPoints) sql=session.query(ParticipationPoints)
.where(ParticipationPoints.Cycle.startswith(quarter_name)) .where(ParticipationPoints.Cycle.startswith(quarter_name))
.statement, .statement,
con=engine, con=engine,
) )
else: else:
df = pd.read_sql_query( ppdf = pd.read_sql_query(
sql=session.query(ParticipationPoints).statement, sql=session.query(ParticipationPoints).statement,
con=engine, con=engine,
) )
if df.empty: if ppdf.empty:
await interaction.followup.send(f'Quarter "{quarter_name}" is invalid.', ephemeral=True) await interaction.followup.send(f'Quarter "{quarter_name}" is invalid.', ephemeral=True)
return return
cycles = df.groupby("Cycle") cycles = ppdf.groupby("Cycle")
content = "" content = ""
for cycle in cycles.groups: for cycle in cycles.groups:
content += f"**{cycle}**\n" content += f"**{cycle}**\n"
unsorted_all = cycles.get_group(cycle).groupby("DiscordMember") unsorted_all = cycles.get_group(cycle).groupby("DiscordMember")
members = unsorted_all.get_group(True) members = unsorted_all.get_group(name=True)
non_members = unsorted_all.get_group(False)
# there can be no non-discord members sometimes.
try:
sorted_non_members = unsorted_all.get_group(name=False).sort_values(by="UserId")
except KeyError:
sorted_non_members = None
sorted_members = members.sort_values( sorted_members = members.sort_values(
by="UserId", by="UserId",
key=lambda user_ids: ([interaction.guild.get_member(int(id)).display_name for id in user_ids]), key=lambda user_ids: (
[interaction.guild.get_member(int(user_id)).display_name.lower() for user_id in user_ids]
),
) )
sorted_non_members = non_members.sort_values(by="UserId")
sorted_all = pd.concat([sorted_members, sorted_non_members], ignore_index=True, sort=False) sorted_all = pd.concat([sorted_members, sorted_non_members], ignore_index=True, sort=False)
for row in sorted_all.itertuples(): for row in sorted_all.itertuples():
userid = row.UserId userid = row.UserId
@@ -83,7 +90,7 @@ class ViewCog(commands.Cog, name="View Commands"):
content += f"<@{userid}>: {points}\n" if discord_member else f"{userid}: {points}\n" content += f"<@{userid}>: {points}\n" if discord_member else f"{userid}: {points}\n"
content += "\n" content += "\n"
if len(content) > 2000: if len(content) > MAX_DISCORD_MESSAGE_LENGTH:
await interaction.followup.send("Message length is too long, please select search to limit results.") await interaction.followup.send("Message length is too long, please select search to limit results.")
return return
@@ -97,12 +104,13 @@ class ViewCog(commands.Cog, name="View Commands"):
description="A word or string to search for in the presences.", description="A word or string to search for in the presences.",
required=False, required=False,
), ),
): ) -> None:
""" """
Gathers all presences from the database and responds with a message showing them. Gathers all presences from the database and responds with a message showing them.
Args: Args:
interaction (nextcord.Integration): User's interaction with the command. interaction (nextcord.Integration): User's interaction with the command.
search (str): string to search for in presences. Is SlashOption.
""" """
# might take a while depending on number of presences # might take a while depending on number of presences
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
@@ -130,8 +138,10 @@ class ViewCog(commands.Cog, name="View Commands"):
description="The election type. Council, Discord Manager, or Clan Leader.", description="The election type. Council, Discord Manager, or Clan Leader.",
choices=VoteType, choices=VoteType,
), ),
): ) -> None:
""" """
View all voters and who they voted for.
Takes in an election name and sends a response message to the user with a list of people who voted in that Takes in an election name and sends a response message to the user with a list of people who voted in that
election and who they voted for. Notes those tho abstained. Tells a user if the election is invalid. Uses election and who they voted for. Notes those tho abstained. Tells a user if the election is invalid. Uses
autocompletion to assist with getting elections. autocompletion to assist with getting elections.
@@ -139,6 +149,7 @@ class ViewCog(commands.Cog, name="View Commands"):
Args: Args:
interaction (nextcord.Interaction): User's interaction with the command. interaction (nextcord.Interaction): User's interaction with the command.
election_name (str): Is SlashOption. The name of the election to view votes for. election_name (str): Is SlashOption. The name of the election to view votes for.
election_type(str): Is SlashOption. The type of election to search for. Choice from StrEnum VoteType.
""" """
stmt = select(Votes).where(Votes.ElectionCycle == election_name).where(Votes.Type == election_type) stmt = select(Votes).where(Votes.ElectionCycle == election_name).where(Votes.Type == election_type)
@@ -160,7 +171,7 @@ class ViewCog(commands.Cog, name="View Commands"):
await interaction.response.send_message(content=content, ephemeral=True) await interaction.response.send_message(content=content, ephemeral=True)
@votes.on_autocomplete("election_name") @votes.on_autocomplete("election_name")
async def election_autocomplete(self, interaction: nextcord.Interaction, election_name: str): async def election_autocomplete(self, interaction: nextcord.Interaction, election_name: str) -> None:
""" """
Collects all elections that start with the election string. Collects all elections that start with the election string.
@@ -177,13 +188,13 @@ class ViewCog(commands.Cog, name="View Commands"):
await interaction.response.send_autocomplete(list(elections)[:25]) await interaction.response.send_autocomplete(list(elections)[:25])
return return
autofilled_elections: "list[str]" = [ autofilled_elections: list[str] = [
election for election in elections if election.lower().startswith(election_name.lower()) election for election in elections if election.lower().startswith(election_name.lower())
] ]
await interaction.response.send_autocomplete(autofilled_elections[:25]) await interaction.response.send_autocomplete(autofilled_elections[:25])
@event_points.on_autocomplete("quarter_name") @event_points.on_autocomplete("quarter_name")
async def event_points_autocomplete(self, interaction: nextcord.Interaction, quarter_name: str): async def event_points_autocomplete(self, interaction: nextcord.Interaction, quarter_name: str) -> None:
""" """
Collects all elections that start with the election string. Collects all elections that start with the election string.
@@ -200,7 +211,7 @@ class ViewCog(commands.Cog, name="View Commands"):
await interaction.response.send_autocomplete(list(quarters)[:25]) await interaction.response.send_autocomplete(list(quarters)[:25])
return return
autofilled_quarters: "list[str]" = [ autofilled_quarters: list[str] = [
quarter for quarter in quarters if quarter.lower().startswith(quarter_name.lower()) quarter for quarter in quarters if quarter.lower().startswith(quarter_name.lower())
] ]
await interaction.response.send_autocomplete(autofilled_quarters[:25]) await interaction.response.send_autocomplete(autofilled_quarters[:25])
+1 -1
View File
@@ -35,4 +35,4 @@ class BotPresence(commands.Cog, name="Bot Presence"):
with Session(engine) as session: with Session(engine) as session:
presence = session.scalar(select(Presences.Presence).order_by(func.random())) presence = session.scalar(select(Presences.Presence).order_by(func.random()))
await self.bot.change_presence(activity=nextcord.CustomActivity(name="Custom Status", state=presence)) await self.bot.change_presence(activity=nextcord.CustomActivity(presence))
+1 -1
View File
@@ -91,7 +91,7 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
# kandy's 1% chance # kandy's 1% chance
if random.randint(0, 99) == 0: # noqa: S311 isn't cryptography related. if random.randint(0, 99) == 0: # noqa: S311 isn't cryptography related.
birthday_list.append(239547993106284544) birthday_list.append(KANDY)
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC).date()
birthday = f"{today.month}/{today.day}" birthday = f"{today.month}/{today.day}"
+15 -10
View File
@@ -4,20 +4,25 @@ version = "0.1.0"
description = "Dakiny's Discord Bot." description = "Dakiny's Discord Bot."
authors = ["imcompiling <[email protected]>"] authors = ["imcompiling <[email protected]>"]
readme = "README.md" readme = "README.md"
package-mode = false
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.12"
gitpython = "3.1.43" gitpython = "^3.1.43"
fastapi = "0.111.0" fastapi = "^0.111.0"
hydra-core = "1.3.2" hydra-core = "^1.3.2"
nextcord = "2.4.2" nextcord = "^2.6.0"
pandas = "1.5.2" pandas = "^2.2.2"
python-dotenv = "1.0.0" python-dotenv = "^1.0.1"
pytz = "2022.7.1" pytz = "^2024.1"
sqlalchemy = "1.4.46" sqlalchemy = "^2.0.30"
aiofiles = "^23.2.1" aiofiles = "^23.2.1"
setuptools = "^70.0.0"
[tool.poetry.group.dev.dependencies]
ruff = "^0.4.5"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
@@ -59,7 +64,7 @@ line-length = 120
indent-width = 4 indent-width = 4
# Assume Python 3.8 # Assume Python 3.8
target-version = "py311" target-version = "py312"
[tool.ruff.lint] [tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
-1
View File
@@ -1 +0,0 @@
python-3.10.4
+24 -16
View File
@@ -26,6 +26,8 @@ NOMINEE_SPLITTER = "========================================\n\n"
REPO_DIR = "./dakiny-messages" REPO_DIR = "./dakiny-messages"
MESSAGES_DIR = f"{REPO_DIR}/messages" MESSAGES_DIR = f"{REPO_DIR}/messages"
TEMPLATES_DIR = f"{REPO_DIR}/message_templates" TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
MAX_DISCORD_MESSAGE_LENGTH = 2000
GUILDS = [1000247361639030804, 239758371358638080]
VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"]) VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
@@ -96,7 +98,7 @@ def get_current_election_cycle(config: DakinyConfig) -> int:
Returns: Returns:
int: cycle index for active election. None for no active cycle int: cycle index for active election. None for no active cycle
""" """
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC)
for cycle, election in enumerate(config.election_cycles): for cycle, election in enumerate(config.election_cycles):
if ( if (
@@ -119,7 +121,7 @@ def get_quarter_cycle(config: DakinyConfig) -> int:
Returns: Returns:
int: cycle index for current cycle. int: cycle index for current cycle.
""" """
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC)
for cycle, election in enumerate(config.election_cycles): for cycle, election in enumerate(config.election_cycles):
if ( if (
today.date() >= get_datetime(election.term_start).date() today.date() >= get_datetime(election.term_start).date()
@@ -135,7 +137,7 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
Creates a string that contains all nominees and their votes to be sent as a discord message. Creates a string that contains all nominees and their votes to be sent as a discord message.
Args: Args:
config (DakinyConfig): Daking config object config (DakinyConfig): Dakiny config object
guild (nextcord.Guild): The guild object. Used to get display names for sorting. guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns: Returns:
@@ -150,7 +152,7 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
for nominee in sorted( for nominee in sorted(
session.scalars(stmt), session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name, key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
): ):
votes[nominee.MemberId] = 0 votes[nominee.MemberId] = 0
@@ -171,7 +173,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
Creates a string that contains all high council nominees and their votes to be sent as a discord message. Creates a string that contains all high council nominees and their votes to be sent as a discord message.
Args: Args:
config (DakinyConfig): Daking config object config (DakinyConfig): Dakiny config object
guild (nextcord.Guild): The guild object. Used to get display names for sorting. guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns: Returns:
@@ -191,7 +193,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
for nominee in sorted( for nominee in sorted(
session.scalars(stmt), session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name, key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
): ):
cl_votes[nominee.MemberId] = 0 cl_votes[nominee.MemberId] = 0
@@ -209,7 +211,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
for nominee in sorted( for nominee in sorted(
session.scalars(stmt), session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name, key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
): ):
dm_votes[nominee.MemberId] = 0 dm_votes[nominee.MemberId] = 0
@@ -300,7 +302,7 @@ def decorate_dm_nomination(user_id: int, speech: str) -> str:
return msg return msg
def record_event_points(config: DakinyConfig, users: Sequence[str], is_member: bool, add_points: int) -> None: # noqa: FBT001 def record_event_points(config: DakinyConfig, users: Sequence[str], add_points: int, *, is_member: bool) -> None:
""" """
Adds event points to all users listed. Adds event points to all users listed.
@@ -310,8 +312,8 @@ def record_event_points(config: DakinyConfig, users: Sequence[str], is_member: b
Args: Args:
config (DakinyConfig): Dakiny config for getting points config (DakinyConfig): Dakiny config for getting points
users (Sequence[int]): users that participated and need points. users (Sequence[int]): users that participated and need points.
is_member (bool): whether the users are a discord member or not
add_points (int): number of points to add add_points (int): number of points to add
is_member (bool): whether the users are a discord member or not
""" """
quarter_name = get_quarter_name(config) quarter_name = get_quarter_name(config)
with Session(engine) as session: with Session(engine) as session:
@@ -372,7 +374,7 @@ def get_allowed_points(session: Session, points_limit: int, user: str, points: i
add_points = ( add_points = (
points points
if limit.Points + points <= points_limit if limit.Points + points <= points_limit
else (points_limit - limit.Points if points_limit - limit.Points > 0 else 0) else (max(0, points_limit - limit.Points))
) )
limit.Points += add_points limit.Points += add_points
@@ -410,8 +412,10 @@ async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[
for file_name in sorted_files: for file_name in sorted_files:
message_name = f"{dir_name}/{file_name}" message_name = f"{dir_name}/{file_name}"
file_path = f"{dir_path}/{file_name}" file_path = f"{dir_path}/{file_name}"
with aiofiles.open(file_path, encoding="utf8") as reader: async with aiofiles.open(file_path, encoding="utf8") as reader:
await edit_clan_message(session, channel, message_name, reader.read(2000)) await edit_clan_message(
session, channel, message_name, await reader.read(MAX_DISCORD_MESSAGE_LENGTH)
)
async def get_clan_message_channel(session: Session, channel_name: str, guild: nextcord.Guild) -> nextcord.TextChannel: async def get_clan_message_channel(session: Session, channel_name: str, guild: nextcord.Guild) -> nextcord.TextChannel:
@@ -508,7 +512,7 @@ async def edit_clan_message(
async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) -> None: async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) -> None:
""" """
Gets the winners of the elections and updates the perminant message that states the current council. Gets the winners of the elections and updates the permanent message that states the current council.
Args: Args:
guild (nextcord.Guild): Guild object that to use to get channel. guild (nextcord.Guild): Guild object that to use to get channel.
@@ -536,14 +540,18 @@ async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) ->
.where(CouncilMembers.Elected == true()) .where(CouncilMembers.Elected == true())
) )
council_member_ids = sorted(
session.scalars(stmt), key=lambda user_id: guild.get_member(user_id).display_name.lower()
)
council_members_strs = [ council_members_strs = [
f"<@{user_id}>" for user_id in session.scalars(stmt) if user_id not in (discord_manager, clan_leader) f"<@{user_id}>" for user_id in council_member_ids if user_id not in (discord_manager, clan_leader)
] ]
quarter = get_quarter_name(config) quarter = get_quarter_name(config)
with aiofiles.open(f"{TEMPLATES_DIR}/council_members.txt", encoding="utf8") as file: async with aiofiles.open(f"{TEMPLATES_DIR}/council_members.txt", encoding="utf8") as file:
message = file.read() message = await file.read()
message = message.format( message = message.format(
quarter=quarter, quarter=quarter,
council="\n".join(council_members_strs), council="\n".join(council_members_strs),