Compare commits

..
Author SHA1 Message Date
imcompiling 1f1e4ad20d Feat: use db models in voting views.
build_develop / build (push) Successful in 39s
fix pylance smells.
2025-07-31 13:50:34 -04:00
imcompiling 354823ca1f Feat: use models in voting highcouncil views.
build_develop / build (push) Successful in 40s
2025-07-31 13:48:11 -04:00
imcompiling 45f17e09d3 Feat: Update Nominate classes to use models.
build_develop / build (push) Successful in 44s
2025-07-24 14:28:39 -04:00
imcompiling 530ca5b172 Feat: Update file to use new models.
build_develop / build (push) Successful in 38s
2025-07-24 13:46:13 -04:00
imcompiling 3dfdc10334 Feat: Swap Election to use new models.
build_develop / build (push) Successful in 1m7s
Address many pylance errors. Rewrite a few functions, will require testing to ensure
functionality remains the same.
2025-07-24 12:39:16 -04:00
imcompiling 6c9353990c Fix: dakiny_settings file rename.
build_develop / build (push) Successful in 34s
2025-07-17 09:18:33 -04:00
imcompiling 5412ab14a3 Feat: Update uv.lock.
build_develop / build (push) Successful in 45s
2025-07-17 09:14:51 -04:00
imcompiling 16cdc27bdb Feat: Update utils to use new models.
Address many pylance smells. Ignoring some I don't know how to handle,
added some TODOs for handling others later.
2025-07-17 09:14:51 -04:00
imcompiling 086f8b50cd Fix: Add default of None to speech ids. 2025-07-17 09:14:51 -04:00
imcompiling a6a73cb46e Feat: Rename DakinySettings -> dakiny_settings.
Also handle the list error from pylance.
2025-07-17 09:14:51 -04:00
imcompiling 57e19f3036 Feat: Remove pylance warnings in app.py.
Also removed old database setup.
2025-07-17 09:14:51 -04:00
imcompiling 51c8483dad Remove: Old database files. 2025-07-17 09:14:51 -04:00
imcompiling 6b60c37726 Fix: errors on models from generating database. 2025-07-17 09:14:51 -04:00
imcompiling 3eea225931 Feat: Add database engine. 2025-07-17 09:14:51 -04:00
imcompiling 0529dc6b4c Add: ondelete to spelling settings. 2025-07-17 09:14:51 -04:00
imcompiling 865fee4ce2 Add: PointsLimit model. 2025-07-17 09:14:51 -04:00
imcompiling 052202d273 Fix: Add default to points. 2025-07-17 09:14:51 -04:00
imcompiling c09dd94284 Add: Messages model. 2025-07-17 09:14:51 -04:00
imcompiling 76f4a9f37f Add: Points model. 2025-07-17 09:14:51 -04:00
imcompiling f446f8e9cc Fix: Add boolean column types. 2025-07-17 09:14:51 -04:00
imcompiling dbd1d6dc00 Add: Presence model. 2025-07-17 09:14:51 -04:00
imcompiling 66621ea2a0 Add: UserBirthday model. 2025-07-17 09:14:51 -04:00
imcompiling 6ae56bf8b2 Add: Vote model. 2025-07-17 09:14:51 -04:00
imcompiling f472c0419b Add: CouncilMembers model. 2025-07-17 09:14:51 -04:00
imcompiling a7532773c1 Feat: Add ElectionInformation table. 2025-07-17 09:14:51 -04:00
imcompiling 81ad4ba313 Feat: Install sqlmodel. 2025-07-17 09:14:51 -04:00
imcompiling b221f1e438 Feat: Update the ban message.
build_develop / build (push) Successful in 37s
build_latest / build (push) Successful in 21s
build_release / build (push) Successful in 22s
For some reason, the user's name was None. I'm guessing because they were
banned and are no longer a member or something. Don't need to record the member's
name anyway though, as its already a part of the audit log.
2025-07-16 21:24:03 -04:00
imcompiling 1b08ba1355 Feat: Set delete messages to 30 minutes.
build_develop / build (push) Successful in 32s
2025-07-16 16:03:26 -04:00
imcompiling e191df1a38 Fix: spelling and ignore pylance warning.
build_develop / build (push) Successful in 32s
2025-07-16 15:57:31 -04:00
imcompiling e35a2a8c8d Feat: Add autoban
build_develop / build (push) Successful in 38s
2025-07-16 15:55:09 -04:00
imcompiling d7046e4595 Add: config for ban channel id. 2025-07-16 15:45:35 -04:00
imcompiling 70037fb47a Update renovate.json
build_latest / build (push) Successful in 19s
Don't automerge major versions, don't automerge docker minor versions.
2025-07-15 09:05:24 -04:00
renovate-bot 4ae3866e44 Update ghcr.io/astral-sh/uv Docker tag to v0.7.20
build_latest / build (push) Successful in 40s
2025-07-13 04:00:48 +00:00
renovate-bot 1349813f0d Update ghcr.io/astral-sh/uv Docker tag to v0.7.19
build_latest / build (push) Successful in 1m0s
2025-07-06 04:00:49 +00:00
renovate-bot 294b672fe2 Update ghcr.io/astral-sh/uv Docker tag to v0.7.16
build_latest / build (push) Successful in 45s
2025-06-29 04:00:48 +00:00
renovate-bot 72349a4cae Update python Docker tag to v3.13.5
build_latest / build (push) Successful in 1m4s
2025-06-15 04:01:25 +00:00
renovate-bot f6e9d3c32e Update ghcr.io/astral-sh/uv Docker tag to v0.7.13
build_latest / build (push) Waiting to run
2025-06-15 04:01:19 +00:00
renovate-bot 7c8455bf29 Update python Docker tag to v3.13.4
build_latest / build (push) Successful in 51s
2025-06-08 04:01:23 +00:00
renovate-bot f4c4a0435b Update ghcr.io/astral-sh/uv Docker tag to v0.7.12
build_latest / build (push) Waiting to run
2025-06-08 04:01:18 +00:00
renovate-bot bbc63dd77a Update dependency setuptools to <80.9.1,>=80.9.0
build_latest / build (push) Successful in 39s
2025-06-01 04:01:50 +00:00
renovate-bot 7ed3f0d613 Update ghcr.io/astral-sh/uv Docker tag to v0.7.9
build_latest / build (push) Waiting to run
2025-06-01 04:01:17 +00:00
imcompiling eb6a8c7479 Feat: Add logging to tell when refresh happens.
build_latest / build (push) Successful in 44s
build_release / build (push) Successful in 22s
2025-05-27 23:33:19 -04:00
imcompiling 58b230afe6 Fix: Catch git error and refresh connection.
build_latest / build (push) Successful in 39s
build_release / build (push) Successful in 23s
I think that the git error is just happening from timing out. Trying to fix by
refreshing the connection when it happens.
2025-05-27 23:31:02 -04:00
imcompiling fb8dfef55e Fix: daily test runs daily loop.
build_develop / build (push) Successful in 42s
build_latest / build (push) Successful in 19s
build_release / build (push) Successful in 20s
2025-05-27 23:13:39 -04:00
imcompiling 487d174b6d Fix: Remove name clobber. Add a try except.
build_develop / build (push) Successful in 40s
Also add environment variable to launch settings.
2025-05-26 21:15:15 -04:00
imcompiling abb50d71b4 Feat: Update logging.
build_develop / build (push) Successful in 36s
Narrowing down where the issue is. Active cycle is being obtained, but
nothing was happening with it. No more debug messages either.
2025-05-26 20:40:12 -04:00
imcompiling 7409e2fd2d Feat: Add filename, lineno, and function name to logging. 2025-05-26 20:34:45 -04:00
imcompiling 6a67804937 Fix: Import logger correctly.
build_develop / build (push) Successful in 42s
2025-05-25 22:31:50 -04:00
imcompiling 4f4d76d151 Feat: Add some logging.
build_develop / build (push) Successful in 43s
Trying to figure out why the elections are not starting.
2025-05-25 22:24:26 -04:00
imcompiling f3985b5d21 Feat: Set debug if env var is true. 2025-05-25 22:15:12 -04:00
imcompiling 142cc337a0 Feat: Add dakiny_bot logger. 2025-05-25 22:11:37 -04:00
imcompiling fd1d1c075e Feat: Remove logger from app.py. It existed it main.j 2025-05-25 22:11:18 -04:00
28 changed files with 838 additions and 396 deletions
+4 -1
View File
@@ -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"
}
} }
] ]
} }
+4 -1
View File
@@ -8,7 +8,10 @@
"CLANLEADER", "CLANLEADER",
"Dakiny", "Dakiny",
"DISCORDMANAGER", "DISCORDMANAGER",
"gitea",
"highcouncil", "highcouncil",
"nextcord" "imcompiling",
"nextcord",
"ondelete"
] ]
} }
+2 -2
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
"""Empty init file for classes module."""
+165 -96
View File
@@ -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))
+44 -19
View File
@@ -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
View File
@@ -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"]
+57 -45
View File
@@ -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(
+42 -29
View File
@@ -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(
+9 -2
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -6
View File
@@ -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()
+8 -1
View File
@@ -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:
+11 -2
View File
@@ -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
-13
View File
@@ -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()
+13
View File
@@ -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)
+218
View File
@@ -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)
-20
View File
@@ -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
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
} }
Generated
+19 -4
View File
@@ -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"