Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f1e4ad20d | ||
|
|
354823ca1f | ||
|
|
45f17e09d3 | ||
|
|
530ca5b172 | ||
|
|
3dfdc10334 | ||
|
|
6c9353990c | ||
|
|
5412ab14a3 | ||
|
|
16cdc27bdb | ||
|
|
086f8b50cd | ||
|
|
a6a73cb46e | ||
|
|
57e19f3036 | ||
|
|
51c8483dad | ||
|
|
6b60c37726 | ||
|
|
3eea225931 | ||
|
|
0529dc6b4c | ||
|
|
865fee4ce2 | ||
|
|
052202d273 | ||
|
|
c09dd94284 | ||
|
|
76f4a9f37f | ||
|
|
f446f8e9cc | ||
|
|
dbd1d6dc00 | ||
|
|
66621ea2a0 | ||
|
|
6ae56bf8b2 | ||
|
|
f472c0419b | ||
|
|
a7532773c1 | ||
|
|
81ad4ba313 | ||
|
|
b221f1e438 | ||
|
|
1b08ba1355 | ||
|
|
e191df1a38 | ||
|
|
e35a2a8c8d | ||
|
|
d7046e4595 | ||
|
|
70037fb47a | ||
|
|
4ae3866e44 | ||
|
|
1349813f0d | ||
|
|
294b672fe2 | ||
|
|
72349a4cae | ||
|
|
f6e9d3c32e | ||
|
|
7c8455bf29 | ||
|
|
f4c4a0435b | ||
|
|
bbc63dd77a | ||
|
|
7ed3f0d613 | ||
|
|
eb6a8c7479 | ||
|
|
58b230afe6 | ||
|
|
fb8dfef55e | ||
|
|
487d174b6d | ||
|
|
abb50d71b4 | ||
|
|
7409e2fd2d | ||
|
|
6a67804937 | ||
|
|
4f4d76d151 | ||
|
|
f3985b5d21 | ||
|
|
142cc337a0 | ||
|
|
fd1d1c075e |
Vendored
+4
-1
@@ -9,7 +9,10 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "dakiny_bot",
|
"module": "dakiny_bot",
|
||||||
"justMyCode": true
|
"justMyCode": true,
|
||||||
|
"env": {
|
||||||
|
"DEBUG": "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-1
@@ -8,7 +8,10 @@
|
|||||||
"CLANLEADER",
|
"CLANLEADER",
|
||||||
"Dakiny",
|
"Dakiny",
|
||||||
"DISCORDMANAGER",
|
"DISCORDMANAGER",
|
||||||
|
"gitea",
|
||||||
"highcouncil",
|
"highcouncil",
|
||||||
"nextcord"
|
"imcompiling",
|
||||||
|
"nextcord",
|
||||||
|
"ondelete"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
FROM python:3.13.3-slim
|
FROM python:3.13.5-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
|
||||||
|
|
||||||
# install uv
|
# install uv
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.7.8 /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.7.20 /uv /uvx /bin/
|
||||||
|
|
||||||
COPY . /home
|
COPY . /home
|
||||||
|
|
||||||
|
|||||||
+18
-19
@@ -1,7 +1,6 @@
|
|||||||
"""Main file for bot."""
|
"""Main file for bot."""
|
||||||
|
|
||||||
import logging
|
import pathlib
|
||||||
import os
|
|
||||||
|
|
||||||
import nextcord
|
import nextcord
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
@@ -10,18 +9,12 @@ from dakiny_bot.classes.nominate_highcouncil_views import NominateHighCouncilVie
|
|||||||
from dakiny_bot.classes.nominate_views import NominateView
|
from dakiny_bot.classes.nominate_views import NominateView
|
||||||
from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView
|
from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView
|
||||||
from dakiny_bot.classes.voting_views import VoteView
|
from dakiny_bot.classes.voting_views import VoteView
|
||||||
from dakiny_bot.database import create_database
|
from dakiny_bot.logger import dakiny_logger
|
||||||
|
|
||||||
from .DakinySettings import settings
|
from .dakiny_settings import settings
|
||||||
|
|
||||||
birthday_list: list[int] = []
|
birthday_list: list[int] = []
|
||||||
|
|
||||||
logger = logging.getLogger("nextcord")
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
|
|
||||||
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
|
|
||||||
def run() -> None:
|
def run() -> None:
|
||||||
"""Builds and runs bot."""
|
"""Builds and runs bot."""
|
||||||
@@ -38,19 +31,25 @@ def run() -> None:
|
|||||||
bot = commands.Bot(intents=intents)
|
bot = commands.Bot(intents=intents)
|
||||||
|
|
||||||
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
|
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
|
||||||
for folder in os.listdir("dakiny_bot/cogs"):
|
for folder in pathlib.Path("dakiny_bot/cogs").iterdir():
|
||||||
bot.load_extension(f"dakiny_bot.cogs.{folder}")
|
bot.load_extension(f"dakiny_bot.cogs.{folder.name}")
|
||||||
|
|
||||||
@bot.listen()
|
@bot.listen()
|
||||||
async def on_ready() -> None:
|
async def on_ready() -> None:
|
||||||
"""When Discord is connected."""
|
"""When Discord is connected."""
|
||||||
assert bot.user is not None # noqa: S101 documentation does this
|
if bot.user is None:
|
||||||
create_database # noqa: B018
|
dakiny_logger.error("Discord connection failed.")
|
||||||
bot.add_view(NominateView(bot.get_guild(settings.params.guild_id)))
|
return
|
||||||
bot.add_view(NominateHighCouncilView(bot.get_guild(settings.params.guild_id)))
|
|
||||||
bot.add_view(VoteView(bot.get_guild(settings.params.guild_id)))
|
if (guild := bot.get_guild(settings.params.guild_id)) is None:
|
||||||
bot.add_view(VoteHighcouncilView(bot.get_guild(settings.params.guild_id)))
|
dakiny_logger.error("Could not find Dakiny guild object.")
|
||||||
print(f"{bot.user.name} has connected to Discord!") # noqa: T201 TODO make logging to print my stuff to console
|
return
|
||||||
|
|
||||||
|
bot.add_view(NominateView(guild))
|
||||||
|
bot.add_view(NominateHighCouncilView(guild))
|
||||||
|
bot.add_view(VoteView(guild))
|
||||||
|
bot.add_view(VoteHighcouncilView(guild))
|
||||||
|
dakiny_logger.info("%s has connected to Discord!", bot.user.name)
|
||||||
|
|
||||||
# Run Discord bot
|
# Run Discord bot
|
||||||
bot.run(settings.params.discord_token)
|
bot.run(settings.params.discord_token)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Empty init file for classes module."""
|
||||||
+165
-96
@@ -5,20 +5,18 @@ from datetime import UTC, datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import nextcord
|
import nextcord
|
||||||
from sqlalchemy import select, true, update
|
from sqlalchemy import select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlmodel import col
|
||||||
|
|
||||||
from dakiny_bot.classes.nominate_highcouncil_views import NominateHighCouncilView
|
from dakiny_bot.classes.nominate_highcouncil_views import NominateHighCouncilView
|
||||||
from dakiny_bot.classes.nominate_views import NominateView
|
from dakiny_bot.classes.nominate_views import NominateView
|
||||||
from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView
|
from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView
|
||||||
from dakiny_bot.classes.voting_views import VoteView
|
from dakiny_bot.classes.voting_views import VoteView
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import (
|
from dakiny_bot.database.db import engine
|
||||||
CouncilMembers,
|
from dakiny_bot.database.models import CouncilMember, ElectionInformation, Vote
|
||||||
ElectionInformation,
|
from dakiny_bot.logger import dakiny_logger
|
||||||
Votes,
|
|
||||||
engine,
|
|
||||||
)
|
|
||||||
from dakiny_bot.utils import (
|
from dakiny_bot.utils import (
|
||||||
TEMPLATES_DIR,
|
TEMPLATES_DIR,
|
||||||
VoteType,
|
VoteType,
|
||||||
@@ -70,7 +68,7 @@ class Election:
|
|||||||
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 Path(f"{TEMPLATES_DIR}/election_announcement.txt", encoding="utf8").open() as file:
|
with Path(f"{TEMPLATES_DIR}/election_announcement.txt").open() as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
|
|
||||||
next_election = get_datetime(
|
next_election = get_datetime(
|
||||||
@@ -98,7 +96,7 @@ class Election:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Message to send.
|
str: Message to send.
|
||||||
"""
|
"""
|
||||||
with Path(f"{TEMPLATES_DIR}/highcouncil_election.txt", encoding="utf8").open() as file:
|
with Path(f"{TEMPLATES_DIR}/highcouncil_election.txt").open() as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
|
|
||||||
return message.format(
|
return message.format(
|
||||||
@@ -116,7 +114,7 @@ class Election:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Message to send.
|
str: Message to send.
|
||||||
"""
|
"""
|
||||||
with Path(f"{TEMPLATES_DIR}/highcouncil_vote.txt", encoding="utf8").open() as file:
|
with Path(f"{TEMPLATES_DIR}/highcouncil_vote.txt").open() as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
|
|
||||||
return message.format(
|
return message.format(
|
||||||
@@ -131,7 +129,7 @@ class Election:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Message to send
|
str: Message to send
|
||||||
"""
|
"""
|
||||||
with Path(f"{TEMPLATES_DIR}/vote_start.txt", encoding="utf8").open() as file:
|
with Path(f"{TEMPLATES_DIR}/vote_start.txt").open() as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
vote_end: datetime = get_datetime(settings.election_cycles[self.cycle].voting_end)
|
vote_end: datetime = get_datetime(settings.election_cycles[self.cycle].voting_end)
|
||||||
|
|
||||||
@@ -144,25 +142,37 @@ class Election:
|
|||||||
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)
|
||||||
|
dakiny_logger.debug("Checking 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():
|
||||||
|
dakiny_logger.debug("Election start.")
|
||||||
await self.election_start_action()
|
await self.election_start_action()
|
||||||
elif today.date() == self.voting_start.date():
|
elif today.date() == self.voting_start.date():
|
||||||
|
dakiny_logger.debug("Voting start.")
|
||||||
await self.voting_start_action()
|
await self.voting_start_action()
|
||||||
elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date():
|
elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date():
|
||||||
|
dakiny_logger.debug("Election reminder.")
|
||||||
await self.election_reminder()
|
await self.election_reminder()
|
||||||
elif today.date() == self.voting_end.date():
|
elif today.date() == self.voting_end.date():
|
||||||
|
dakiny_logger.debug("Voting end.")
|
||||||
await self.voting_end_action()
|
await self.voting_end_action()
|
||||||
elif today.date() == self.highcouncil_election_start.date():
|
elif today.date() == self.highcouncil_election_start.date():
|
||||||
|
dakiny_logger.debug("Highcouncil election start.")
|
||||||
await self.highcouncil_election_start_action()
|
await self.highcouncil_election_start_action()
|
||||||
elif today.date() == self.term_start.date():
|
elif today.date() == self.term_start.date():
|
||||||
|
dakiny_logger.debug("Term start")
|
||||||
await self.term_start_action()
|
await self.term_start_action()
|
||||||
|
else:
|
||||||
|
dakiny_logger.warning("Check date didn't do anything. This should never happen.")
|
||||||
|
|
||||||
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(settings.channel_ids.voting)
|
parent = self.guild.get_channel(settings.channel_ids.voting)
|
||||||
|
if not isinstance(parent, nextcord.TextChannel):
|
||||||
|
dakiny_logger.error("Voting channel (id: %s) is not a text channel.", settings.channel_ids.voting)
|
||||||
|
return
|
||||||
quarter_name: str = get_election_name()
|
quarter_name: str = get_election_name()
|
||||||
|
|
||||||
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
|
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
|
||||||
@@ -183,9 +193,9 @@ class Election:
|
|||||||
# create new entry into database.
|
# create new entry into database.
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
new_cycle = ElectionInformation(
|
new_cycle = ElectionInformation(
|
||||||
ElectionName=quarter_name,
|
name=quarter_name,
|
||||||
ThreadId=thread.id,
|
thread_id=thread.id,
|
||||||
CouncilNominationMsgId=nominate_msg.id,
|
council_nomination_msg_id=nominate_msg.id,
|
||||||
)
|
)
|
||||||
session.add(new_cycle)
|
session.add(new_cycle)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -198,64 +208,98 @@ class Election:
|
|||||||
Removes write permission from the general clan member role.
|
Removes write permission from the general clan member role.
|
||||||
"""
|
"""
|
||||||
message = self.build_vote_start()
|
message = self.build_vote_start()
|
||||||
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
|
|
||||||
await election_thread.send(message)
|
|
||||||
vote_msg = await election_thread.send(
|
|
||||||
build_votes_str(self.guild),
|
|
||||||
view=VoteView(self.guild),
|
|
||||||
)
|
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == get_election_name())
|
stmt = select(ElectionInformation).where(col(ElectionInformation.name == get_election_name()))
|
||||||
info = session.scalar(stmt)
|
if (election_info := session.scalar(stmt)) is None:
|
||||||
info.CouncilVoteMsgId = vote_msg.id
|
dakiny_logger.error("Unable to find election %s", get_election_name())
|
||||||
|
return
|
||||||
|
if (election_thread := self.guild.get_thread(election_info.thread_id)) is None:
|
||||||
|
dakiny_logger.error("Recorded thread id %s does not result in a thread.", election_info.thread_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
await election_thread.send(message)
|
||||||
|
|
||||||
|
vote_msg = await election_thread.send(
|
||||||
|
build_votes_str(self.guild),
|
||||||
|
view=VoteView(self.guild),
|
||||||
|
)
|
||||||
|
|
||||||
|
election_info.council_vote_msg_id = vote_msg.id
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
thread: nextcord.Thread = self.guild.get_thread(info.ThreadId)
|
if election_info.council_nomination_msg_id is None:
|
||||||
nominate_msg: nextcord.Message = await thread.fetch_message(info.CouncilNominationMsgId)
|
dakiny_logger.error("Election doesn't have a nomination message set.")
|
||||||
|
return
|
||||||
|
|
||||||
|
nominate_msg: nextcord.Message = await election_thread.fetch_message(
|
||||||
|
election_info.council_nomination_msg_id
|
||||||
|
)
|
||||||
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(settings.role_ids.clan_council_elect)
|
if (council_elect_role := self.guild.get_role(settings.role_ids.clan_council_elect)) is None:
|
||||||
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
|
dakiny_logger.error("Clan council elect role not found (id: %s).", settings.role_ids.clan_council_elect)
|
||||||
|
return
|
||||||
nominate_msg: nextcord.Message = await election_thread.send(message, view=NominateHighCouncilView(self.guild))
|
|
||||||
|
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
stmt = select(ElectionInformation).where(col(ElectionInformation.name == election_name))
|
||||||
|
if (info := session.scalar(stmt)) is None:
|
||||||
|
dakiny_logger.error("Unable to find election %s", election_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (election_thread := self.guild.get_thread(info.thread_id)) is None:
|
||||||
|
dakiny_logger.error("Recorded thread id %s does not result in a thread.", info.thread_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
nominate_msg: nextcord.Message = await election_thread.send(
|
||||||
|
message, view=NominateHighCouncilView(self.guild)
|
||||||
|
)
|
||||||
|
|
||||||
# remove vote button from vote message
|
# remove vote button from vote message
|
||||||
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == election_name)
|
if not info.council_vote_msg_id:
|
||||||
info = session.scalar(stmt)
|
dakiny_logger.error("Council vote message id not set.")
|
||||||
vote_msg: nextcord.Message = await election_thread.fetch_message(info.CouncilVoteMsgId)
|
return
|
||||||
|
vote_msg: nextcord.Message = await election_thread.fetch_message(info.council_vote_msg_id)
|
||||||
await vote_msg.edit(view=None)
|
await vote_msg.edit(view=None)
|
||||||
|
|
||||||
# add the highcouncil nomination message to election information
|
# add the highcouncil nomination message to election information
|
||||||
info.HighCouncilNominationMsgId = nominate_msg.id
|
info.high_council_nomination_msg_id = nominate_msg.id
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database
|
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(col(Vote.voted_for_id))
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.Type == VoteType.COUNCIL)
|
.where(col(Vote.type == VoteType.COUNCIL))
|
||||||
)
|
)
|
||||||
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 user_id is None:
|
||||||
|
# Abstained votes, ignore
|
||||||
|
continue
|
||||||
if votes >= REQUIRED_VOTES_TO_BE_ELECTED:
|
if votes >= REQUIRED_VOTES_TO_BE_ELECTED:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == user_id)
|
.where(col(CouncilMember.member_id == user_id))
|
||||||
)
|
)
|
||||||
council_member = session.scalar(stmt)
|
if (council_member := session.scalar(stmt)) is None:
|
||||||
council_member.Elected = True
|
dakiny_logger.error(
|
||||||
|
"Council member (id: %s) for election %s not found.", user_id, election_name
|
||||||
|
)
|
||||||
|
return
|
||||||
|
council_member.elected = True
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
member: nextcord.Member = self.guild.get_member(user_id)
|
if (member := self.guild.get_member(user_id)) is None:
|
||||||
|
dakiny_logger.error("Member (id: %s) not found in guild.", user_id)
|
||||||
|
return
|
||||||
|
|
||||||
await member.add_roles(
|
await member.add_roles(
|
||||||
council_elect_role,
|
council_elect_role,
|
||||||
@@ -265,42 +309,58 @@ class Election:
|
|||||||
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)
|
|
||||||
await election_thread.send(message)
|
|
||||||
vote_msg = await election_thread.send(
|
|
||||||
build_highcouncil_votes_str(self.guild),
|
|
||||||
view=VoteHighcouncilView(self.guild),
|
|
||||||
)
|
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == get_election_name())
|
stmt = select(ElectionInformation).where(col(ElectionInformation.name == get_election_name()))
|
||||||
info = session.scalar(stmt)
|
if (info := session.scalar(stmt)) is None:
|
||||||
info.HighCouncilVoteMsgId = vote_msg.id
|
dakiny_logger.error("Unknown election name %s.", get_election_name())
|
||||||
|
return
|
||||||
|
|
||||||
|
if (election_thread := self.guild.get_thread(info.thread_id)) is None:
|
||||||
|
dakiny_logger.error("Recorded thread id %s does not result in a thread.", info.thread_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
await election_thread.send(message)
|
||||||
|
vote_msg = await election_thread.send(
|
||||||
|
build_highcouncil_votes_str(self.guild),
|
||||||
|
view=VoteHighcouncilView(self.guild),
|
||||||
|
)
|
||||||
|
|
||||||
|
info.high_council_vote_msg_id = vote_msg.id
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# remove highcouncil nomination view
|
# remove highcouncil nomination view
|
||||||
nomination_msg: nextcord.Message = await election_thread.fetch_message(info.HighCouncilNominationMsgId)
|
if info.high_council_nomination_msg_id is None:
|
||||||
|
dakiny_logger.error("Election doesn't have a nomination message set.")
|
||||||
|
return
|
||||||
|
|
||||||
|
nomination_msg: nextcord.Message = await election_thread.fetch_message(info.high_council_nomination_msg_id)
|
||||||
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(settings.role_ids.clan_council_elect)
|
if (elect_role := self.guild.get_role(settings.role_ids.clan_council_elect)) is None:
|
||||||
council_role: nextcord.Role = self.guild.get_role(settings.role_ids.clan_council)
|
dakiny_logger.error("Cannot find Elect Role.")
|
||||||
|
return
|
||||||
|
if (council_role := self.guild.get_role(settings.role_ids.clan_council)) is None:
|
||||||
|
dakiny_logger.error("Cannot find Council Role.")
|
||||||
|
return
|
||||||
|
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# role management
|
# role management
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(col(CouncilMember.member_id))
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.Elected == true())
|
.where(col(CouncilMember.elected))
|
||||||
)
|
)
|
||||||
|
|
||||||
for member_id in session.scalars(stmt):
|
for member_id in session.scalars(stmt):
|
||||||
member = self.guild.get_member(member_id)
|
if (member := self.guild.get_member(member_id)) is None:
|
||||||
|
dakiny_logger.warning("Cannot find member (id: %s) to manage roles.", member_id)
|
||||||
|
continue
|
||||||
await member.remove_roles(
|
await member.remove_roles(
|
||||||
elect_role,
|
elect_role,
|
||||||
reason=f"{member.display_name} no longer needs the elect role.",
|
reason=f"{member.display_name} no longer needs the elect role.",
|
||||||
@@ -313,35 +373,36 @@ class Election:
|
|||||||
# announce high council members
|
# announce high council members
|
||||||
# ties are undefined behavior.
|
# ties are undefined behavior.
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(col(Vote.voted_for_id))
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.Type == VoteType.DISCORDMANAGER)
|
.where(col(Vote.type == VoteType.DISCORDMANAGER))
|
||||||
)
|
)
|
||||||
dm_votes = collections.Counter(session.scalars(stmt))
|
dm_votes = collections.Counter(session.scalars(stmt))
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(col(Vote.voted_for_id))
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.Type == VoteType.CLANLEADER)
|
.where(col(Vote.type == VoteType.CLANLEADER))
|
||||||
)
|
)
|
||||||
cl_votes = collections.Counter(session.scalars(stmt))
|
cl_votes = collections.Counter(session.scalars(stmt))
|
||||||
|
cl_votes.most_common()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dm_winner = max(dm_votes, key=dm_votes.get)
|
dm_winner = dm_votes.most_common(1).pop()[0]
|
||||||
stmt = (
|
stmt = (
|
||||||
update(CouncilMembers)
|
update(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == dm_winner)
|
.where(col(CouncilMember.member_id == dm_winner))
|
||||||
.values(DiscordManager=True)
|
.values(DiscordManager=True)
|
||||||
)
|
)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
dm_winner = "No winner"
|
dm_winner = "No winner"
|
||||||
try:
|
try:
|
||||||
cl_winner = max(cl_votes, key=cl_votes.get)
|
cl_winner = cl_votes.most_common(1).pop()[0]
|
||||||
stmt = (
|
stmt = (
|
||||||
update(CouncilMembers)
|
update(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == cl_winner)
|
.where(col(CouncilMember.member_id == cl_winner))
|
||||||
.values(ClanLeader=True)
|
.values(ClanLeader=True)
|
||||||
)
|
)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
@@ -350,14 +411,21 @@ class Election:
|
|||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
|
stmt = select(ElectionInformation).where(col(ElectionInformation.name == election_name))
|
||||||
|
if (info := session.scalar(stmt)) is None:
|
||||||
|
dakiny_logger.error("Could not find information for this election (%s).", election_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (election_thread := self.guild.get_thread(info.thread_id)) is None:
|
||||||
|
dakiny_logger.error("Recorded thread id %s does not result in a thread.", info.thread_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if info.high_council_vote_msg_id is None:
|
||||||
|
dakiny_logger.error("No recorded High Council Voting Message.")
|
||||||
|
return
|
||||||
|
|
||||||
# clear view from vote message
|
# clear view from vote message
|
||||||
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
vote_msg: nextcord.Message = await election_thread.fetch_message(info.high_council_vote_msg_id)
|
||||||
ElectionInformation.ElectionName == get_election_name()
|
|
||||||
)
|
|
||||||
vote_msg_id = session.scalar(stmt)
|
|
||||||
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
|
|
||||||
await vote_msg.edit(view=None)
|
await vote_msg.edit(view=None)
|
||||||
|
|
||||||
highcouncil_msg: str = f"Congratulations for our high council members!\nClan Leader: <@{cl_winner}>\n"
|
highcouncil_msg: str = f"Congratulations for our high council members!\nClan Leader: <@{cl_winner}>\n"
|
||||||
@@ -369,22 +437,23 @@ class Election:
|
|||||||
await election_thread.edit(archived=True, locked=True)
|
await election_thread.edit(archived=True, locked=True)
|
||||||
await edit_clan_council_info(self.guild)
|
await edit_clan_council_info(self.guild)
|
||||||
|
|
||||||
def get_election_info(self) -> ElectionInformation:
|
|
||||||
"""
|
|
||||||
Gets information for current election.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ElectionInformation: The information from the database about the current election.
|
|
||||||
"""
|
|
||||||
with Session(engine) as session:
|
|
||||||
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == get_election_name())
|
|
||||||
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(settings.channel_ids.general)
|
general = self.guild.get_channel(settings.channel_ids.general)
|
||||||
|
|
||||||
message = f"Reminder to <@&{settings.role_ids.clan_member}>! Clan elections are happening! Head over "
|
if not isinstance(general, nextcord.TextChannel):
|
||||||
message += f"to <#{self.get_election_info().ThreadId}> to cast your vote!"
|
dakiny_logger.error(
|
||||||
|
"Recorded general channel (id: %s) is not a text channel.", settings.channel_ids.general
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await general.send(content=message)
|
with Session(engine) as session:
|
||||||
|
stmt = select(ElectionInformation).where(col(ElectionInformation.name == get_election_name()))
|
||||||
|
if (info := session.scalar(stmt)) is None:
|
||||||
|
dakiny_logger.error("Could not find election %s.", get_election_name())
|
||||||
|
return
|
||||||
|
|
||||||
|
message = f"Reminder to <@&{settings.role_ids.clan_member}>! Clan elections are happening! Head over "
|
||||||
|
message += f"to <#{info.thread_id}> to cast your vote!"
|
||||||
|
|
||||||
|
await general.send(content=message)
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import nextcord
|
|||||||
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 sqlmodel import col
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import CouncilMembers, engine
|
from dakiny_bot.database.db import engine
|
||||||
|
from dakiny_bot.database.models import CouncilMember
|
||||||
|
from dakiny_bot.logger import dakiny_logger
|
||||||
from dakiny_bot.utils import (
|
from dakiny_bot.utils import (
|
||||||
NOMINEE_SPLITTER,
|
NOMINEE_SPLITTER,
|
||||||
decorate_cl_nomination,
|
decorate_cl_nomination,
|
||||||
@@ -53,26 +56,35 @@ class NominateDMModal(nextcord.ui.Modal):
|
|||||||
interaction (Interaction): Interaction with user.
|
interaction (Interaction): Interaction with user.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
speech = decorate_dm_nomination(interaction.user.id, self.speech.value)
|
if interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
return
|
||||||
|
|
||||||
|
speech = decorate_dm_nomination(interaction.user.id, self.speech.value if self.speech.value else "")
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# user already in table
|
# user already in table
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == self.usr_id)
|
.where(col(CouncilMember.member_id == self.usr_id))
|
||||||
)
|
)
|
||||||
council_elect = session.scalar(stmt)
|
council_elect = session.scalar(stmt)
|
||||||
|
|
||||||
if not self.usr_nominated:
|
if council_elect is None:
|
||||||
|
dakiny_logger.error("Did not find member (id: %s) in election %s.", self.usr_id, election_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.usr_nominated or council_elect.discord_manager_speech_id is None:
|
||||||
msg = await interaction.channel.send(speech)
|
msg = await interaction.channel.send(speech)
|
||||||
|
|
||||||
council_elect.DiscordManagerSpeechId = msg.id
|
council_elect.discord_manager_speech_id = msg.id
|
||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.DiscordManagerSpeechId)
|
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.discord_manager_speech_id)
|
||||||
await msg.edit(content=decorate_dm_nomination(interaction.user.id, self.speech.value))
|
await msg.edit(content=speech)
|
||||||
|
|
||||||
|
|
||||||
class NominateCLModal(nextcord.ui.Modal):
|
class NominateCLModal(nextcord.ui.Modal):
|
||||||
@@ -113,26 +125,35 @@ class NominateCLModal(nextcord.ui.Modal):
|
|||||||
interaction (Interaction): Interaction with user.
|
interaction (Interaction): Interaction with user.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
speech = decorate_cl_nomination(interaction.user.id, self.speech.value)
|
if interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
return
|
||||||
|
|
||||||
|
speech = decorate_cl_nomination(interaction.user.id, self.speech.value if self.speech.value else "")
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# user already in table
|
# user already in table
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == self.usr_id)
|
.where(col(CouncilMember.member_id == self.usr_id))
|
||||||
)
|
)
|
||||||
council_elect = session.scalar(stmt)
|
council_elect = session.scalar(stmt)
|
||||||
|
|
||||||
if not self.usr_nominated:
|
if council_elect is None:
|
||||||
|
dakiny_logger.error("Did not find member (id: %s) in election %s.", self.usr_id, election_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.usr_nominated or council_elect.clan_leader_speech_id is None:
|
||||||
msg = await interaction.channel.send(speech)
|
msg = await interaction.channel.send(speech)
|
||||||
|
|
||||||
council_elect.ClanLeaderSpeechId = msg.id
|
council_elect.clan_leader_speech_id = msg.id
|
||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.ClanLeaderSpeechId)
|
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.clan_leader_speech_id)
|
||||||
await msg.edit(content=decorate_cl_nomination(interaction.user.id, self.speech.value))
|
await msg.edit(content=speech)
|
||||||
|
|
||||||
|
|
||||||
class NominateHighCouncilView(nextcord.ui.View):
|
class NominateHighCouncilView(nextcord.ui.View):
|
||||||
@@ -158,8 +179,16 @@ class NominateHighCouncilView(nextcord.ui.View):
|
|||||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||||
"""
|
"""
|
||||||
|
if interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
return
|
||||||
|
|
||||||
# enforce council elect only
|
# enforce council elect only
|
||||||
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
|
if (
|
||||||
|
isinstance(interaction.user, nextcord.User)
|
||||||
|
or self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles
|
||||||
|
):
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
|
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
@@ -171,16 +200,15 @@ class NominateHighCouncilView(nextcord.ui.View):
|
|||||||
speech_content = ""
|
speech_content = ""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == interaction.user.id)
|
.where(col(CouncilMember.member_id == interaction.user.id))
|
||||||
)
|
)
|
||||||
obj = session.scalar(stmt)
|
council_elect = session.scalar(stmt)
|
||||||
|
|
||||||
# user needs to exist to get here anyways...
|
if council_elect is not None and council_elect.discord_manager_speech_id is not None:
|
||||||
if obj.DiscordManagerSpeechId is not None:
|
|
||||||
usr_nominated = True
|
usr_nominated = True
|
||||||
msg = await interaction.channel.fetch_message(obj.DiscordManagerSpeechId)
|
msg = await interaction.channel.fetch_message(council_elect.discord_manager_speech_id)
|
||||||
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
||||||
|
|
||||||
await interaction.response.send_modal(NominateDMModal(interaction.user.id, usr_nominated, speech_content))
|
await interaction.response.send_modal(NominateDMModal(interaction.user.id, usr_nominated, speech_content))
|
||||||
@@ -201,8 +229,16 @@ class NominateHighCouncilView(nextcord.ui.View):
|
|||||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||||
"""
|
"""
|
||||||
|
if interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
return
|
||||||
|
|
||||||
# enforce council elect only
|
# enforce council elect only
|
||||||
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
|
if (
|
||||||
|
isinstance(interaction.user, nextcord.User)
|
||||||
|
or self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles
|
||||||
|
):
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
|
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
@@ -214,16 +250,15 @@ class NominateHighCouncilView(nextcord.ui.View):
|
|||||||
speech_content = ""
|
speech_content = ""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == interaction.user.id)
|
.where(col(CouncilMember.member_id == interaction.user.id))
|
||||||
)
|
)
|
||||||
obj = session.scalar(stmt)
|
council_elect = session.scalar(stmt)
|
||||||
|
|
||||||
# user needs to exist to get here anyways...
|
if council_elect is not None and council_elect.clan_leader_speech_id is not None:
|
||||||
if obj.ClanLeaderSpeechId is not None:
|
|
||||||
usr_nominated = True
|
usr_nominated = True
|
||||||
msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId)
|
msg = await interaction.channel.fetch_message(council_elect.clan_leader_speech_id)
|
||||||
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
||||||
|
|
||||||
await interaction.response.send_modal(NominateCLModal(interaction.user.id, usr_nominated, speech_content))
|
await interaction.response.send_modal(NominateCLModal(interaction.user.id, usr_nominated, speech_content))
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import nextcord
|
|||||||
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 sqlmodel import col
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import CouncilMembers, engine
|
from dakiny_bot.database.db import engine
|
||||||
|
from dakiny_bot.database.models import CouncilMember
|
||||||
|
from dakiny_bot.logger import dakiny_logger
|
||||||
from dakiny_bot.utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name
|
from dakiny_bot.utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name
|
||||||
|
|
||||||
|
|
||||||
@@ -48,31 +51,45 @@ class NominateModal(nextcord.ui.Modal):
|
|||||||
interaction (Interaction): Interaction with user.
|
interaction (Interaction): Interaction with user.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
speech = decorate_nomination(interaction.user.id, self.speech.value)
|
if interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
return
|
||||||
|
|
||||||
|
speech = decorate_nomination(interaction.user.id, self.speech.value if self.speech.value else "")
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
if not self.usr_nominated:
|
if not self.usr_nominated:
|
||||||
msg = await interaction.channel.send(speech)
|
msg = await interaction.channel.send(speech)
|
||||||
|
|
||||||
council_member = CouncilMembers(
|
council_member = CouncilMember(
|
||||||
ElectionName=election_name,
|
election_name=election_name,
|
||||||
MemberId=interaction.user.id,
|
member_id=interaction.user.id,
|
||||||
SpeechMsgId=msg.id,
|
speech_msg_id=msg.id,
|
||||||
)
|
)
|
||||||
session.add(council_member)
|
session.add(council_member)
|
||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
# user already nominated, just need to update the election speech
|
# user already nominated, just need to update the election speech
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == self.usr_id)
|
.where(col(CouncilMember.member_id == self.usr_id))
|
||||||
)
|
)
|
||||||
obj = session.scalar(stmt)
|
council_elect = session.scalar(stmt)
|
||||||
|
|
||||||
msg: nextcord.Message = await interaction.channel.fetch_message(obj.SpeechMsgId)
|
if council_elect is None:
|
||||||
await msg.edit(content=decorate_nomination(interaction.user.id, self.speech.value))
|
dakiny_logger.error("Did not find member (id: %s) in election %s.", self.usr_id, election_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if council_elect.speech_msg_id is None:
|
||||||
|
msg = await interaction.channel.send(speech)
|
||||||
|
council_elect.speech_msg_id = msg.id
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.speech_msg_id)
|
||||||
|
await msg.edit(content=speech)
|
||||||
|
|
||||||
|
|
||||||
class NominateView(nextcord.ui.View):
|
class NominateView(nextcord.ui.View):
|
||||||
@@ -98,8 +115,16 @@ class NominateView(nextcord.ui.View):
|
|||||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||||
"""
|
"""
|
||||||
|
if interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
return
|
||||||
|
|
||||||
# enforce members only
|
# enforce members only
|
||||||
if self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles:
|
if (
|
||||||
|
isinstance(interaction.user, nextcord.User)
|
||||||
|
or self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles
|
||||||
|
):
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"You must have the <@&{settings.role_ids.clan_member}> role to vote.",
|
f"You must have the <@&{settings.role_ids.clan_member}> role to vote.",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
@@ -111,14 +136,14 @@ class NominateView(nextcord.ui.View):
|
|||||||
speech_content = ""
|
speech_content = ""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.MemberId == interaction.user.id)
|
.where(col(CouncilMember.member_id == interaction.user.id))
|
||||||
)
|
)
|
||||||
obj = session.scalar(stmt)
|
obj = session.scalar(stmt)
|
||||||
if obj is not None:
|
if obj is not None and obj.speech_msg_id is not None:
|
||||||
usr_nominated = True
|
usr_nominated = True
|
||||||
msg = await interaction.channel.fetch_message(obj.SpeechMsgId)
|
msg = await interaction.channel.fetch_message(obj.speech_msg_id)
|
||||||
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
||||||
|
|
||||||
await interaction.response.send_modal(NominateModal(interaction.user.id, usr_nominated, speech_content))
|
await interaction.response.send_modal(NominateModal(interaction.user.id, usr_nominated, speech_content))
|
||||||
|
|||||||
@@ -4,7 +4,3 @@ extend = "../../pyproject.toml"
|
|||||||
|
|
||||||
# Exclude a variety of commonly ignored directories.
|
# Exclude a variety of commonly ignored directories.
|
||||||
exclude = ["gitea_webhook.py"]
|
exclude = ["gitea_webhook.py"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
|
||||||
# ignore implicit-namespace-package in this directory.
|
|
||||||
ignore = ["INP001"]
|
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ from nextcord import SelectOption
|
|||||||
from nextcord.interactions import Interaction
|
from nextcord.interactions import Interaction
|
||||||
from sqlalchemy import exc, select, update
|
from sqlalchemy import exc, select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlmodel import col
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import (
|
from dakiny_bot.database.db import engine
|
||||||
CouncilMembers,
|
from dakiny_bot.database.models import CouncilMember, ElectionInformation, Vote
|
||||||
ElectionInformation,
|
from dakiny_bot.logger import dakiny_logger
|
||||||
Votes,
|
|
||||||
engine,
|
|
||||||
)
|
|
||||||
from dakiny_bot.utils import VoteType, build_highcouncil_votes_str, get_election_name
|
from dakiny_bot.utils import VoteType, build_highcouncil_votes_str, get_election_name
|
||||||
|
|
||||||
|
|
||||||
@@ -38,20 +36,32 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
Args:
|
Args:
|
||||||
interaction (Interaction): Interaction with this callback.
|
interaction (Interaction): Interaction with this callback.
|
||||||
"""
|
"""
|
||||||
|
if (
|
||||||
|
interaction.user is None
|
||||||
|
or not isinstance(interaction.channel, nextcord.TextChannel)
|
||||||
|
or interaction.guild is None
|
||||||
|
):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
# if no guild, is problem because should only be in guild.
|
||||||
|
return
|
||||||
|
|
||||||
election_name: str = get_election_name()
|
election_name: str = get_election_name()
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
stmt = select(col(ElectionInformation.high_council_vote_msg_id)).where(
|
||||||
ElectionInformation.ElectionName == election_name
|
col(ElectionInformation.name == election_name)
|
||||||
)
|
)
|
||||||
vote_msg_id = session.scalar(stmt)
|
if (vote_msg_id := session.scalar(stmt)) is None:
|
||||||
|
dakiny_logger.error("Unable to find a Highcouncil Vote Message ID in database.")
|
||||||
|
return
|
||||||
|
|
||||||
voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
|
voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
|
||||||
|
|
||||||
new_vote = Votes(
|
new_vote = Vote(
|
||||||
ElectionName=election_name,
|
election_name=election_name,
|
||||||
VoterId=interaction.user.id,
|
voter_id=interaction.user.id,
|
||||||
VotedForId=voted_for,
|
voted_for_id=voted_for,
|
||||||
Type=self.election_type,
|
type=self.election_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -62,10 +72,10 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
# user already has a vote in the election and election type
|
# user already has a vote in the election and election type
|
||||||
session.rollback()
|
session.rollback()
|
||||||
stmt = (
|
stmt = (
|
||||||
update(Votes)
|
update(Vote)
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(col(Vote.voter_id == interaction.user.id))
|
||||||
.where(Votes.Type == self.election_type)
|
.where(col(Vote.type == self.election_type))
|
||||||
.values(VotedForId=voted_for)
|
.values(VotedForId=voted_for)
|
||||||
)
|
)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
@@ -104,6 +114,10 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(interaction.user, nextcord.Member):
|
||||||
|
# the user should only be a member of the guild.
|
||||||
|
return
|
||||||
|
|
||||||
# enforce council elect only
|
# enforce council elect only
|
||||||
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
|
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
@@ -116,32 +130,29 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(col(Vote.voted_for_id))
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(col(Vote.voter_id == interaction.user.id))
|
||||||
.where(Votes.Type == VoteType.CLANLEADER)
|
.where(col(Vote.type == VoteType.CLANLEADER))
|
||||||
)
|
)
|
||||||
voted_for = session.scalar(stmt)
|
voted_for = session.scalar(stmt)
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(col(CouncilMember.member_id))
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
.filter(col(CouncilMember.clan_leader_speech_id is not None))
|
||||||
)
|
)
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOption(
|
SelectOption(
|
||||||
label=(
|
label=(f"{member.display_name} ☑️" if voted_for == nominee else f"{member.display_name}"),
|
||||||
f"{self.guild.get_member(nominee).display_name} ☑️"
|
|
||||||
if voted_for == nominee
|
|
||||||
else f"{self.guild.get_member(nominee).display_name}"
|
|
||||||
),
|
|
||||||
value=str(nominee),
|
value=str(nominee),
|
||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
|
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
)
|
)
|
||||||
|
if (member := self.guild.get_member(nominee)) is not None
|
||||||
]
|
]
|
||||||
options.append(
|
options.append(
|
||||||
SelectOption(
|
SelectOption(
|
||||||
@@ -176,6 +187,10 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(interaction.user, nextcord.Member):
|
||||||
|
# the user should only be a member of the guild.
|
||||||
|
return
|
||||||
|
|
||||||
# enforce council elect only
|
# enforce council elect only
|
||||||
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
|
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
@@ -188,32 +203,29 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(col(Vote.voted_for_id))
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(col(Vote.voter_id == interaction.user.id))
|
||||||
.where(Votes.Type == VoteType.DISCORDMANAGER)
|
.where(col(Vote.type == VoteType.DISCORDMANAGER))
|
||||||
)
|
)
|
||||||
voted_for = session.scalar(stmt)
|
voted_for = session.scalar(stmt)
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(col(CouncilMember.member_id))
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
.filter(col(CouncilMember.discord_manager_speech_id is not None))
|
||||||
)
|
)
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOption(
|
SelectOption(
|
||||||
label=(
|
label=(f"{member.display_name} ☑️" if voted_for == nominee else f"{member.display_name}"),
|
||||||
f"{self.guild.get_member(nominee).display_name} ☑️"
|
|
||||||
if voted_for == nominee
|
|
||||||
else f"{self.guild.get_member(nominee).display_name}"
|
|
||||||
),
|
|
||||||
value=str(nominee),
|
value=str(nominee),
|
||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
|
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
)
|
)
|
||||||
|
if (member := self.guild.get_member(nominee)) is not None
|
||||||
]
|
]
|
||||||
options.append(
|
options.append(
|
||||||
SelectOption(
|
SelectOption(
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ from nextcord import SelectOption
|
|||||||
from nextcord.interactions import Interaction
|
from nextcord.interactions import Interaction
|
||||||
from sqlalchemy import exc, select, update
|
from sqlalchemy import exc, select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlmodel import col
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import (
|
from dakiny_bot.database.db import engine
|
||||||
CouncilMembers,
|
from dakiny_bot.database.models import CouncilMember, ElectionInformation, Vote
|
||||||
ElectionInformation,
|
from dakiny_bot.logger import dakiny_logger
|
||||||
Votes,
|
|
||||||
engine,
|
|
||||||
)
|
|
||||||
from dakiny_bot.utils import VoteType, build_votes_str, get_election_name
|
from dakiny_bot.utils import VoteType, build_votes_str, get_election_name
|
||||||
|
|
||||||
|
|
||||||
@@ -37,18 +35,32 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
Args:
|
Args:
|
||||||
interaction (Interaction): Interaction with this callback.
|
interaction (Interaction): Interaction with this callback.
|
||||||
"""
|
"""
|
||||||
|
if (
|
||||||
|
interaction.user is None
|
||||||
|
or not isinstance(interaction.channel, nextcord.TextChannel)
|
||||||
|
or interaction.guild is None
|
||||||
|
):
|
||||||
|
# ignore none... Don't know what situation this is.
|
||||||
|
# if not textchannel, don't know how callback happened
|
||||||
|
# if no guild, is problem because should only be in guild.
|
||||||
|
return
|
||||||
|
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation.CouncilVoteMsgId).where(ElectionInformation.ElectionName == election_name)
|
stmt = select(col(ElectionInformation.council_vote_msg_id)).where(
|
||||||
vote_msg_id = session.scalar(stmt)
|
col(ElectionInformation.name == election_name)
|
||||||
|
)
|
||||||
|
if (vote_msg_id := session.scalar(stmt)) is None:
|
||||||
|
dakiny_logger.error("Unable to find a Vote Message ID in database.")
|
||||||
|
return
|
||||||
|
|
||||||
voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
|
voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
|
||||||
|
|
||||||
new_vote = Votes(
|
new_vote = Vote(
|
||||||
ElectionName=election_name,
|
election_name=election_name,
|
||||||
VoterId=interaction.user.id,
|
voter_id=interaction.user.id,
|
||||||
VotedForId=voted_for,
|
voted_for_id=voted_for,
|
||||||
Type=VoteType.COUNCIL,
|
type=VoteType.COUNCIL,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -58,10 +70,10 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
except exc.IntegrityError:
|
except exc.IntegrityError:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
stmt = (
|
stmt = (
|
||||||
update(Votes)
|
update(Vote)
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(col(Vote.voter_id == interaction.user.id))
|
||||||
.where(Votes.Type == VoteType.COUNCIL)
|
.where(col(Vote.type == VoteType.COUNCIL))
|
||||||
.values(VotedForId=voted_for)
|
.values(VotedForId=voted_for)
|
||||||
)
|
)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
@@ -95,6 +107,10 @@ class VoteView(nextcord.ui.View):
|
|||||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(interaction.user, nextcord.Member):
|
||||||
|
# the user should only be a member of the guild.
|
||||||
|
return
|
||||||
|
|
||||||
# enforce members only
|
# enforce members only
|
||||||
if self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles:
|
if self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
@@ -107,28 +123,25 @@ class VoteView(nextcord.ui.View):
|
|||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(col(Vote.voted_for_id))
|
||||||
.where(Votes.ElectionName == election_name)
|
.where(col(Vote.election_name == election_name))
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(col(Vote.voter_id == interaction.user.id))
|
||||||
.where(Votes.Type == VoteType.COUNCIL)
|
.where(col(Vote.type == VoteType.COUNCIL))
|
||||||
)
|
)
|
||||||
voted_for = session.scalar(stmt)
|
voted_for = session.scalar(stmt)
|
||||||
|
|
||||||
stmt = select(CouncilMembers.MemberId).where(CouncilMembers.ElectionName == election_name)
|
stmt = select(col(CouncilMember.member_id)).where(col(CouncilMember.election_name == election_name))
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOption(
|
SelectOption(
|
||||||
label=(
|
label=(f"{member.display_name} ☑️" if voted_for == nominee else f"{member.display_name}"),
|
||||||
f"{self.guild.get_member(nominee).display_name} ☑️"
|
|
||||||
if voted_for == nominee
|
|
||||||
else f"{self.guild.get_member(nominee).display_name}"
|
|
||||||
),
|
|
||||||
value=str(nominee),
|
value=str(nominee),
|
||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
|
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
)
|
)
|
||||||
|
if (member := self.guild.get_member(nominee)) is not None
|
||||||
]
|
]
|
||||||
options.append(
|
options.append(
|
||||||
SelectOption(
|
SelectOption(
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ from typing import TYPE_CHECKING
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from git import Repo
|
from git import Repo
|
||||||
|
from git.exc import GitCommandError
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from dakiny_bot.classes.gitea_webhook import GiteaWebhook
|
from dakiny_bot.classes.gitea_webhook import GiteaWebhook
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
|
from dakiny_bot.logger import dakiny_logger
|
||||||
from dakiny_bot.utils import REPO_DIR, edit_clan_council_info, update_clan_messages
|
from dakiny_bot.utils import REPO_DIR, edit_clan_council_info, update_clan_messages
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -55,7 +57,12 @@ class ClanMessages(commands.Cog, name="Clan Messages"):
|
|||||||
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}
|
||||||
channel_updates = {file.split("/")[1] for file in modified_files if file.split("/")[1] not in template_updates}
|
channel_updates = {file.split("/")[1] for file in modified_files if file.split("/")[1] not in template_updates}
|
||||||
|
|
||||||
self.origin.pull()
|
try:
|
||||||
|
self.origin.pull()
|
||||||
|
except GitCommandError:
|
||||||
|
# I think the git connection timed out. Get a new one.
|
||||||
|
dakiny_logger.info("Refreshing git connection.")
|
||||||
|
self.repo_setup()
|
||||||
guild: nextcord.Guild = self.bot.get_guild(settings.params.guild_id)
|
guild: nextcord.Guild = self.bot.get_guild(settings.params.guild_id)
|
||||||
|
|
||||||
await update_clan_messages(guild, channel_updates)
|
await update_clan_messages(guild, channel_updates)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import nextcord
|
|||||||
from discord import TextInputStyle
|
from discord import TextInputStyle
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, record_points
|
from dakiny_bot.utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, record_points
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import nextcord
|
|||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import Presences, engine
|
from dakiny_bot.database.sql_data_classes import Presences, engine
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import nextcord
|
import nextcord
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.utils import GUILDS
|
from dakiny_bot.utils import GUILDS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import nextcord
|
|||||||
from nextcord.ext import application_checks, commands
|
from nextcord.ext import application_checks, commands
|
||||||
|
|
||||||
from dakiny_bot.classes.election import Election
|
from dakiny_bot.classes.election import Election
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.cogs.loops import daily_loop
|
||||||
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.utils import GUILDS, get_active_election_cycle, get_quarter_name
|
from dakiny_bot.utils import GUILDS, get_active_election_cycle, get_quarter_name
|
||||||
|
|
||||||
ME = 206602713511886849
|
ME = 206602713511886849
|
||||||
@@ -119,11 +120,8 @@ class TestCog(commands.Cog, name="Test commands."):
|
|||||||
Args:
|
Args:
|
||||||
interaction (nextcord.Interaction): Interaction with the message.
|
interaction (nextcord.Interaction): Interaction with the message.
|
||||||
"""
|
"""
|
||||||
cycle = Election(
|
dl = daily_loop.DailyLoop(self.bot)
|
||||||
get_active_election_cycle(),
|
await dl.daily_loop()
|
||||||
self.bot.get_guild(settings.params.guild_id),
|
|
||||||
)
|
|
||||||
await cycle.check_date()
|
|
||||||
await interaction.send("Test command sent.", ephemeral=True)
|
await interaction.send("Test command sent.", ephemeral=True)
|
||||||
|
|
||||||
@test.subcommand()
|
@test.subcommand()
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from dakiny_bot.app import birthday_list
|
from dakiny_bot.app import birthday_list
|
||||||
from dakiny_bot.classes.election import Election
|
from dakiny_bot.classes.election import Election
|
||||||
from dakiny_bot.DakinySettings import settings
|
from dakiny_bot.dakiny_settings import settings
|
||||||
from dakiny_bot.database.sql_data_classes import UserBirthday, engine
|
from dakiny_bot.database.sql_data_classes import UserBirthday, engine
|
||||||
|
from dakiny_bot.logger import dakiny_logger
|
||||||
from dakiny_bot.utils import get_active_election_cycle
|
from dakiny_bot.utils import get_active_election_cycle
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -43,8 +44,10 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
|
|||||||
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.
|
||||||
"""
|
"""
|
||||||
|
dakiny_logger.debug("Daily loop. Current election: %s.", self.current_election)
|
||||||
if self.current_election is None:
|
if self.current_election is None:
|
||||||
self.current_election = self.get_election()
|
self.current_election = self.get_election()
|
||||||
|
dakiny_logger.debug("Current election: %s.", self.current_election)
|
||||||
|
|
||||||
if self.current_election is not None:
|
if self.current_election is not None:
|
||||||
await self.current_election.check_date()
|
await self.current_election.check_date()
|
||||||
@@ -78,13 +81,17 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
|
|||||||
Election: Current election cycle, None if there is no election happening.
|
Election: Current election cycle, None if there is no election happening.
|
||||||
"""
|
"""
|
||||||
cycle = get_active_election_cycle()
|
cycle = get_active_election_cycle()
|
||||||
|
dakiny_logger.debug("Current cycle: %s.", cycle)
|
||||||
|
|
||||||
# prevent exception from using cycle as an Int in an array
|
# prevent exception from using cycle as an Int in an array
|
||||||
if cycle is None:
|
if cycle is None:
|
||||||
|
dakiny_logger.info("Cycle was None. Returning without an election.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
election = Election(cycle, self.bot.get_guild(settings.params.guild_id))
|
election = Election(cycle, self.bot.get_guild(settings.params.guild_id))
|
||||||
|
|
||||||
|
dakiny_logger.debug("Election active: %s", election.active)
|
||||||
|
|
||||||
return election if election.active else None
|
return election if election.active else None
|
||||||
|
|
||||||
def update_birthdays(self) -> None:
|
def update_birthdays(self) -> None:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import nextcord
|
|||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from dakiny_bot.app import birthday_list
|
from dakiny_bot.app import birthday_list
|
||||||
|
from dakiny_bot.dakiny_settings import settings
|
||||||
|
|
||||||
|
|
||||||
class SubmissionListener(commands.Cog, name="Message Listener"):
|
class SubmissionListener(commands.Cog, name="Message Listener"):
|
||||||
@@ -16,7 +17,15 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
|
|||||||
async def on_message(self, message: nextcord.Message) -> None:
|
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: # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ban all members who message in this channel. Everyone in guild is a member, so shouldn't miss a ban
|
||||||
|
# due to message.author not being a nextcord.Member object.
|
||||||
|
if message.channel.id == settings.channel_ids.ban and isinstance(message.author, nextcord.Member):
|
||||||
|
await message.author.ban(
|
||||||
|
delete_message_seconds=30 * 60, reason="Probable spam. The ban channel was messaged."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await birthday_action(message)
|
await birthday_action(message)
|
||||||
@@ -31,7 +40,7 @@ def setup(bot: commands.Bot) -> None:
|
|||||||
|
|
||||||
async def birthday_action(message: nextcord.Message) -> None:
|
async def birthday_action(message: nextcord.Message) -> None:
|
||||||
"""
|
"""
|
||||||
Checks a message's auther and mentions to see if they are in the birthday list.
|
Checks a message's author and mentions to see if they are in the birthday list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message (nextcord.Message): The message to check for a birthday person.
|
message (nextcord.Message): The message to check for a birthday person.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class ChannelIds:
|
|||||||
event_post: int
|
event_post: int
|
||||||
general: int
|
general: int
|
||||||
voting: int
|
voting: int
|
||||||
|
ban: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -83,4 +84,5 @@ class DakinySettings(YAMLWizard):
|
|||||||
self.to_yaml_file(CONFIG_LOCATION)
|
self.to_yaml_file(CONFIG_LOCATION)
|
||||||
|
|
||||||
|
|
||||||
settings = DakinySettings.from_yaml_file(CONFIG_LOCATION)
|
tmp = DakinySettings.from_yaml_file(CONFIG_LOCATION)
|
||||||
|
settings: DakinySettings = tmp[0] if isinstance(tmp, list) else tmp
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"""Small script to create the database if it doesn't already exist."""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
if not Path("database/dakiny.db").exists():
|
|
||||||
with sqlite3.connect("database/dakiny.db") as con:
|
|
||||||
cur = con.cursor()
|
|
||||||
|
|
||||||
with Path("dakiny_bot/database/database.sql").open() as file:
|
|
||||||
database_script = file.read()
|
|
||||||
cur.executescript(database_script)
|
|
||||||
con.commit()
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""Simple file to set up the sqlmodel connection to database."""
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel, create_engine
|
||||||
|
|
||||||
|
from dakiny_bot.logger import dakiny_logger
|
||||||
|
|
||||||
|
from . import models # noqa: F401 - Need this import for creating the models.
|
||||||
|
|
||||||
|
url = "sqlite:///database/dakiny.db"
|
||||||
|
dakiny_logger.info("Connected to local dev database.")
|
||||||
|
engine = create_engine(url)
|
||||||
|
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""File holds all model definitions of SQL tables."""
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.mysql import BIGINT, BOOLEAN, INTEGER, VARCHAR
|
||||||
|
from sqlmodel import Column, Field, ForeignKey, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class ElectionInformation(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Represents all required information for an election.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: Name of the election.
|
||||||
|
thread_id: Thread of where all the election related messages take place.
|
||||||
|
council_vote_msg_id: Message where the council vote is presented.
|
||||||
|
high_council_vote_msg_id: Message where the high council vote is presented.
|
||||||
|
council_nomination_msg_id: Message where council nomination button is held.
|
||||||
|
high_council_nomination_msg_id: Message where high council nomination button is held.
|
||||||
|
council_members: List of council members who ran during this election.
|
||||||
|
votes: All votes cast during this election.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "ElectionInformation"
|
||||||
|
|
||||||
|
name: str = Field(sa_column=Column("ElectionName", VARCHAR, primary_key=True, unique=True), max_length=30)
|
||||||
|
thread_id: int = Field(sa_column=Column("ThreadId", BIGINT(unsigned=True), nullable=False))
|
||||||
|
council_vote_msg_id: int | None = Field(
|
||||||
|
sa_column=Column("CouncilVoteMsgId", BIGINT(unsigned=True), nullable=True), default=None
|
||||||
|
)
|
||||||
|
high_council_vote_msg_id: int | None = Field(
|
||||||
|
sa_column=Column("HighCouncilVoteMsgId", BIGINT(unsigned=True), nullable=True), default=None
|
||||||
|
)
|
||||||
|
council_nomination_msg_id: int | None = Field(
|
||||||
|
sa_column=Column("CouncilNominationMsgId", BIGINT(unsigned=True), nullable=True), default=None
|
||||||
|
)
|
||||||
|
high_council_nomination_msg_id: int | None = Field(
|
||||||
|
sa_column=Column("HighCouncilNominationMsgId", BIGINT(unsigned=True), nullable=True), default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
council_members: list["CouncilMember"] = Relationship(back_populates="election", cascade_delete=True)
|
||||||
|
votes: list["Vote"] = Relationship(back_populates="election", cascade_delete=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CouncilMember(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Represents members involved in a given election.
|
||||||
|
|
||||||
|
Records speech message ids for general election and high council position. Booleans record if the user was elected
|
||||||
|
to a council/high council position.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
member_id: User that is running for council's discord id.
|
||||||
|
speech_msg_id: Message id of user's speech.
|
||||||
|
elected: Whether the user got elected to council.
|
||||||
|
discord_manager: Whether the user got elected to the Discord Manager position.
|
||||||
|
clan_leader: Whether the user got elected to the Clan Leader position.
|
||||||
|
discord_manager_speech_id: Message id of the user's Discord Manager speech.
|
||||||
|
clan_leader_speech_id: Message id of the user's Clan Leader speech.
|
||||||
|
election: The election this user object is for.
|
||||||
|
votes: A list of votes for this user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "CouncilMembers"
|
||||||
|
|
||||||
|
election_name: str = Field(
|
||||||
|
sa_column=Column(
|
||||||
|
"ElectionName",
|
||||||
|
VARCHAR,
|
||||||
|
ForeignKey("ElectionInformation.ElectionName", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
max_length=30,
|
||||||
|
)
|
||||||
|
member_id: int = Field(sa_column=Column("MemberId", BIGINT(unsigned=True), primary_key=True, nullable=False))
|
||||||
|
speech_msg_id: int | None = Field(
|
||||||
|
sa_column=Column("SpeechMsgId", BIGINT(unsigned=True), nullable=False), default=None
|
||||||
|
)
|
||||||
|
elected: bool = Field(sa_column=Column("Elected", BOOLEAN), default=False)
|
||||||
|
discord_manager: bool = Field(sa_column=Column("DiscordManager", BOOLEAN), default=False)
|
||||||
|
clan_leader: bool = Field(sa_column=Column("ClanLeader", BOOLEAN), default=False)
|
||||||
|
discord_manager_speech_id: int | None = Field(
|
||||||
|
sa_column=Column("DiscordManagerSpeechId", BIGINT(unsigned=True), nullable=True), default=None
|
||||||
|
)
|
||||||
|
clan_leader_speech_id: int | None = Field(
|
||||||
|
sa_column=Column("ClanLeaderSpeechId", BIGINT(unsigned=True), nullable=True), default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
election: ElectionInformation | None = Relationship(back_populates="council_members")
|
||||||
|
votes: list["Vote"] = Relationship(back_populates="council_member")
|
||||||
|
|
||||||
|
|
||||||
|
class Vote(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Represents a vote cast during an election.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
election_name: Election this vote was cast in.
|
||||||
|
voter_id: User id that cast the vote.
|
||||||
|
voted_for_id: User id that was voted for.
|
||||||
|
type: Type of election, council, discordmanager, or clanleader.
|
||||||
|
election: A link to the ElectionInformation object that the vote was cast in.
|
||||||
|
council_member: A link to the CouncilMember object that the vote is for.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "Votes"
|
||||||
|
|
||||||
|
election_name: str = Field(
|
||||||
|
sa_column=Column(
|
||||||
|
"ElectionName",
|
||||||
|
VARCHAR,
|
||||||
|
ForeignKey("ElectionInformation.ElectionName", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
max_length=30,
|
||||||
|
)
|
||||||
|
voter_id: int = Field(sa_column=Column("VoterId", BIGINT(unsigned=True), primary_key=True, nullable=False))
|
||||||
|
# might have to set this foreign key later with alembic migration...
|
||||||
|
voted_for_id: int | None = Field(
|
||||||
|
sa_column=Column("VotedForId", BIGINT(unsigned=True), ForeignKey("CouncilMembers.MemberId"), nullable=True)
|
||||||
|
)
|
||||||
|
type: str = Field(
|
||||||
|
sa_column=Column("Type", VARCHAR, primary_key=True, nullable=False, default="council"), max_length=14
|
||||||
|
)
|
||||||
|
|
||||||
|
election: ElectionInformation | None = Relationship(back_populates="votes")
|
||||||
|
council_member: CouncilMember | None = Relationship(back_populates="votes")
|
||||||
|
|
||||||
|
|
||||||
|
class UserBirthday(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Represents a user's birth month and day.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: User's discord id.
|
||||||
|
birthday: Month/day that is a user's birthday.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: int = Field(sa_column=Column("UserId", BIGINT(unsigned=True), primary_key=True, nullable=False))
|
||||||
|
|
||||||
|
# TODO(imcompiling): split into month and day. Also relies on Alembic migration.
|
||||||
|
# https://gitea.cheezers.org/imcompiling/dakiny-bot/issues/93
|
||||||
|
birthday: str = Field(sa_column=Column("Birthday", VARCHAR, nullable=False), max_length=5)
|
||||||
|
|
||||||
|
|
||||||
|
class Presence(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Stores presences for the bot to pull and use.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Auto-incremented id for each entry.
|
||||||
|
presence: The presence itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "Presences"
|
||||||
|
|
||||||
|
id: int | None = Field(sa_column=Column("Id", INTEGER, primary_key=True), default=None)
|
||||||
|
presence: str = Field(sa_column=Column("Presence", VARCHAR, nullable=False), max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Points(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Stores activity points a user can gain by participating in clan activities.
|
||||||
|
|
||||||
|
Event points and clan points were separated out.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
quarter_name: Name of the quarter the points were earned in.
|
||||||
|
user_id: The user that earned them. Is a discord id if the user is a discord member, otherwise their rsn.
|
||||||
|
discord_member: Boolean on whether the user is a discord member or not. Used to know what the user_id
|
||||||
|
represents.
|
||||||
|
event_points: Event points earned during the quarter.
|
||||||
|
clan_points: Clan points earned during the quarter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
quarter_name: str = Field(sa_column=Column("QuarterName", VARCHAR, primary_key=True, nullable=False), max_length=30)
|
||||||
|
user_id: str = Field(sa_column=Column("UserId", VARCHAR, primary_key=True, nullable=False), max_length=32)
|
||||||
|
discord_member: bool = Field(sa_column=Column("DiscordMember", BOOLEAN, nullable=False))
|
||||||
|
event_points: int = Field(sa_column=Column("EventPoints", INTEGER(unsigned=True), nullable=False), default=0)
|
||||||
|
clan_points: int = Field(sa_column=Column("ClanPoints", INTEGER(unsigned=True), nullable=False), default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class Message(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Table is used to map folders and files to channels and messages in discord.
|
||||||
|
|
||||||
|
If folder name is stored in path, the corresponding id is a channel id. If the path is to a file, the id is for a
|
||||||
|
message.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
path: Either directory or file pathname.
|
||||||
|
id: Channel or message id depending on the path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "Messages"
|
||||||
|
|
||||||
|
path: str = Field(sa_column=Column("Path", VARCHAR, primary_key=True, nullable=False), max_length=100)
|
||||||
|
id: int = Field(sa_column=Column("Id", BIGINT(unsigned=True), nullable=False))
|
||||||
|
|
||||||
|
|
||||||
|
class PointsLimit(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
Table tracks how many points a user has gained in a week.
|
||||||
|
|
||||||
|
Council didn't want users spamming points, so they added a limit to how many can be gained in a week. This table
|
||||||
|
tracks point gain per week.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
user_id: The user that earned points. Is a discord id if the user is a discord member, otherwise their rsn.
|
||||||
|
week: week number/year representation of the week. Ex: 04/2025.
|
||||||
|
event_points: Number of event points earned during this week.
|
||||||
|
clan_points: Number of clan points earned during this week.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_id: str = Field(sa_column=Column("UserId", VARCHAR, primary_key=True, nullable=False), max_length=32)
|
||||||
|
|
||||||
|
# Maybe should break this out into two columns, week and year. Better data validation and more clear.
|
||||||
|
week: str = Field(sa_column=Column("Week", VARCHAR, primary_key=True, nullable=False), max_length=7)
|
||||||
|
event_points: int = Field(sa_column=Column("EventPoints", INTEGER(unsigned=True), nullable=False), default=0)
|
||||||
|
clan_points: int = Field(sa_column=Column("ClanPoints", INTEGER(unsigned=True), nullable=False), default=0)
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
"""Simple file to set up the sqlalchemy connection to database."""
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.ext.automap import automap_base
|
|
||||||
|
|
||||||
# connect with sql alchemy and autoload schema
|
|
||||||
URL = "sqlite+pysqlite:///database/dakiny.db"
|
|
||||||
engine = create_engine(URL)
|
|
||||||
Base = automap_base()
|
|
||||||
Base.prepare(autoload_with=engine)
|
|
||||||
|
|
||||||
# define the classes
|
|
||||||
Points = Base.classes.Points
|
|
||||||
PointsLimit = Base.classes.PointsLimit
|
|
||||||
CouncilMembers = Base.classes.CouncilMembers
|
|
||||||
ElectionInformation = Base.classes.ElectionInformation
|
|
||||||
Messages = Base.classes.Messages
|
|
||||||
Presences = Base.classes.Presences
|
|
||||||
UserBirthday = Base.classes.UserBirthday
|
|
||||||
Votes = Base.classes.Votes
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""Creates logger for Dakiny bot."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
dakiny_logger = logging.getLogger("default")
|
||||||
|
dakiny_logger.setLevel(logging.DEBUG if os.getenv("DEBUG") == "true" else logging.INFO)
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s:%(levelname)s [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
|
||||||
|
)
|
||||||
|
dakiny_logger.addHandler(handler)
|
||||||
+117
-90
@@ -1,7 +1,7 @@
|
|||||||
"""File contains util functions."""
|
"""File contains util functions."""
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
import os
|
import pathlib
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
@@ -9,19 +9,15 @@ from pathlib import Path
|
|||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import nextcord
|
import nextcord
|
||||||
from sqlalchemy import select, true, update
|
from sqlalchemy import select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlmodel import col
|
||||||
|
|
||||||
from dakiny_bot.database.sql_data_classes import (
|
from dakiny_bot.database.db import engine
|
||||||
CouncilMembers,
|
from dakiny_bot.database.models import CouncilMember, Message, Points, PointsLimit, Vote
|
||||||
Messages,
|
from dakiny_bot.logger import dakiny_logger
|
||||||
Points,
|
|
||||||
PointsLimit,
|
|
||||||
Votes,
|
|
||||||
engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .DakinySettings import settings
|
from .dakiny_settings import settings
|
||||||
|
|
||||||
NOMINEE_SPLITTER = "========================================\n\n"
|
NOMINEE_SPLITTER = "========================================\n\n"
|
||||||
REPO_DIR = "./dakiny-messages"
|
REPO_DIR = "./dakiny-messages"
|
||||||
@@ -34,20 +30,22 @@ VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
|
|||||||
PointType = StrEnum("PointType", ["CLAN", "EVENT"])
|
PointType = StrEnum("PointType", ["CLAN", "EVENT"])
|
||||||
|
|
||||||
|
|
||||||
def get_datetime(day: str) -> datetime:
|
# TODO(imcompiling): resolve returning None from this function
|
||||||
|
# https://gitea.cheezers.org/imcompiling/dakiny-bot/issues/102
|
||||||
|
def get_datetime(month_day: str) -> datetime: # pyright: ignore[reportReturnType]
|
||||||
"""
|
"""
|
||||||
Takes in a string month and day and returns a datetime object of that Month/Day this year.
|
Takes in a string month and day and returns a datetime object of that Month/Day this year.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
day (str): Month/Day in that format.
|
month_day (str): Month/Day in that format.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
datetime: Datetime representation of Month/Day.
|
datetime: Datetime representation of Month/Day.
|
||||||
"""
|
"""
|
||||||
today = datetime.now(tz=UTC).date()
|
today = datetime.now(tz=UTC).date()
|
||||||
year = today.year
|
year = today.year
|
||||||
month = int(day.split("/")[0])
|
month = int(month_day.split("/")[0])
|
||||||
day = int(day.split("/")[1])
|
day = int(month_day.split("/")[1])
|
||||||
|
|
||||||
# ensure that year rolls over if current month is December and the next month is less than 12.
|
# ensure that year rolls over if current month is December and the next month is less than 12.
|
||||||
if today.month == calendar.DECEMBER and month < calendar.OCTOBER:
|
if today.month == calendar.DECEMBER and month < calendar.OCTOBER:
|
||||||
@@ -55,8 +53,10 @@ def get_datetime(day: str) -> datetime:
|
|||||||
|
|
||||||
if today.month == calendar.JANUARY and month > calendar.MARCH:
|
if today.month == calendar.JANUARY and month > calendar.MARCH:
|
||||||
year -= 1
|
year -= 1
|
||||||
|
try:
|
||||||
return datetime(year, month, day, tzinfo=UTC)
|
return datetime(year, month, day, tzinfo=UTC)
|
||||||
|
except ValueError:
|
||||||
|
dakiny_logger.warning("Invalid month day: %s.", month_day)
|
||||||
|
|
||||||
|
|
||||||
def get_election_name() -> str:
|
def get_election_name() -> str:
|
||||||
@@ -92,12 +92,14 @@ def get_active_election_cycle() -> int:
|
|||||||
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)
|
today = datetime.now(tz=UTC)
|
||||||
|
dakiny_logger.debug("Today is %s", today)
|
||||||
|
|
||||||
for cycle, election in enumerate(settings.election_cycles):
|
for cycle, election in enumerate(settings.election_cycles):
|
||||||
if (
|
if (
|
||||||
today.date() >= get_datetime(election.election_start).date()
|
today.date() >= get_datetime(election.election_start).date()
|
||||||
and today.date() <= get_datetime(election.term_start).date()
|
and today.date() <= get_datetime(election.term_start).date()
|
||||||
):
|
):
|
||||||
|
dakiny_logger.debug("Cycle is %d", cycle)
|
||||||
return cycle
|
return cycle
|
||||||
|
|
||||||
# It isn't a current election, get the quarter
|
# It isn't a current election, get the quarter
|
||||||
@@ -114,14 +116,19 @@ def get_quarter_cycle() -> int:
|
|||||||
int: cycle index for current cycle.
|
int: cycle index for current cycle.
|
||||||
"""
|
"""
|
||||||
today = datetime.now(tz=UTC)
|
today = datetime.now(tz=UTC)
|
||||||
|
dakiny_logger.debug("Today is %s", today)
|
||||||
for cycle, election in enumerate(settings.election_cycles):
|
for cycle, election in enumerate(settings.election_cycles):
|
||||||
if (
|
if (
|
||||||
today.date() >= get_datetime(election.term_start).date()
|
today.date() >= get_datetime(election.term_start).date()
|
||||||
and today.date() <= get_datetime(election.term_end).date()
|
and today.date() <= get_datetime(election.term_end).date()
|
||||||
):
|
):
|
||||||
|
dakiny_logger.debug("Cycle is %d", cycle)
|
||||||
return cycle
|
return cycle
|
||||||
|
|
||||||
return None
|
dakiny_logger.debug("Quarter cycle is None.")
|
||||||
|
# TODO(imcompiling) Add None to return signature or find better return value
|
||||||
|
# https://gitea.cheezers.org/imcompiling/dakiny-bot/issues/102
|
||||||
|
return None # pyright: ignore[reportReturnType]
|
||||||
|
|
||||||
|
|
||||||
def build_votes_str(guild: nextcord.Guild) -> str:
|
def build_votes_str(guild: nextcord.Guild) -> str:
|
||||||
@@ -139,19 +146,20 @@ def build_votes_str(guild: nextcord.Guild) -> str:
|
|||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(CouncilMembers).where(CouncilMembers.ElectionName == election_name)
|
stmt = select(CouncilMember).where(col(CouncilMember.election_name == election_name))
|
||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
|
# Unsure how to None check in the lambda.
|
||||||
|
key=lambda nominee: guild.get_member(nominee.member_id).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
):
|
):
|
||||||
votes[nominee.MemberId] = 0
|
votes[nominee.member_id] = 0
|
||||||
|
|
||||||
stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == VoteType.COUNCIL)
|
stmt = select(Vote).where(col(Vote.election_name == election_name)).where(col(Vote.type == VoteType.COUNCIL))
|
||||||
|
|
||||||
for vote in session.scalars(stmt):
|
for vote in session.scalars(stmt):
|
||||||
if vote.VotedForId is not None:
|
if vote.voted_for_id is not None:
|
||||||
votes[vote.VotedForId] += 1
|
votes[vote.voted_for_id] += 1
|
||||||
|
|
||||||
votes_str: str = "===================="
|
votes_str: str = "===================="
|
||||||
votes_str += user_vote_str(votes)
|
votes_str += user_vote_str(votes)
|
||||||
@@ -176,46 +184,52 @@ def build_highcouncil_votes_str(guild: nextcord.Guild) -> str:
|
|||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
.filter(col(CouncilMember.clan_leader_speech_id is not None))
|
||||||
)
|
)
|
||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
|
# Unsure how to None check in the lambda.
|
||||||
|
key=lambda nominee: guild.get_member(nominee.member_id).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
):
|
):
|
||||||
cl_votes[nominee.MemberId] = 0
|
cl_votes[nominee.member_id] = 0
|
||||||
|
|
||||||
stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == VoteType.CLANLEADER)
|
stmt = select(Vote).where(col(Vote.election_name == election_name)).where(col(Vote.type == VoteType.CLANLEADER))
|
||||||
|
|
||||||
for vote in session.scalars(stmt):
|
for vote in session.scalars(stmt):
|
||||||
if vote.VotedForId is not None:
|
if vote.voted_for_id is not None:
|
||||||
cl_votes[vote.VotedForId] += 1
|
cl_votes[vote.voted_for_id] += 1
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMember)
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
.filter(col(CouncilMember.discord_manager_speech_id is not None))
|
||||||
)
|
)
|
||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
|
# Unsure how to None check in the lambda.
|
||||||
|
key=lambda nominee: guild.get_member(nominee.member_id).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
):
|
):
|
||||||
dm_votes[nominee.MemberId] = 0
|
dm_votes[nominee.member_id] = 0
|
||||||
|
|
||||||
stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == VoteType.DISCORDMANAGER)
|
stmt = (
|
||||||
|
select(Vote)
|
||||||
|
.where(col(Vote.election_name == election_name))
|
||||||
|
.where(col(Vote.type == VoteType.DISCORDMANAGER))
|
||||||
|
)
|
||||||
|
|
||||||
for vote in session.scalars(stmt):
|
for vote in session.scalars(stmt):
|
||||||
if vote.VotedForId is not None:
|
if vote.voted_for_id is not None:
|
||||||
dm_votes[vote.VotedForId] += 1
|
dm_votes[vote.voted_for_id] += 1
|
||||||
|
|
||||||
# build the votes string
|
# build the votes string
|
||||||
votes_str: str = "Clan Leader Votes\n===================="
|
votes_str: str = "Clan Leader Vote\n===================="
|
||||||
votes_str += user_vote_str(cl_votes)
|
votes_str += user_vote_str(cl_votes)
|
||||||
|
|
||||||
votes_str += "\n\nDiscord Manager Votes\n===================="
|
votes_str += "\n\nDiscord Manager Vote\n===================="
|
||||||
votes_str += user_vote_str(dm_votes)
|
votes_str += user_vote_str(dm_votes)
|
||||||
|
|
||||||
return votes_str
|
return votes_str
|
||||||
@@ -309,7 +323,7 @@ def record_points(users: Sequence[str], add_points: int, point_type: PointType,
|
|||||||
quarter_name = get_quarter_name()
|
quarter_name = get_quarter_name()
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
for user in users:
|
for user in users:
|
||||||
stmt = select(Points).where(Points.UserId == user).where(Points.QuarterName == quarter_name)
|
stmt = select(Points).where(col(Points.user_id == user)).where(col(Points.quarter_name == quarter_name))
|
||||||
|
|
||||||
points_obj = session.scalar(stmt)
|
points_obj = session.scalar(stmt)
|
||||||
add_points = get_allowed_points(session, point_type, user, add_points)
|
add_points = get_allowed_points(session, point_type, user, add_points)
|
||||||
@@ -317,16 +331,16 @@ def record_points(users: Sequence[str], add_points: int, point_type: PointType,
|
|||||||
add_event_points = add_points if point_type == PointType.EVENT else 0
|
add_event_points = add_points if point_type == PointType.EVENT else 0
|
||||||
if points_obj is None:
|
if points_obj is None:
|
||||||
new_points = Points(
|
new_points = Points(
|
||||||
QuarterName=quarter_name,
|
quarter_name=quarter_name,
|
||||||
UserId=user,
|
user_id=user,
|
||||||
DiscordMember=is_member,
|
discord_member=is_member,
|
||||||
EventPoints=add_event_points,
|
event_points=add_event_points,
|
||||||
ClanPoints=add_clan_points,
|
clan_points=add_clan_points,
|
||||||
)
|
)
|
||||||
session.add(new_points)
|
session.add(new_points)
|
||||||
else:
|
else:
|
||||||
points_obj.EventPoints += add_event_points
|
points_obj.event_points += add_event_points
|
||||||
points_obj.ClanPoints += add_clan_points
|
points_obj.clan_points += add_clan_points
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -356,7 +370,7 @@ def get_allowed_points(session: Session, point_type: PointType, user: str, point
|
|||||||
if point_type == PointType.EVENT
|
if point_type == PointType.EVENT
|
||||||
else settings.point_settings.clan_points_weekly_limit
|
else settings.point_settings.clan_points_weekly_limit
|
||||||
)
|
)
|
||||||
stmt = select(PointsLimit).where(PointsLimit.UserId == user).where(PointsLimit.Week == this_week)
|
stmt = select(PointsLimit).where(col(PointsLimit.user_id == user)).where(col(PointsLimit.week == this_week))
|
||||||
|
|
||||||
# if under limit, can add up to however many points would make the current limit equal to the
|
# if under limit, can add up to however many points would make the current limit equal to the
|
||||||
# allowed limit.
|
# allowed limit.
|
||||||
@@ -364,16 +378,16 @@ def get_allowed_points(session: Session, point_type: PointType, user: str, point
|
|||||||
if limit_obj is None:
|
if limit_obj is None:
|
||||||
clan_points = points if point_type == PointType.CLAN else 0
|
clan_points = points if point_type == PointType.CLAN else 0
|
||||||
event_points = points if point_type == PointType.EVENT else 0
|
event_points = points if point_type == PointType.EVENT else 0
|
||||||
new_limit = PointsLimit(UserId=user, Week=this_week, EventPoints=event_points, ClanPoints=clan_points)
|
new_limit = PointsLimit(user_id=user, week=this_week, event_points=event_points, clan_points=clan_points)
|
||||||
add_points = points
|
add_points = points
|
||||||
session.add(new_limit)
|
session.add(new_limit)
|
||||||
else:
|
else:
|
||||||
curr_points = limit_obj.EventPoints if point_type == PointType.EVENT else limit_obj.ClanPoints
|
curr_points = limit_obj.event_points if point_type == PointType.EVENT else limit_obj.clan_points
|
||||||
add_points = points if curr_points + points <= points_limit else (max(0, points_limit - curr_points))
|
add_points = points if curr_points + points <= points_limit else (max(0, points_limit - curr_points))
|
||||||
if point_type == PointType.EVENT:
|
if point_type == PointType.EVENT:
|
||||||
limit_obj.EventPoints += add_points
|
limit_obj.event_points += add_points
|
||||||
else:
|
else:
|
||||||
limit_obj.ClanPoints += add_points
|
limit_obj.clan_points += add_points
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -402,8 +416,8 @@ async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[
|
|||||||
dir_path = f"{MESSAGES_DIR}/{dir_name}"
|
dir_path = f"{MESSAGES_DIR}/{dir_name}"
|
||||||
channel = await get_clan_message_channel(session, dir_name, guild)
|
channel = await get_clan_message_channel(session, dir_name, guild)
|
||||||
sorted_files = sorted(
|
sorted_files = sorted(
|
||||||
os.listdir(dir_path),
|
pathlib.Path(dir_path).iterdir(),
|
||||||
key=lambda filename: int(filename.split(".")[0]),
|
key=lambda file: int(file.name.split(".")[0]),
|
||||||
)
|
)
|
||||||
|
|
||||||
for file_name in sorted_files:
|
for file_name in sorted_files:
|
||||||
@@ -432,41 +446,49 @@ async def get_clan_message_channel(session: Session, channel_name: str, guild: n
|
|||||||
Returns:
|
Returns:
|
||||||
nextcord.TextChannel: Channel object with name channel_name.
|
nextcord.TextChannel: Channel object with name channel_name.
|
||||||
"""
|
"""
|
||||||
stmt = select(Messages.Id).where(Messages.Path == channel_name)
|
stmt = select(col(Message.id)).where(col(Message.path == channel_name))
|
||||||
channel_id = session.scalar(stmt)
|
channel_id = session.scalar(stmt)
|
||||||
|
ret_channel = None
|
||||||
if channel_id is None:
|
if channel_id is None:
|
||||||
ret_channel = None
|
|
||||||
# channel is not tracked. see if it exists
|
# channel is not tracked. see if it exists
|
||||||
channels = await guild.fetch_channels()
|
channels = await guild.fetch_channels()
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
if channel.name == channel_name and channel.__class__ is nextcord.TextChannel:
|
if channel.name == channel_name and isinstance(channel, nextcord.TextChannel):
|
||||||
ret_channel = channel
|
ret_channel = channel
|
||||||
|
|
||||||
if ret_channel is None:
|
if ret_channel is None:
|
||||||
# channel doesn't exist, we must create it
|
# channel doesn't exist, we must create it
|
||||||
ret_channel = await guild.create_text_channel(
|
ret_channel = await guild.create_text_channel(
|
||||||
channel_name,
|
channel_name,
|
||||||
reason="Creating channel as required for by design of Dakiny-Messages repo.",
|
reason="Creating channel as required for by design of Dakiny-Message repo.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# record channel information
|
# record channel information
|
||||||
new_item = Messages(Path=channel_name, Id=ret_channel.id)
|
new_item = Message(path=channel_name, id=ret_channel.id)
|
||||||
session.add(new_item)
|
session.add(new_item)
|
||||||
session.commit()
|
session.commit()
|
||||||
return ret_channel
|
|
||||||
|
|
||||||
try:
|
else:
|
||||||
return await guild.fetch_channel(channel_id)
|
try:
|
||||||
except nextcord.NotFound:
|
ret_channel = await guild.fetch_channel(channel_id)
|
||||||
# the channel was deleted for some reason
|
if not isinstance(ret_channel, nextcord.TextChannel):
|
||||||
ret_channel = await guild.create_text_channel(
|
dakiny_logger.warning(
|
||||||
channel_name,
|
"Channel %s is not a text channel. Does not work for clan messages.", ret_channel.name
|
||||||
reason="Creating channel as required for by design of Dakiny-Messages repo.",
|
)
|
||||||
)
|
# TODO(imcompiling) Figure out how to handle returning None here. Maybe delete channel and remake it?
|
||||||
stmt = update(Messages).where(Messages.Path == channel_name).values(Id=ret_channel.id)
|
# https://gitea.cheezers.org/imcompiling/dakiny-bot/issues/102
|
||||||
session.execute(stmt)
|
return None # pyright: ignore[reportReturnType]
|
||||||
session.commit()
|
except nextcord.NotFound:
|
||||||
return ret_channel
|
# the channel was deleted for some reason
|
||||||
|
ret_channel = await guild.create_text_channel(
|
||||||
|
channel_name,
|
||||||
|
reason="Creating channel as required for by design of Dakiny-Message repo.",
|
||||||
|
)
|
||||||
|
stmt = update(Message).where(col(Message.path == channel_name)).values(id=ret_channel.id)
|
||||||
|
session.execute(stmt)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return ret_channel
|
||||||
|
|
||||||
|
|
||||||
async def edit_clan_message(
|
async def edit_clan_message(
|
||||||
@@ -488,21 +510,21 @@ async def edit_clan_message(
|
|||||||
message_name (str): Name of the message. Corresponds to the file that contains message content.
|
message_name (str): Name of the message. Corresponds to the file that contains message content.
|
||||||
message_content (str): Content of the message to edit/send.
|
message_content (str): Content of the message to edit/send.
|
||||||
"""
|
"""
|
||||||
stmt = select(Messages.Id).where(Messages.Path == message_name)
|
stmt = select(col(Message.id)).where(col(Message.path == message_name))
|
||||||
msg_id = session.scalar(stmt)
|
msg_id = session.scalar(stmt)
|
||||||
if msg_id is None:
|
if msg_id is None:
|
||||||
# don't have a message for this file
|
# don't have a message for this file
|
||||||
msg = await channel.send(message_content)
|
msg = await channel.send(message_content)
|
||||||
new_message = Messages(Path=message_name, Id=msg.id)
|
new_message = Message(path=message_name, id=msg.id)
|
||||||
session.add(new_message)
|
session.add(new_message)
|
||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
msg: nextcord.Message = await channel.fetch_message(msg_id)
|
msg: nextcord.Message = await channel.fetch_message(msg_id)
|
||||||
await msg.edit(message_content)
|
await msg.edit(content=message_content)
|
||||||
except nextcord.NotFound:
|
except nextcord.NotFound:
|
||||||
msg = await channel.send(message_content)
|
msg = await channel.send(message_content)
|
||||||
stmt = update(Messages).where(Messages.Path == message_name).values(Id=msg.id)
|
stmt = update(Message).where(col(Message.path == message_name)).values(id=msg.id)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -514,30 +536,35 @@ async def edit_clan_council_info(guild: nextcord.Guild) -> None:
|
|||||||
Args:
|
Args:
|
||||||
guild (nextcord.Guild): Guild object that to use to get channel.
|
guild (nextcord.Guild): Guild object that to use to get channel.
|
||||||
"""
|
"""
|
||||||
clan_info_channel: nextcord.TextChannel = await guild.fetch_channel(settings.channel_ids.clan_info)
|
clan_info_channel = await guild.fetch_channel(settings.channel_ids.clan_info)
|
||||||
|
|
||||||
|
if not isinstance(clan_info_channel, nextcord.TextChannel):
|
||||||
|
return
|
||||||
|
|
||||||
election_name = get_election_name()
|
election_name = get_election_name()
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(col(CouncilMember.member_id))
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.DiscordManager == true())
|
.where(col(CouncilMember.discord_manager is True))
|
||||||
)
|
)
|
||||||
discord_manager = session.scalar(stmt)
|
discord_manager = session.scalar(stmt)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(col(CouncilMember.member_id))
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.ClanLeader == true())
|
.where(col(CouncilMember.clan_leader is True))
|
||||||
)
|
)
|
||||||
clan_leader = session.scalar(stmt)
|
clan_leader = session.scalar(stmt)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(col(CouncilMember.member_id))
|
||||||
.where(CouncilMembers.ElectionName == election_name)
|
.where(col(CouncilMember.election_name == election_name))
|
||||||
.where(CouncilMembers.Elected == true())
|
.where(col(CouncilMember.elected is True))
|
||||||
)
|
)
|
||||||
|
|
||||||
council_member_ids = sorted(
|
council_member_ids = sorted(
|
||||||
session.scalars(stmt), key=lambda user_id: guild.get_member(user_id).display_name.lower()
|
session.scalars(stmt),
|
||||||
|
# Unsure how to None check in the lambda.
|
||||||
|
key=lambda user_id: guild.get_member(user_id).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess]
|
||||||
)
|
)
|
||||||
|
|
||||||
council_members_strs = [
|
council_members_strs = [
|
||||||
|
|||||||
+2
-1
@@ -9,10 +9,11 @@ dependencies = [
|
|||||||
"pytz<2025.3,>=2025.2",
|
"pytz<2025.3,>=2025.2",
|
||||||
"sqlalchemy<3.0.0,>=2.0.30",
|
"sqlalchemy<3.0.0,>=2.0.30",
|
||||||
"aiofiles<24.0.0,>=23.2.1",
|
"aiofiles<24.0.0,>=23.2.1",
|
||||||
"setuptools<80.8.1,>=80.8.0",
|
"setuptools<80.9.1,>=80.9.0",
|
||||||
"dataclass-wizard[yaml]>=0.22.3,<1.0.0",
|
"dataclass-wizard[yaml]>=0.22.3,<1.0.0",
|
||||||
"uvicorn>=0.34.0",
|
"uvicorn>=0.34.0",
|
||||||
"audioop-lts>=0.2.1",
|
"audioop-lts>=0.2.1",
|
||||||
|
"sqlmodel>=0.0.24",
|
||||||
]
|
]
|
||||||
name = "dakiny-bot"
|
name = "dakiny-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
+12
-3
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"automerge": true,
|
"automerge": true
|
||||||
"enabled": true
|
},
|
||||||
|
"major": {
|
||||||
|
"automerge": false,
|
||||||
|
"automergeType": "pr"
|
||||||
},
|
},
|
||||||
"minor": {
|
"minor": {
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
@@ -16,5 +19,11 @@
|
|||||||
"automerge": true,
|
"automerge": true,
|
||||||
"automergeType": "branch"
|
"automergeType": "branch"
|
||||||
},
|
},
|
||||||
"ignoreTests": true
|
"ignoreTests": true,
|
||||||
|
"dockerfile": {
|
||||||
|
"minor": {
|
||||||
|
"automerge": false,
|
||||||
|
"automergeType": "pr"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +170,7 @@ dependencies = [
|
|||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
{ name = "setuptools" },
|
{ name = "setuptools" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
|
{ name = "sqlmodel" },
|
||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -188,8 +189,9 @@ requires-dist = [
|
|||||||
{ name = "nextcord", specifier = ">=2.6.0,<3.0.0" },
|
{ name = "nextcord", specifier = ">=2.6.0,<3.0.0" },
|
||||||
{ name = "pandas", specifier = ">=2.2.2,<3.0.0" },
|
{ name = "pandas", specifier = ">=2.2.2,<3.0.0" },
|
||||||
{ name = "pytz", specifier = ">=2025.2,<2025.3" },
|
{ name = "pytz", specifier = ">=2025.2,<2025.3" },
|
||||||
{ name = "setuptools", specifier = ">=80.8.0,<80.8.1" },
|
{ name = "setuptools", specifier = ">=80.9.0,<80.9.1" },
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0.30,<3.0.0" },
|
{ name = "sqlalchemy", specifier = ">=2.0.30,<3.0.0" },
|
||||||
|
{ name = "sqlmodel", specifier = ">=0.0.24" },
|
||||||
{ name = "uvicorn", specifier = ">=0.34.0" },
|
{ name = "uvicorn", specifier = ">=0.34.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -508,11 +510,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "80.8.0"
|
version = "80.9.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 }
|
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 },
|
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -562,6 +564,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 },
|
{ url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlmodel"
|
||||||
|
version = "0.0.24"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.41.3"
|
version = "0.41.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user