Compare commits

..
Author SHA1 Message Date
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
imcompiling 6500f68d86 Start working on files in cogs.
build_develop / build (push) Successful in 1m24s
2024-05-22 17:35:43 -04:00
imcompiling d9914529ca Fix Path().is_dir() usage.
build_latest / build (push) Successful in 1m24s
build_release / build (push) Successful in 17s
2024-05-22 17:30:01 -04:00
imcompiling 6cd82dfcc4 Fix ruff smells in classes dir and config.py. 2024-05-22 16:58:27 -04:00
imcompiling e485c119f6 Fix database_setup and main lint smells. 2024-05-22 16:33:09 -04:00
26 changed files with 408 additions and 393 deletions
+20 -27
View File
@@ -1,9 +1,8 @@
""" """Contains the election cycle class."""
Contains the election cycle class
"""
import collections import collections
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from pathlib import Path
import nextcord import nextcord
from classes.nominate_highcouncil_views import NominateHighCouncilView from classes.nominate_highcouncil_views import NominateHighCouncilView
@@ -29,9 +28,13 @@ from utils import (
get_election_name, get_election_name,
) )
REQUIRED_VOTES_TO_BE_ELECTED = 3
class ElectionCycle: class ElectionCycle:
""" """
ElectionCycle class.
This class represents all the dates required for an election cycle along with all the functions needed to operate This class represents all the dates required for an election cycle along with all the functions needed to operate
the cycle. the cycle.
""" """
@@ -67,7 +70,7 @@ class ElectionCycle:
list[str]: A list of message contents in the correct order. Must be a string because the message is too list[str]: A list of message contents in the correct order. Must be a string because the message is too
long. long.
""" """
with open(f"{TEMPLATES_DIR}/election_announcement.txt", encoding="utf8") as file: with Path(f"{TEMPLATES_DIR}/election_announcement.txt", encoding="utf8").open() as file:
message = file.read() message = file.read()
next_election = get_datetime( next_election = get_datetime(
@@ -95,7 +98,7 @@ class ElectionCycle:
Returns: Returns:
str: Message to send. str: Message to send.
""" """
with open(f"{TEMPLATES_DIR}/highcouncil_election.txt", encoding="utf8") as file: with Path(f"{TEMPLATES_DIR}/highcouncil_election.txt", encoding="utf8").open() as file:
message = file.read() message = file.read()
return message.format( return message.format(
@@ -113,7 +116,7 @@ class ElectionCycle:
Returns: Returns:
str: Message to send. str: Message to send.
""" """
with open(f"{TEMPLATES_DIR}/highcouncil_vote.txt", encoding="utf8") as file: with Path(f"{TEMPLATES_DIR}/highcouncil_vote.txt", encoding="utf8").open() as file:
message = file.read() message = file.read()
return message.format( return message.format(
@@ -128,7 +131,7 @@ class ElectionCycle:
Returns: Returns:
str: Message to send str: Message to send
""" """
with open(f"{TEMPLATES_DIR}/vote_start.txt", encoding="utf8") as file: with Path(f"{TEMPLATES_DIR}/vote_start.txt", encoding="utf8").open() as file:
message = file.read() message = file.read()
vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end) vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end)
@@ -139,10 +142,8 @@ 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)
"""
today = datetime.now(tz=UTC).date()
# 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():
@@ -159,9 +160,7 @@ class ElectionCycle:
await self.term_start_action() await self.term_start_action()
async def election_start_action(self) -> None: async def election_start_action(self) -> None:
""" """Performs required actions for when the election cycle starts."""
Performs required actions for when the election cycle starts.
"""
# create new channel for election # create new channel for election
parent: nextcord.TextChannel = self.guild.get_channel(self.config.params.voting_parent_id) parent: nextcord.TextChannel = self.guild.get_channel(self.config.params.voting_parent_id)
quarter_name: str = get_election_name(self.config) quarter_name: str = get_election_name(self.config)
@@ -193,6 +192,8 @@ class ElectionCycle:
async def voting_start_action(self) -> None: async def voting_start_action(self) -> None:
""" """
Actions performed when starting a vote.
Sends message informing members that voting has started. Sends message informing members that voting has started.
Removes write permission from the general clan member role. Removes write permission from the general clan member role.
""" """
@@ -217,9 +218,7 @@ class ElectionCycle:
await nominate_msg.edit(view=None) await nominate_msg.edit(view=None)
async def voting_end_action(self) -> None: async def voting_end_action(self) -> None:
""" """Performs required actions for when voting time has ended."""
Performs required actions for when voting time has ended.
"""
message = self.build_highcouncil_election_announce() message = self.build_highcouncil_election_announce()
council_elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id) council_elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id)
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId) election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
@@ -250,7 +249,7 @@ class ElectionCycle:
vote_count = collections.Counter(session.scalars(stmt)) vote_count = collections.Counter(session.scalars(stmt))
for user_id, votes in vote_count.items(): for user_id, votes in vote_count.items():
if votes >= 3: if votes >= REQUIRED_VOTES_TO_BE_ELECTED:
stmt = ( stmt = (
select(CouncilMembers) select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_name) .where(CouncilMembers.ElectionCycle == election_name)
@@ -268,9 +267,7 @@ class ElectionCycle:
) )
async def highcouncil_election_start_action(self) -> None: async def highcouncil_election_start_action(self) -> None:
""" """Performs required actions for when the highcouncil is elected."""
Performs required actions for when the highcouncil is elected.
"""
message = self.build_highcouncil_vote_start() message = self.build_highcouncil_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId) election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
await election_thread.send(message) await election_thread.send(message)
@@ -292,9 +289,7 @@ class ElectionCycle:
await nomination_msg.edit(view=None) await nomination_msg.edit(view=None)
async def term_start_action(self) -> None: async def term_start_action(self) -> None:
""" """Marks the election as over."""
Marks the election as over.
"""
self.election_over = True self.election_over = True
elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id) elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id)
@@ -394,9 +389,7 @@ class ElectionCycle:
return session.scalar(stmt) return session.scalar(stmt)
async def election_reminder(self) -> None: async def election_reminder(self) -> None:
""" """Sends a message to general chat reminder clan about election."""
Sends a message to general chat reminder clan about election.
"""
general: nextcord.TextChannel = self.guild.get_channel(self.config.params.general_channel_id) general: nextcord.TextChannel = self.guild.get_channel(self.config.params.general_channel_id)
message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over " message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over "
+18 -15
View File
@@ -1,14 +1,11 @@
""" """Contains various views to be used in the high council nomination process."""
Contains various views to be used in the high council nomination process.
"""
import nextcord import nextcord
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from nextcord.interactions import Interaction from nextcord.interactions import Interaction
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import ( from utils import (
NOMINEE_SPLITTER, NOMINEE_SPLITTER,
decorate_cl_nomination, decorate_cl_nomination,
@@ -19,14 +16,16 @@ from utils import (
class NominateDMModal(nextcord.ui.Modal): class NominateDMModal(nextcord.ui.Modal):
""" """
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. Modal for nominating oneself.
User writes a speech and then the callback sends the speech into the correct channel.
""" """
def __init__( def __init__(
self, self,
config: DakinyConfig, config: DakinyConfig,
usr_id: int, usr_id: int,
usr_nominated: bool, usr_nominated: bool, # noqa: FBT001
speech_content: str, speech_content: str,
): ):
super().__init__( super().__init__(
@@ -79,14 +78,16 @@ class NominateDMModal(nextcord.ui.Modal):
class NominateCLModal(nextcord.ui.Modal): class NominateCLModal(nextcord.ui.Modal):
""" """
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. Modal for nominating oneself.
User writes a speech and then the callback sends the speech into the correct channel.
""" """
def __init__( def __init__(
self, self,
config: DakinyConfig, config: DakinyConfig,
usr_id: int, usr_id: int,
usr_nominated: bool, usr_nominated: bool, # noqa: FBT001
speech_content: str, speech_content: str,
): ):
super().__init__( super().__init__(
@@ -138,9 +139,7 @@ class NominateCLModal(nextcord.ui.Modal):
class NominateHighCouncilView(nextcord.ui.View): class NominateHighCouncilView(nextcord.ui.View):
""" """The buttons to press to start your High Council nomination."""
The buttons to press to start your High Council nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild): def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None) super().__init__(timeout=None)
@@ -152,8 +151,10 @@ class NominateHighCouncilView(nextcord.ui.View):
style=nextcord.ButtonStyle.primary, style=nextcord.ButtonStyle.primary,
custom_id="nominate-dm", custom_id="nominate-dm",
) )
async def nominate_dm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: async def nominate_dm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button for nominating oneself for Discord Manager.
Gathers previous information about the user if they have already nominated themselves, then launches Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech. the nomination modal to get their speech.
@@ -195,8 +196,10 @@ class NominateHighCouncilView(nextcord.ui.View):
style=nextcord.ButtonStyle.primary, style=nextcord.ButtonStyle.primary,
custom_id="nominate-cl", custom_id="nominate-cl",
) )
async def nominate_cl(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: async def nominate_cl(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button for nominating oneself for Clan Leader.
Gathers previous information about the user if they have already nominated themselves, then launches Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech. the nomination modal to get their speech.
+11 -12
View File
@@ -1,27 +1,26 @@
""" """Contains various views to be used in the nomination process."""
Contains various views to be used in the nomination process.
"""
import nextcord import nextcord
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from nextcord.interactions import Interaction from nextcord.interactions import Interaction
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name
class NominateModal(nextcord.ui.Modal): class NominateModal(nextcord.ui.Modal):
""" """
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. Modal for nominating oneself.
User writes a speech and then the callback sends the speech into the correct channel.
""" """
def __init__( def __init__(
self, self,
config: DakinyConfig, config: DakinyConfig,
usr_id: int, usr_id: int,
usr_nominated: bool, usr_nominated: bool, # noqa: FBT001
speech_content: str, speech_content: str,
): ):
super().__init__( super().__init__(
@@ -78,9 +77,7 @@ class NominateModal(nextcord.ui.Modal):
class NominateView(nextcord.ui.View): class NominateView(nextcord.ui.View):
""" """The button to press to start your nomination."""
The button to press to start your nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild): def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None) super().__init__(timeout=None)
@@ -92,8 +89,10 @@ class NominateView(nextcord.ui.View):
style=nextcord.ButtonStyle.primary, style=nextcord.ButtonStyle.primary,
custom_id="nominate", custom_id="nominate",
) )
async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button to nominate onself for Clan Council.
Gathers previous information about the user if they have already nominated themselves, then launches Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech. the nomination modal to get their speech.
+3
View File
@@ -2,6 +2,9 @@
# Extend the `pyproject.toml` file in the parent directory... # Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml" extend = "../pyproject.toml"
# Exclude a variety of commonly ignored directories.
exclude = ["gitea_webhook.py"]
[tool.ruff.lint] [tool.ruff.lint]
# ignore implicit-namespace-package in this directory. # ignore implicit-namespace-package in this directory.
ignore = ["INP001"] ignore = ["INP001"]
+26 -23
View File
@@ -1,14 +1,6 @@
""" """Contains various views to be used while voting for high council. Controls the logic when performing the vote."""
Contains various views to be used while voting for high council. Controls the logic when performing the vote.
"""
import nextcord import nextcord
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from config import DakinyConfig from config import DakinyConfig
from database_setup.sql_data_classes import ( from database_setup.sql_data_classes import (
CouncilMembers, CouncilMembers,
@@ -16,10 +8,16 @@ from database_setup.sql_data_classes import (
Votes, Votes,
engine, engine,
) )
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from utils import VoteType, build_highcouncil_votes_str, get_election_name from utils import VoteType, build_highcouncil_votes_str, get_election_name
class VoteSelect(nextcord.ui.StringSelect): class VoteSelect(nextcord.ui.StringSelect):
"""Vote view string select."""
def __init__(self, options: list["SelectOption"], config: DakinyConfig, election_type: str) -> None: def __init__(self, options: list["SelectOption"], config: DakinyConfig, election_type: str) -> None:
super().__init__( super().__init__(
placeholder="Choose 1", placeholder="Choose 1",
@@ -30,7 +28,16 @@ class VoteSelect(nextcord.ui.StringSelect):
self.config = config self.config = config
self.election_type = election_type self.election_type = election_type
async def callback(self, interaction: Interaction): async def callback(self, interaction: Interaction) -> None:
"""
Callback override for the StringSelect.
Submits the High Council vote to database if a user is selected. Handles adding vs editing a vote.
Returns message informing user that vote has been recorded.
Args:
interaction (Interaction): Interaction with this callback.
"""
election_name: str = get_election_name(self.config) election_name: str = get_election_name(self.config)
with Session(engine) as session: with Session(engine) as session:
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where( stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
@@ -38,10 +45,7 @@ class VoteSelect(nextcord.ui.StringSelect):
) )
vote_msg_id = session.scalar(stmt) vote_msg_id = session.scalar(stmt)
if self.values[0] == "Abstain": voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
voted_for = None
else:
voted_for = int(self.values[0])
new_vote = Votes( new_vote = Votes(
ElectionCycle=election_name, ElectionCycle=election_name,
@@ -71,18 +75,13 @@ class VoteSelect(nextcord.ui.StringSelect):
vote_msg: nextcord.Message = await interaction.channel.fetch_message(vote_msg_id) vote_msg: nextcord.Message = await interaction.channel.fetch_message(vote_msg_id)
await vote_msg.edit(content=vote_message_str) await vote_msg.edit(content=vote_message_str)
if voted_for is not None: msg = f"You have voted for <@{voted_for}>" if voted_for is not None else "You have abstained from voting."
msg = f"You have voted for <@{voted_for}>"
else:
msg = "You have abstained from voting."
await interaction.edit(content=msg, view=None) await interaction.edit(content=msg, view=None)
class VoteHighcouncilView(nextcord.ui.View): class VoteHighcouncilView(nextcord.ui.View):
""" """The button to press to start voting for HighCouncil."""
The button to press to start voting for HighCouncil.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild): def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None) super().__init__(timeout=None)
@@ -94,8 +93,10 @@ class VoteHighcouncilView(nextcord.ui.View):
style=nextcord.ButtonStyle.primary, style=nextcord.ButtonStyle.primary,
custom_id="vote-cl", custom_id="vote-cl",
) )
async def vote_cl(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: async def vote_cl(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button to vote for Clan Leader.
Enforces only council elect vote. Gets election cycle, creates voting Enforces only council elect vote. Gets election cycle, creates voting
options for clan leader (checking if user has voted already), options for clan leader (checking if user has voted already),
then sends the voting view. then sends the voting view.
@@ -164,8 +165,10 @@ class VoteHighcouncilView(nextcord.ui.View):
style=nextcord.ButtonStyle.primary, style=nextcord.ButtonStyle.primary,
custom_id="vote-dm", custom_id="vote-dm",
) )
async def vote_dm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: async def vote_dm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button to vote for Discord Manager.
Enforces only council elect vote. Gets election cycle, creates voting Enforces only council elect vote. Gets election cycle, creates voting
options for discord manager (checking if user has voted already), options for discord manager (checking if user has voted already),
then sends the voting view. then sends the voting view.
+24 -21
View File
@@ -1,26 +1,30 @@
""" """File contains logic for creating a webhook listener, pulling from git, and updating all messages."""
File contains logic for creating a webhook listener, pulling from git, and updating all messages.
"""
import os from pathlib import Path
from typing import TYPE_CHECKING
import nextcord
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from git import Repo from git import Repo
from nextcord.ext import commands from nextcord.ext import commands
from classes.gitea_webhook import GiteaWebhook from classes.gitea_webhook import GiteaWebhook
from config import DakinyConfig
from utils import REPO_DIR, edit_clan_council_info, update_clan_messages from utils import REPO_DIR, edit_clan_council_info, update_clan_messages
if TYPE_CHECKING:
import nextcord
from config import DakinyConfig
app = FastAPI() app = FastAPI()
REPO_URL = "https://gitea.cheezers.org/imcompiling/dakiny-messages.git" REPO_URL = "https://gitea.cheezers.org/imcompiling/dakiny-messages.git"
class ClanMessages(commands.Cog, name="Clan Messages"): class ClanMessages(commands.Cog, name="Clan Messages"):
""" """
Cog to be loaded into the bot. Used to be able to have a fastapi post listener and access to the bot at Cog to be loaded into the bot.
Used to be able to have a fastapi post listener and access to the bot at
the same time. the same time.
""" """
@@ -30,23 +34,24 @@ class ClanMessages(commands.Cog, name="Clan Messages"):
self.repo_setup() self.repo_setup()
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): async def on_ready(self) -> None:
""" """Triggers when the bot is in ready state. Adds the webhook listener to FastAPI and serves."""
Triggers when the bot is in ready state. Adds the webhook listener to FastAPI and serves.
"""
app.add_api_route("/messages/", self.update_messages, methods=["POST"]) app.add_api_route("/messages/", self.update_messages, methods=["POST"])
config = uvicorn.Config(app=app, host="0.0.0.0", port=5000, log_level="info") # Bind to all interfaces, don't know the docker interface ahead of time.
config = uvicorn.Config(app=app, host="0.0.0.0", port=5000, log_level="info") # noqa: S104
server = uvicorn.Server(config) server = uvicorn.Server(config)
await server.serve() await server.serve()
async def update_messages(self, hook_info: GiteaWebhook): async def update_messages(self, hook_info: GiteaWebhook) -> dict:
""" """
Triggered by webhook.
Pulls from git and then calls method to update all the discord messages as described by the Pulls from git and then calls method to update all the discord messages as described by the
files in the git repo. files in the git repo.
""" """
# Get all modified/added files # Get all modified/added files
modified_files: list[str] = [ modified_files: list[str] = [
file for list in [commit.modified + commit.added for commit in hook_info.commits] for file in list file for files in [commit.modified + commit.added for commit in hook_info.commits] for file in files
] ]
template_updates = {file.split("/")[1] for file in modified_files if "message_templates" in file} template_updates = {file.split("/")[1] for file in modified_files if "message_templates" in file}
@@ -63,12 +68,10 @@ class ClanMessages(commands.Cog, name="Clan Messages"):
return {"message": "Successfully updated all discord messages."} return {"message": "Successfully updated all discord messages."}
def repo_setup(self): def repo_setup(self) -> None:
""" """Checks if the repo exists and creates repo object, clones if it does not."""
Checks if the repo exists and creates repo object, clones if it does not.
"""
# check if repo has been cloned. open repo object # check if repo has been cloned. open repo object
if os.path.isdir(REPO_DIR): if Path(REPO_DIR).is_dir():
self.repo = Repo(REPO_DIR) self.repo = Repo(REPO_DIR)
else: else:
self.repo = Repo.clone_from(REPO_URL, REPO_DIR) self.repo = Repo.clone_from(REPO_URL, REPO_DIR)
@@ -85,6 +88,6 @@ class ClanMessages(commands.Cog, name="Clan Messages"):
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
"""Adds function to bot""" """Adds function to bot."""
bot.add_cog(ClanMessages(bot)) bot.add_cog(ClanMessages(bot))
+5 -9
View File
@@ -1,6 +1,4 @@
""" """Cog loader. Adds all command cogs to the bot."""
Cog loader. Adds all command cogs to the bot.
"""
from nextcord.ext import commands from nextcord.ext import commands
@@ -8,21 +6,19 @@ 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.view import ViewCog from cogs.commands.view import ViewCog
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
""" """Adds all command classes to bot."""
Adds all command classes to bot.
"""
bot.add_cog(RecordCog(bot)) bot.add_cog(RecordCog(bot))
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(TestCog(bot)) bot.add_cog(TestCog(bot))
bot.add_cog(ViewCog()) bot.add_cog(ViewCog())
bot.add_cog(setCog()) bot.add_cog(SetCog())
+44 -39
View File
@@ -1,6 +1,4 @@
""" """Contained commands and views for record commands."""
Contained commands and views for record commands.
"""
import nextcord import nextcord
from discord import TextInputStyle from discord import TextInputStyle
@@ -11,22 +9,16 @@ from utils import record_event_points
class RecordCog(commands.Cog, name="Record Commands"): class RecordCog(commands.Cog, name="Record Commands"):
""" """Class holds record commands."""
Class holds record commands.
"""
def __init__(self, bot) -> None: def __init__(self, bot: commands.Bot) -> None:
""" """Default constructor for RecordCog class. Does nothing."""
Default constructor for RecordCog class. Does nothing.
"""
super().__init__() super().__init__()
self.bot: commands.Bot = bot self.bot: commands.Bot = bot
@nextcord.slash_command() @nextcord.slash_command()
async def record(self, interaction: nextcord.Interaction): async def record(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for record. Never called."""
Parent slash command for record. Never called.
"""
@record.subcommand(description="Record an event and its participants.") @record.subcommand(description="Record an event and its participants.")
async def event( async def event(
@@ -37,8 +29,10 @@ class RecordCog(commands.Cog, name="Record Commands"):
required=False, required=False,
description="An alternative host if you are recording event for someone else.", description="An alternative host if you are recording event for someone else.",
), ),
): ) -> None:
""" """
Create and send recording event.
This command creates the base event recording embed and sends a message with it and the EventView This command creates the base event recording embed and sends a message with it and the EventView
for gathering more information on the event. for gathering more information on the event.
@@ -73,7 +67,9 @@ class RecordCog(commands.Cog, name="Record Commands"):
class EventView(nextcord.ui.View): class EventView(nextcord.ui.View):
""" """
Class handles gathering information on an event. Has buttons to edit the information, cancel, and submit. Class handles gathering information on an event.
Has buttons to edit the information, cancel, and submit.
Also contains a UserSelect object to pick discord members. Also contains a UserSelect object to pick discord members.
""" """
@@ -89,33 +85,31 @@ class EventView(nextcord.ui.View):
self.add_item(self.event_participants) self.add_item(self.event_participants)
@nextcord.ui.button(label="Cancel", style=nextcord.ButtonStyle.red, row=1) @nextcord.ui.button(label="Cancel", style=nextcord.ButtonStyle.red, row=1)
async def cancel(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def cancel(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """Cancels event submission. Removes embed and view."""
Cancels event submission. Removes embed and view.
"""
await interaction.response.edit_message("Canceled", ephemeral=True, embed=None, view=None) await interaction.response.edit_message("Canceled", ephemeral=True, embed=None, view=None)
self.stop() self.stop()
@nextcord.ui.button(label="Edit", style=nextcord.ButtonStyle.blurple, row=1) @nextcord.ui.button(label="Edit", style=nextcord.ButtonStyle.blurple, row=1)
async def edit(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def edit(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """Calls modal to update the text input information of the event."""
Calls modal to update the text input information of the event.
"""
await interaction.response.send_modal(ParticipantModal(interaction.message.embeds[0])) await interaction.response.send_modal(ParticipantModal(interaction.message.embeds[0]))
@nextcord.ui.button(label="Confirm", style=nextcord.ButtonStyle.green, row=1) @nextcord.ui.button(label="Confirm", style=nextcord.ButtonStyle.green, row=1)
async def confirm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def confirm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Submits the event embed to the event channel. Removes view and embed from recording message. Submits the event embed to the event channel.
Removes view and embed from recording message.
Sends point information to database. Sends point information to database.
""" """
discord_users = self.event_participants.values.members discord_users = self.event_participants.values.members # noqa: PD011 not a pandas object
if discord_users: if discord_users:
discord_users_list: list[str] = [str(user.id) for user in discord_users if user.id is not self.host_id] discord_users_list: list[str] = [str(user.id) for user in discord_users if user.id is not self.host_id]
record_event_points( record_event_points(
self.config, self.config,
discord_users_list, discord_users_list,
True, True, # noqa: FBT003
self.config.params.event_participate_points, self.config.params.event_participate_points,
) )
@@ -125,11 +119,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])
@@ -140,12 +134,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,
@@ -175,7 +170,13 @@ class ParticipantModal(nextcord.ui.Modal):
self.add_item(self.description) self.add_item(self.description)
self.add_item(self.participants) self.add_item(self.participants)
async def callback(self, interaction: nextcord.Interaction): 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)
@@ -184,9 +185,7 @@ class ParticipantModal(nextcord.ui.Modal):
class ParticipantSelect(nextcord.ui.UserSelect): class ParticipantSelect(nextcord.ui.UserSelect):
""" """A selection drop down for discord members. Updates the embed with all users selected."""
A selection drop down for discord members. Updates the embed with all users selected.
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
@@ -196,7 +195,13 @@ class ParticipantSelect(nextcord.ui.UserSelect):
row=0, row=0,
) )
async def callback(self, interaction: nextcord.Interaction): 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 -38
View File
@@ -1,51 +1,49 @@
""" """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 get_current_election_cycle from utils import get_current_election_cycle
if TYPE_CHECKING:
from config import DakinyConfig
def is_me():
"""
predicate that makes sure only I can run the test commands.
"""
def predicate(interaction: nextcord.Interaction): ME = 206602713511886849
return interaction.user.id == 206602713511886849 GUILDS = [1000247361639030804]
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 +52,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 +70,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 +88,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 +106,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 +124,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 +142,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 +160,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)
+36 -29
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,38 +33,49 @@ 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) non_members = unsorted_all.get_group(name=False)
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 for user_id in user_ids]
),
) )
sorted_non_members = non_members.sort_values(by="UserId") 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)
@@ -83,7 +86,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 +100,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 +134,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 +145,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 +167,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 +184,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 +207,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])
+3 -7
View File
@@ -1,6 +1,4 @@
""" """Initializing code for all loops."""
Initializing code for all loops.
"""
from nextcord.ext import commands from nextcord.ext import commands
@@ -8,9 +6,7 @@ from cogs.loops.bot_presence import BotPresence
from cogs.loops.daily_loop import DailyLoop from cogs.loops.daily_loop import DailyLoop
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
""" """Adds functions to bot."""
Adds functions to bot.
"""
bot.add_cog(BotPresence(bot)) bot.add_cog(BotPresence(bot))
bot.add_cog(DailyLoop(bot)) bot.add_cog(DailyLoop(bot))
+6 -9
View File
@@ -1,6 +1,4 @@
""" """Functions for controlling the bot's presence. Also contains the loop."""
Functions for controlling the bot's presence. Also contains the loop.
"""
import nextcord import nextcord
from nextcord.ext import commands, tasks from nextcord.ext import commands, tasks
@@ -17,23 +15,22 @@ class BotPresence(commands.Cog, name="Bot Presence"):
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument async def on_guild_join(self, guild: nextcord.Guild) -> None: # noqa: ARG002
"""Listener for Guild Join. Updated presence on guild join.""" """Listener for Guild Join. Updated presence on guild join."""
await self.change_presence() await self.change_presence()
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): async def on_ready(self) -> None:
"""Starts the loop when bot is ready.""" """Starts the loop when bot is ready."""
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop. self.periodic_change_presence.start()
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=5) @tasks.loop(minutes=5)
async def periodic_change_presence(self): async def periodic_change_presence(self) -> None:
"""Loops every 5 minutes and changes presence.""" """Loops every 5 minutes and changes presence."""
await self.change_presence() await self.change_presence()
async def change_presence(self): async def change_presence(self) -> None:
"""Randomly chooses a presence and updates bot's presence.""" """Randomly chooses a presence and updates bot's 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()))
+27 -25
View File
@@ -1,27 +1,29 @@
""" """Defines the daily loop that runs the Dakiny bot functions."""
Defines the daily loop that runs the Dakiny bot functions.
"""
import random import random
from datetime import UTC, datetime, time from datetime import UTC, datetime, time
from typing import TYPE_CHECKING
import nextcord
from nextcord.ext import commands, tasks from nextcord.ext import commands, tasks
from pytz import timezone from pytz import timezone
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from classes.election_cycle import ElectionCycle from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from database_setup.sql_data_classes import UserBirthday, engine from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list from main import birthday_list
from utils import get_current_election_cycle from utils import get_current_election_cycle
if TYPE_CHECKING:
import nextcord
from config import DakinyConfig
KANDY = 239547993106284544
class DailyLoop(commands.Cog, name="Daily Loop"): class DailyLoop(commands.Cog, name="Daily Loop"):
""" """Class for performing daily functions."""
Class for performing daily functions.
"""
def __init__(self, bot: commands.Bot) -> None: def __init__(self, bot: commands.Bot) -> None:
self.bot = bot self.bot = bot
@@ -31,10 +33,12 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
# pylint doesn't recognize that daily_loop is subclassed from tasks.loop. # pylint doesn't recognize that daily_loop is subclassed from tasks.loop.
self.daily_loop.start() # pylint: disable=no-member self.daily_loop.start() # pylint: disable=no-member
@tasks.loop(time=time(hour=0, tzinfo=timezone("GMT"))) @tasks.loop(time=time(hour=0, tzinfo=timezone("UTC")))
async def daily_loop(self): async def daily_loop(self) -> None:
""" """
Runs daily. Checks if the current election is None. If so, checks if there is an active election. If there is Runs daily.
Checks if the current election is None. If so, checks if there is an active election. If there is
an active election, runs the check_date() function for the election to perform actions. Finally checks if the an active election, runs the check_date() function for the election to perform actions. Finally checks if the
election is over to delete the current election object. election is over to delete the current election object.
""" """
@@ -48,18 +52,16 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
self.update_birthdays() self.update_birthdays()
if 239547993106284544 in birthday_list: if KANDY in birthday_list:
# inform kandy that it is her "birthday" # inform kandy that it is her "birthday"
kandy: nextcord.User = await self.bot.fetch_user(239547993106284544) kandy: nextcord.User = await self.bot.fetch_user(KANDY)
await kandy.send('Happy "Birthday"!') await kandy.send('Happy "Birthday"!')
await self.cakedays() await self.cakedays()
@daily_loop.before_loop @daily_loop.before_loop
async def before_loop(self): async def before_loop(self) -> None:
""" """Actions run before the loop starts. Want to wait for bot to be ready and get the current cycle."""
Actions run before the loop starts. Want to wait for bot to be ready and get the current cycle.
"""
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
# Bot needs to know guilds to get the current cycle. # Bot needs to know guilds to get the current cycle.
@@ -67,8 +69,9 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
def get_election_cycle(self) -> ElectionCycle: def get_election_cycle(self) -> ElectionCycle:
""" """
Creates an object for the current election cycle if one is active. An election is considered active Creates an object for the current election cycle if one is active.
between the election start and the term start.
An election is considered active between the election start and the term start.
Returns: Returns:
ElectionCycle: Current election cycle, None if there is no election happening. ElectionCycle: Current election cycle, None if there is no election happening.
@@ -83,13 +86,11 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id)) return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
def update_birthdays(self) -> None: def update_birthdays(self) -> None:
""" """Goes through database to collect all birthdays that are today. Adds user's id to birthday list."""
Goes through database to collect all birthdays that are today. Adds user's id to birthday list.
"""
birthday_list.clear() birthday_list.clear()
# kandy's 1% chance # kandy's 1% chance
if random.randint(0, 99) == 0: if random.randint(0, 99) == 0: # noqa: S311 isn't cryptography related.
birthday_list.append(239547993106284544) birthday_list.append(239547993106284544)
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC).date()
@@ -102,8 +103,9 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
async def cakedays(self) -> None: async def cakedays(self) -> None:
""" """
Loops through the guild's humans checking for joined_at dates that match today's date. Sends a message Loops through the guild's humans checking for joined_at dates that match today's date.
saying happy cakeday.
Sends a message saying happy cakeday.
""" """
today = datetime.now(tz=UTC).date() today = datetime.now(tz=UTC).date()
guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id) guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id)
+4 -6
View File
@@ -1,6 +1,4 @@
""" """Defines cog for on message listener."""
Defines cog for on message listener.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
@@ -15,7 +13,7 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: nextcord.Message): async def on_message(self, message: nextcord.Message) -> None:
"""Listener for message creation. Adds score and attachments to submission.""" """Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot. # don't want to process any messages from bot.
if message.author.id == self.bot.user.id: if message.author.id == self.bot.user.id:
@@ -26,8 +24,8 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
"""Adds function to bot""" """Adds function to bot."""
bot.add_cog(SubmissionListener(bot)) bot.add_cog(SubmissionListener(bot))
+4 -12
View File
@@ -1,15 +1,11 @@
""" """File contains classes defining the Hydra configuraton structure."""
File contains classes defining the Hydra configuraton structure.
"""
from dataclasses import dataclass from dataclasses import dataclass
@dataclass @dataclass
class Params: class Params:
""" """Class defines the various configuration parameters."""
Class defines the various configuration parameters.
"""
admin_channel_id: int # might want to rename this... admin_channel_id: int # might want to rename this...
announcement_category_id: int announcement_category_id: int
@@ -32,9 +28,7 @@ class Params:
@dataclass @dataclass
class ElectionCycle: class ElectionCycle:
""" """Class defines relevant election dates."""
Class defines relevant election dates.
"""
election_start: str election_start: str
highcouncil_election_announce: str highcouncil_election_announce: str
@@ -49,9 +43,7 @@ class ElectionCycle:
@dataclass @dataclass
class DakinyConfig: class DakinyConfig:
""" """Class defines all configs required for Dakiny Bot."""
Class defines all configs required for Dakiny Bot.
"""
election_cycles: list[ElectionCycle] election_cycles: list[ElectionCycle]
params: Params params: Params
+4 -6
View File
@@ -1,15 +1,13 @@
""" """Small script to create the database if it doesn't already exist."""
Small script to create the database if it doesn't already exist.
"""
import sqlite3 import sqlite3
from os.path import exists from pathlib import Path
if not exists("database/dakiny.db"): if not Path("database/dakiny.db").exists():
with sqlite3.connect("database/dakiny.db") as con: with sqlite3.connect("database/dakiny.db") as con:
cur = con.cursor() cur = con.cursor()
with open("database_setup/database.sql", encoding="utf-8") as file: with Path("database_setup/database.sql", encoding="utf-8").open() as file:
database_script = file.read() database_script = file.read()
cur.executescript(database_script) cur.executescript(database_script)
con.commit() con.commit()
+7
View File
@@ -0,0 +1,7 @@
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml"
[tool.ruff.lint]
# ignore implicit-namespace-package in this directory.
ignore = ["INP001"]
+1 -3
View File
@@ -1,6 +1,4 @@
""" """Simple file to set up the sqlalchemy connection to database."""
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
+14 -9
View File
@@ -1,3 +1,5 @@
"""Main file for bot."""
import logging import logging
import os import os
import sys import sys
@@ -27,7 +29,13 @@ logger.addHandler(handler)
@hydra.main(config_path="conf", config_name="config") @hydra.main(config_path="conf", config_name="config")
def main(cfg: DakinyConfig): def main(cfg: DakinyConfig) -> None:
"""
Builds and runs bot.
Args:
cfg (DakinyConfig): Passed in hydra configuration.
"""
# Allows privledged intents for monitoring members joining, roles editing, and role assignments # Allows privledged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well # These need to be enabled in the developer portal as well
intents = nextcord.Intents.default() intents = nextcord.Intents.default()
@@ -35,9 +43,6 @@ def main(cfg: DakinyConfig):
# Required in order to read messages (eg. prefix commands) # Required in order to read messages (eg. prefix commands)
intents.message_content = True intents.message_content = True
# To enable the guilds priveleged intent:
# intents.guilds = True
# To enable the members priveleged intent: # To enable the members priveleged intent:
intents.members = True intents.members = True
@@ -49,15 +54,15 @@ def main(cfg: DakinyConfig):
bot.load_extension(f"cogs.{folder}") bot.load_extension(f"cogs.{folder}")
@bot.listen() @bot.listen()
async def on_ready(): async def on_ready() -> None:
"""When Discord is connected""" """When Discord is connected."""
assert bot.user is not None assert bot.user is not None # noqa: S101 documentation does this
create_database # pylint: disable=pointless-statement create_database # noqa: B018
bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!") print(f"{bot.user.name} has connected to Discord!") # noqa: T201 TODO make logging to print my stuff to console
# Run Discord bot # Run Discord bot
bot.run(bot.config.params.discord_token) bot.run(bot.config.params.discord_token)
+18 -11
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"
@@ -51,6 +56,7 @@ exclude = [
"node_modules", "node_modules",
"site-packages", "site-packages",
"venv", "venv",
"dakiny-messages", # Don't need to lint this git repo.
] ]
# Same as Black. # Same as Black.
@@ -58,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.
@@ -67,9 +73,10 @@ target-version = "py311"
select = ["ALL"] select = ["ALL"]
ignore = [ ignore = [
"ANN101", "ANN101",
"B008", # I build SlashOptions as arguments.
"COM812", "COM812",
"COM819", "COM819",
"D107", # don't want docstrings on init functions "D107", # don't want docstrings on init functions
"D203", "D203",
"D206", "D206",
"D212", "D212",
-1
View File
@@ -1 +0,0 @@
python-3.10.4
+12 -9
View File
@@ -26,6 +26,7 @@ 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
VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"]) VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
@@ -96,7 +97,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 +120,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()
@@ -300,7 +301,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 +311,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:
@@ -396,7 +397,7 @@ async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[
guild (nextcord.Guild): Guild object that all channels and messages belong to. guild (nextcord.Guild): Guild object that all channels and messages belong to.
channel_updates: (Sequence[str]): a sequence of the channels to update. channel_updates: (Sequence[str]): a sequence of the channels to update.
""" """
dirs = [dir_name for dir_name in channel_updates if Path.is_dir(f"{MESSAGES_DIR}/{dir_name}")] dirs = [dir_name for dir_name in channel_updates if Path(f"{MESSAGES_DIR}/{dir_name}").is_dir()]
with Session(engine) as session: with Session(engine) as session:
for dir_name in dirs: for dir_name in dirs:
@@ -410,8 +411,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:
@@ -542,8 +545,8 @@ async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) ->
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),