Compare commits

...
71 Commits
Author SHA1 Message Date
imcompiling 069dafe9ae Merge branch '56-announce-clan-council'
build_latest / build (push) Successful in 21s
build_release / build (push) Successful in 16s
build_develop / build (push) Successful in 17s
2024-05-16 16:10:30 -04:00
imcompiling 7d54a0c30b Create refresh council info command. Change method name.
build_develop / build (push) Successful in 59s
2024-05-16 16:10:16 -04:00
imcompiling 39fd55146e move edit clan council message into utils. 2024-05-16 16:04:08 -04:00
imcompiling a8323c9a88 Update the message to look more like current. 2024-05-16 15:55:09 -04:00
imcompiling 51d6713940 format returns the string. 2024-05-16 15:45:33 -04:00
imcompiling 65ed5e7b59 Made function to gather and send the council members message in the clan info channel. 2024-05-16 15:37:34 -04:00
imcompiling 2059814394 Broke edit clan message to own function to allow for usage in other places. 2024-05-16 15:36:50 -04:00
imcompiling 063baa07ee Merge branch '53-Not-recording-High-Council-results-in-CouncilMembers-database'
build_latest / build (push) Successful in 55s
build_develop / build (push) Successful in 16s
2024-05-16 14:53:55 -04:00
imcompiling 24ffb02869 Update CouncilMembers database with high council winners.
build_develop / build (push) Successful in 59s
2024-05-16 14:41:03 -04:00
imcompiling 370bd71154 The defer needs to be ephemeral too.
build_latest / build (push) Successful in 57s
build_release / build (push) Successful in 22s
2024-05-16 09:55:33 -04:00
imcompiling cb3eb4b09b Merge branch 'main' of https://gitea.cheezers.org/imcompiling/dakiny-bot
build_latest / build (push) Successful in 22s
build_release / build (push) Successful in 17s
build_develop / build (push) Successful in 22s
2024-05-16 09:19:11 -04:00
imcompiling c39e2c137b Merge branch '6-add-clan-rules' 2024-05-16 09:18:35 -04:00
imcompiling eedaf1c71f Don't need to refresh messages on bot start.
build_develop / build (push) Successful in 1m11s
2024-05-16 09:13:34 -04:00
imcompiling 70e4a84db3 Set uvicorn to listen on all addresses.
build_develop / build (push) Successful in 57s
2024-05-15 23:16:12 -04:00
imcompiling 443b92dfd3 Revert "Add expose to see if that fixes issue."
build_develop / build (push) Successful in 21s
This reverts commit e1be3218b3.
2024-05-15 20:43:19 -04:00
imcompiling e1be3218b3 Add expose to see if that fixes issue.
build_develop / build (push) Successful in 1m10s
2024-05-15 20:40:31 -04:00
imcompiling dfd4ef9347 Need to defer followup on refresh because it takes a while. 2024-05-15 17:27:35 -04:00
imcompiling 094a22f5e7 Update README.md
build_latest / build (push) Successful in 23s
2024-05-15 17:13:45 -04:00
imcompiling 7333100b1d Need git on the docker image now.
build_develop / build (push) Successful in 1m32s
2024-05-15 14:52:02 -04:00
imcompiling e5d885e2a8 Merge branch 'main' into 6-add-clan-rules
build_develop / build (push) Successful in 1m4s
2024-05-15 14:36:50 -04:00
imcompiling 592819e028 Update dockerfile to use main.
build_latest / build (push) Successful in 1m2s
2024-05-15 14:31:26 -04:00
imcompiling f822ed2e2d Correct sub command name. Add response.
build_develop / build (push) Failing after 19s
2024-05-15 14:17:27 -04:00
imcompiling 5726875c59 Store updated channel id. 2024-05-15 14:15:11 -04:00
imcompiling 52ee85f17c Add a refresh command. 2024-05-15 14:07:27 -04:00
imcompiling cf5b5f8068 Move update clan messages into utils, a slash command will need the functionality. 2024-05-15 13:57:51 -04:00
imcompiling ee6b301791 Fill out some docustrings 2024-05-15 13:46:13 -04:00
imcompiling b9362f7eca Handle a channel being deleted after entered into database. 2024-05-15 13:33:07 -04:00
imcompiling 36c738bfaa Finish the update messages function. 2024-05-15 13:27:18 -04:00
imcompiling a1f955abbe Fill out more of the update message method. 2024-05-14 18:15:58 -04:00
imcompiling 9ab4d9e339 Add the messages table to database. 2024-05-14 18:06:16 -04:00
imcompiling f51f66697f Start working on update_messages function. 2024-05-14 17:54:58 -04:00
imcompiling a3e195925a Get on the right branch 2024-05-14 17:17:51 -04:00
imcompiling 9d78070c45 Add gitpython requirement. Clone the repo. 2024-05-14 16:34:31 -04:00
imcompiling 4ae31159dc Move the webhook listener into a cog so that I have access to the bot for the action method. 2024-05-14 16:04:52 -04:00
imcompiling 3fec5fab41 Webhook is using external file.
build_develop / build (push) Failing after 16s
2024-05-14 12:19:11 -04:00
imcompiling b12e2427a3 Merge branch 'main' into 6-add-clan-rules
build_develop / build (push) Failing after 19s
2024-05-14 11:19:19 -04:00
imcompiling 967d41b8ef Rename dakiny_bot.py to main.py.
build_latest / build (push) Failing after 16s
2024-05-14 11:18:55 -04:00
imcompiling 4776b1857b Start using fastapi. 2024-05-14 11:16:10 -04:00
imcompiling 01687ae8e8 Reduce duplicate code when getting cycle name.
build_develop / build (push) Successful in 48s
2024-05-13 21:32:19 -04:00
imcompiling 3089da7282 Sort members by display name then non members.
build_develop / build (push) Successful in 1m16s
build_latest / build (push) Successful in 29s
build_release / build (push) Successful in 21s
change how view message looks.
2024-05-11 23:51:44 -04:00
imcompiling 24ae0fe21f Allow multiple quarters to be selected from search string, as long as they match the start with.
build_develop / build (push) Successful in 49s
2024-05-11 13:34:30 -04:00
imcompiling ad3da6f2c6 Hosts don't get participation points. 2024-05-11 13:31:39 -04:00
imcompiling 5057e9a844 Change pandas version.
build_develop / build (push) Successful in 50s
2024-05-10 08:49:11 -04:00
imcompiling fea590bf9a Add handling for bad quarter name.
build_develop / build (push) Successful in 48s
2024-05-09 23:33:23 -04:00
imcompiling 058bdfef50 Add auto complete for event points 2024-05-09 23:29:28 -04:00
imcompiling 257a7eccd7 Add view points command. 2024-05-09 23:27:06 -04:00
imcompiling 443609f5ba Fix small bugs with getting cycle name and recording events. 2024-05-09 20:57:36 -04:00
imcompiling fde17edf09 Have to make sure things exist before making list of them.
Don't delete the embed before done using it.
2024-05-09 20:53:48 -04:00
imcompiling 084c9df65c Record points in the command. 2024-05-09 20:34:16 -04:00
imcompiling 4f9524a4c1 Slight database change, add util functions for adding points to users. 2024-05-09 20:25:01 -04:00
imcompiling 1495fb7ee6 Updated all docustrings in record.py. 2024-05-09 17:13:59 -04:00
imcompiling fa312eec87 Change database to handle non-discord participants. 2024-05-09 17:00:20 -04:00
imcompiling eb3cf2b77f Clear submittion view after submitted.
build_develop / build (push) Successful in 36s
2024-05-09 16:52:44 -04:00
imcompiling c08cd84bed Cleaned up embed and event record message. 2024-05-09 16:51:07 -04:00
imcompiling 905314cfb5 Create custom user select to edit the embed with selected people. 2024-05-09 16:37:45 -04:00
imcompiling bd3debf025 Change up recording process. use a modal to edit text values. 2024-05-09 16:26:20 -04:00
imcompiling 3a3c4b7995 Remove hard coded guild id. 2024-05-08 21:43:37 -04:00
imcompiling f465117435 Added basic event recording. No points being tracked yet.
build_develop / build (push) Successful in 34s
2024-05-08 21:34:18 -04:00
imcompiling 0777b71578 More filling out of the record command. The UserSelect seems to work for what I want.
build_develop / build (push) Successful in 39s
2024-05-08 17:13:32 -04:00
imcompiling a2afdc0295 Create record command class. Update configs for recording events. 2024-05-08 16:40:16 -04:00
imcompiling 3b6bef3ff0 fix incorrect description. 2024-05-08 16:39:49 -04:00
imcompiling a963171b16 change get election cycle name, add new function to get the current cycle for quarter name usage. 2024-05-08 16:20:46 -04:00
imcompiling e1787f68d5 Add new table to database 2024-05-08 16:20:19 -04:00
imcompiling a8697b0187 Bandaid fix so HC votes can be viewed.
build_latest / build (push) Successful in 1m39s
build_release / build (push) Successful in 48s
build_develop / build (push) Successful in 52s
2024-01-09 20:04:49 -05:00
imcompiling 8c8dc2c638 Merge branch '44-sort-candidates'
build_latest / build (push) Successful in 47s
build_release / build (push) Successful in 51s
2024-01-03 21:47:59 -05:00
imcompiling 54933b427e Sort nominees in voting views by display name.
build_develop / build (push) Successful in 1m27s
2024-01-03 21:46:54 -05:00
imcompiling 5d05006a23 Prevent unneeded exceptions.
build_latest / build (push) Successful in 1m23s
build_develop / build (push) Successful in 1m7s
2024-01-01 14:11:07 -05:00
imcompiling 2596b9fa7b Merge branch '47-incorrect-election-cycle'
build_latest / build (push) Successful in 44s
build_release / build (push) Successful in 45s
2024-01-01 13:32:41 -05:00
imcompiling 8c654612a9 Ensure that rolback months are properly year-ed.
build_develop / build (push) Successful in 1m27s
2024-01-01 13:32:17 -05:00
imcompiling e836ead399 Force crash on no election cycle. -1 was causing last item in config array to be used.
build_develop / build (push) Successful in 1m38s
2024-01-01 13:23:29 -05:00
imcompiling fa691ced5d Space on the wrong side of the quote.
build_latest / build (push) Successful in 1m33s
build_release / build (push) Successful in 45s
2023-12-27 09:50:01 -05:00
23 changed files with 933 additions and 69 deletions
+3 -2
View File
@@ -130,7 +130,7 @@ dmypy.json
# logging # logging
nextcord.log nextcord.log
# data file for bot # bot files
highscores_data.json highscores_data.json
highscores_config.json highscores_config.json
deleted_backups/ deleted_backups/
@@ -139,4 +139,5 @@ TODO.txt
sql_tester.py sql_tester.py
conf conf
outputs outputs
database database
dakiny-messages/
+4 -7
View File
@@ -1,7 +1,7 @@
FROM python:3.10.6-slim FROM python:3.10.6-slim
# install dependencies for mysqlclient # install dependencies
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential RUN apt-get update && apt-get -y upgrade && apt-get -y install git
RUN pip install --upgrade pip RUN pip install --upgrade pip
@@ -10,7 +10,7 @@ WORKDIR /home
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
COPY config.py config.py COPY config.py config.py
COPY dakiny_bot.py dakiny_bot.py COPY main.py main.py
COPY utils.py utils.py COPY utils.py utils.py
COPY bot_messages bot_messages/ COPY bot_messages bot_messages/
@@ -20,7 +20,4 @@ COPY database_setup database_setup/
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
# remove build deps that aren't needed to run CMD ["python3", "-u", "./main.py"]
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "-u", "./dakiny_bot.py"]
+5 -1
View File
@@ -1,5 +1,9 @@
# Dakiny Bot # Dakiny Bot
Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined. Dakiny clan's own discord bot!
### Assumptions
- For Elections: Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined.
Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already
exist in the database. exist in the database.
+6 -5
View File
@@ -1,7 +1,8 @@
Here's the {quarter} Clan Council: Here's the **{quarter}** Clan Council:
Dakiny Council:
{council}
Dakiny High Council: Dakiny High Council:
{high_council} <@{clan_leader}> - Clan Leader
<@{discord_manager}> - Discord Manager
Dakiny Council:
{council}
+26 -4
View File
@@ -6,7 +6,7 @@ import collections
from datetime import datetime, timedelta from datetime import datetime, timedelta
import nextcord import nextcord
from sqlalchemy import select, true from sqlalchemy import select, true, update
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from classes.nominate_highcouncil_views import NominateHighCouncilView from classes.nominate_highcouncil_views import NominateHighCouncilView
@@ -23,6 +23,7 @@ from database_setup.sql_data_classes import (
from utils import ( from utils import (
build_highcouncil_votes_str, build_highcouncil_votes_str,
build_votes_str, build_votes_str,
edit_clan_council_info,
get_cl_election_name, get_cl_election_name,
get_datetime, get_datetime,
get_dm_election_name, get_dm_election_name,
@@ -239,7 +240,8 @@ class ElectionCycle:
) )
await election_thread.send(message) await election_thread.send(message)
vote_msg = await election_thread.send( vote_msg = await election_thread.send(
build_votes_str(self.config), view=VoteView(self.config, self.guild) build_votes_str(self.config, self.guild),
view=VoteView(self.config, self.guild),
) )
with Session(engine) as session: with Session(engine) as session:
@@ -324,7 +326,7 @@ class ElectionCycle:
) )
await election_thread.send(message) await election_thread.send(message)
vote_msg = await election_thread.send( vote_msg = await election_thread.send(
build_highcouncil_votes_str(self.config), build_highcouncil_votes_str(self.config, self.guild),
view=VoteHighcouncilView(self.config, self.guild), view=VoteHighcouncilView(self.config, self.guild),
) )
@@ -387,15 +389,32 @@ class ElectionCycle:
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election) stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election)
cl_votes = collections.Counter(session.scalars(stmt)) cl_votes = collections.Counter(session.scalars(stmt))
election_cycle = get_election_cycle_name(self.config)
try: try:
dm_winner = max(dm_votes, key=dm_votes.get) dm_winner = max(dm_votes, key=dm_votes.get)
stmt = (
update(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_cycle)
.where(CouncilMembers.MemberId == dm_winner)
.values(DiscordManager=True)
)
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 = max(cl_votes, key=cl_votes.get)
stmt = (
update(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_cycle)
.where(CouncilMembers.MemberId == cl_winner)
.values(ClanLeader=True)
)
session.execute(stmt)
except ValueError: except ValueError:
cl_winner = "No winner" cl_winner = "No winner"
session.commit()
election_thread: nextcord.Thread = self.guild.get_thread( election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId self.get_election_info().ThreadId
) )
@@ -408,7 +427,9 @@ class ElectionCycle:
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id) 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!\n Clan Leader: <@{cl_winner}>\n" highcouncil_msg: str = (
f"Congratulations for our high council members!\n Clan Leader: <@{cl_winner}>\n"
)
highcouncil_msg += f"Discord Manager: <@{dm_winner}>" highcouncil_msg += f"Discord Manager: <@{dm_winner}>"
await election_thread.send(highcouncil_msg) await election_thread.send(highcouncil_msg)
@@ -417,6 +438,7 @@ class ElectionCycle:
) )
await election_thread.edit(archived=True, locked=True) await election_thread.edit(archived=True, locked=True)
await edit_clan_council_info(self.guild, self.config)
def get_election_info(self) -> ElectionInformation: def get_election_info(self) -> ElectionInformation:
""" """
+9 -3
View File
@@ -82,7 +82,7 @@ class VoteSelect(nextcord.ui.StringSelect):
session.execute(stmt) session.execute(stmt)
session.commit() session.commit()
vote_message_str = build_highcouncil_votes_str(self.config) vote_message_str = build_highcouncil_votes_str(self.config, interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message( vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id vote_msg_id
) )
@@ -159,7 +159,10 @@ class VoteHighcouncilView(nextcord.ui.View):
else f"{self.guild.get_member(nominee).display_name}", else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee), value=str(nominee),
) )
for nominee in session.scalars(stmt) for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name,
)
] ]
options.append( options.append(
SelectOption( SelectOption(
@@ -232,7 +235,10 @@ class VoteHighcouncilView(nextcord.ui.View):
else f"{self.guild.get_member(nominee).display_name}", else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee), value=str(nominee),
) )
for nominee in session.scalars(stmt) for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name,
)
] ]
options.append( options.append(
SelectOption( SelectOption(
+5 -2
View File
@@ -64,7 +64,7 @@ class VoteSelect(nextcord.ui.StringSelect):
session.execute(stmt) session.execute(stmt)
session.commit() session.commit()
vote_message_str = build_votes_str(self.config) vote_message_str = build_votes_str(self.config, interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message( vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id vote_msg_id
) )
@@ -135,7 +135,10 @@ class VoteView(nextcord.ui.View):
else f"{self.guild.get_member(nominee).display_name}", else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee), value=str(nominee),
) )
for nominee in session.scalars(stmt) for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name,
)
] ]
options.append( options.append(
SelectOption( SelectOption(
+82
View File
@@ -0,0 +1,82 @@
"""
File contains logic for creating a webhook listener, pulling from git, and updating all messages.
"""
import os
import nextcord
import uvicorn
from fastapi import FastAPI
from git import Repo
from nextcord.ext import commands
from config import DakinyConfig
from utils import REPO_DIR, update_clan_messages
app = FastAPI()
REPO_URL = "https://gitea.cheezers.org/imcompiling/dakiny-messages.git"
class ClanMessages(commands.Cog, name="Clan Messages"):
"""
Cog to be loaded into the bot. Used to be able to have a fastapi post listener and access to the bot at
the same time.
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.config: DakinyConfig = self.bot.config
self.repo_setup()
@commands.Cog.listener()
async def on_ready(self):
"""
Triggers when the bot is in ready state. Adds the webhook listener to FastAPI and serves.
"""
app.add_api_route("/messages/", self.update_messages, methods=["POST"])
config = uvicorn.Config(app=app, host="0.0.0.0", port=5000, log_level="info")
server = uvicorn.Server(config)
await server.serve()
async def update_messages(self):
"""
Pulls from git and then calls method to update all the discord messages as described by the
files in the git repo.
"""
self.origin.pull()
guild: nextcord.Guild = self.bot.get_guild(self.config.params.guild_id)
await update_clan_messages(guild)
return {"message": "Successfully updated all discord messages."}
def repo_setup(self):
"""
Checks if the repo exists and creates repo object, clones if it does not.
"""
# check if repo has been cloned. open repo object
if os.path.isdir(REPO_DIR):
self.repo = Repo(REPO_DIR)
else:
self.repo = Repo.clone_from(REPO_URL, REPO_DIR)
self.origin = self.repo.remote("origin")
if self.config.params.git_branch == "test":
self.repo.create_head("test", self.origin.refs.test).set_tracking_branch(
self.origin.refs.test
).checkout()
else:
self.repo.create_head("main", self.origin.refs.main).set_tracking_branch(
self.origin.refs.main
).checkout()
self.origin.pull()
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(ClanMessages(bot))
+4
View File
@@ -4,6 +4,8 @@ Cog loader. Adds all command cogs to the bot.
from nextcord.ext import commands from nextcord.ext import commands
from cogs.commands.record import RecordCog
from cogs.commands.refresh import RefreshCog
from cogs.commands.remove import RemoveCog from cogs.commands.remove import RemoveCog
from cogs.commands.request import RequestCog from cogs.commands.request import RequestCog
from cogs.commands.set import setCog from cogs.commands.set import setCog
@@ -18,6 +20,8 @@ def setup(bot: commands.Bot):
Adds all command classes to bot. Adds all command classes to bot.
""" """
bot.add_cog(RecordCog(bot))
bot.add_cog(RefreshCog(bot))
bot.add_cog(RemoveCog()) bot.add_cog(RemoveCog())
bot.add_cog(RequestCog(bot)) bot.add_cog(RequestCog(bot))
bot.add_cog(TestCog(bot)) bot.add_cog(TestCog(bot))
+218
View File
@@ -0,0 +1,218 @@
"""
Contained commands and views for record commands.
"""
import nextcord
from discord import TextInputStyle
from nextcord.ext import commands
from config import DakinyConfig
from utils import record_event_host, record_event_participation
class RecordCog(commands.Cog, name="Record Commands"):
"""
Class holds record commands.
"""
def __init__(self, bot) -> None:
"""
Default constructor for RecordCog class. Does nothing.
"""
super().__init__()
self.bot: commands.Bot = bot
@nextcord.slash_command()
async def record(self, interaction: nextcord.Interaction):
"""
Parent slash command for record. Never called.
"""
@record.subcommand(description="Record an event and its participants.")
async def event(
self,
interaction: nextcord.Interaction,
name: str = nextcord.SlashOption(
required=True, description="Name of the event that was hosted."
),
host: nextcord.Member = nextcord.SlashOption(
required=False,
description="An alternative host if you are recording event for someone else.",
),
):
"""
This command creates the base event recording embed and sends a message with it and the EventView
for gathering more information on the event.
Args:
interaction (nextcord.Interaction): interaction with this command.
name (str): Name of the event.
host (nextcord.Member, optional): Alternative host to user using command.
"""
# check role
if (
interaction.guild.get_role(self.bot.config.params.event_host_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.bot.config.params.event_host_role_id}> role to record an event.",
ephemeral=True,
)
return
if host is None:
host = interaction.user
embed: nextcord.Embed = nextcord.Embed(title=name, description="")
embed.add_field(name="Participants", value="")
embed.add_field(name="Non-Discord Participants", value="")
embed.add_field(name="Hosted by", value=f"<@{host.id}>", inline=False)
await interaction.response.send_message(
"Fill out event details and select all members who participated!",
view=EventView(name, self.bot.config, host.id),
ephemeral=True,
embed=embed,
)
class EventView(nextcord.ui.View):
"""
Class handles gathering information on an event. Has buttons to edit the information, cancel, and submit.
Also contains a UserSelect object to pick discord members.
"""
def __init__(self, name: str, config: DakinyConfig, host_id: int):
super().__init__()
self.name: str = name
self.config: DakinyConfig = config
self.host_id: int = host_id
self.event_participants = ParticipantSelect()
self.add_item(self.event_participants)
@nextcord.ui.button(label="Cancel", style=nextcord.ButtonStyle.red, row=1)
async def cancel(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Cancels event submission. Removes embed and view.
"""
await interaction.response.edit_message(
"Canceled", ephemeral=True, embed=None, view=None
)
self.stop()
@nextcord.ui.button(label="Edit", style=nextcord.ButtonStyle.blurple, row=1)
async def edit(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Calls modal to update the text input information of the event.
"""
await interaction.response.send_modal(
ParticipantModal(interaction.message.embeds[0])
)
@nextcord.ui.button(label="Confirm", style=nextcord.ButtonStyle.green, row=1)
async def confirm(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Submits the event embed to the event channel. Removes view and embed from recording message.
Sends point information to database.
"""
discord_users = self.event_participants.values.members
if discord_users:
discord_users_list: list[str] = [
str(user.id) for user in discord_users if user.id is not self.host_id
]
record_event_participation(self.config, discord_users_list, True)
non_discord_users = interaction.message.embeds[0].fields[1].value
if non_discord_users:
non_discord_users_list: list[str] = non_discord_users.split("\n")
record_event_participation(self.config, non_discord_users_list, False)
record_event_host(self.config, self.host_id)
event_channel: nextcord.TextChannel = interaction.guild.get_channel(
self.config.params.event_post_channel_id
)
await event_channel.send(embed=interaction.message.embeds[0])
await interaction.response.edit_message(
content="Event has been submitted.", view=None, embed=None
)
self.stop()
class ParticipantModal(nextcord.ui.Modal):
"""
This modal contains all the text edits for an event. You can change the name, description, and add the usernames
of participants who are not in the discord.
"""
# TODO turn this embed into a dict
def __init__(self, embed):
super().__init__(
"Edit the event.",
timeout=5 * 60,
)
self.embed: nextcord.Embed = embed
self.name: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=True,
label="Event Name",
default_value=self.embed.title,
)
self.description: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=False,
label="Event Description",
default_value=self.embed.description,
style=TextInputStyle.paragraph,
placeholder="Add a description if desired.",
max_length=1000,
)
self.participants: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=False,
label="Non-Discord Usernames",
style=TextInputStyle.paragraph,
placeholder="Please separate usernames with a new line.",
default_value=self.embed.fields[1].value,
)
self.add_item(self.name)
self.add_item(self.description)
self.add_item(self.participants)
async def callback(self, interaction: nextcord.Interaction):
self.embed.title = self.name.value
self.embed.description = self.description.value
self.embed.set_field_at(
index=1, name="Non-Discord Participants", value=self.participants.value
)
await interaction.response.edit_message(embed=self.embed)
class ParticipantSelect(nextcord.ui.UserSelect):
"""
A selection drop down for discord members. Updates the embed with all users selected.
"""
def __init__(self) -> None:
super().__init__(
min_values=1,
max_values=25,
placeholder="Select all participants",
row=0,
)
async def callback(self, interaction: nextcord.Interaction):
embed: nextcord.Embed = interaction.message.embeds[0]
participants: str = "\n".join(
[f"<@{participant.id}>" for participant in self.values.members]
)
embed.set_field_at(0, name="Participants", value=participants)
await interaction.response.edit_message(embed=embed)
+54
View File
@@ -0,0 +1,54 @@
"""
Contained commands and views for refresh commands.
"""
import nextcord
from nextcord.ext import commands
from utils import edit_clan_council_info, update_clan_messages
class RefreshCog(commands.Cog, name="Refresh Commands"):
"""
Class holds refresh commands.
"""
def __init__(self, bot) -> None:
"""
Default constructor for RefreshCog class. Does nothing.
"""
super().__init__()
self.bot: commands.Bot = bot
@nextcord.slash_command(
default_member_permissions=nextcord.Permissions(administrator=True)
)
async def refresh(self, interaction: nextcord.Interaction):
"""
Parent slash command for refresh. Never called.
"""
@refresh.subcommand(description="Force refresh all of the bot managed messages.")
async def messages(self, interaction: nextcord.Interaction):
"""
Calls update_clan_messages() to update all messages.
Args:
interaction (nextcord.Interaction): Interaction with the command.
"""
await interaction.response.defer(ephemeral=True)
await update_clan_messages(interaction.guild)
await interaction.followup.send("Messages have been refreshed.", ephemeral=True)
@refresh.subcommand(description="Updates the current council info message.")
async def council_info(self, interaction: nextcord.Interaction):
"""
Calls edit_clan_council_info().
Args:
interaction (nextcord.Interaction): Interaction with the command.
"""
await edit_clan_council_info(interaction.guild, self.bot.config)
await interaction.response.send_message(
"Clan Council Info message updated.", ephemeral=True
)
+2 -2
View File
@@ -24,7 +24,7 @@ class RequestCog(commands.Cog, name="Request Commands"):
@nextcord.slash_command() @nextcord.slash_command()
async def request(self, interaction: nextcord.Interaction): async def request(self, interaction: nextcord.Interaction):
""" """
Parent slash command for help. Never called. Parent slash command for request. Never called.
""" """
@request.subcommand( @request.subcommand(
@@ -49,7 +49,7 @@ class RequestCog(commands.Cog, name="Request Commands"):
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to add presence" {presence}."' content = f'<@{interaction.user.id}> would like to add presence "{presence}."'
view = PresenceChange(presence) view = PresenceChange(presence)
await self.bot.get_channel(config.params.admin_channel_id).send( await self.bot.get_channel(config.params.admin_channel_id).send(
content=content, view=view content=content, view=view
+25 -9
View File
@@ -10,8 +10,8 @@ from sqlalchemy import delete, select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list
class setCog(commands.Cog, name="Set Commands"): class setCog(commands.Cog, name="Set Commands"):
@@ -30,12 +30,18 @@ class setCog(commands.Cog, name="Set Commands"):
Parent slash command for set. Never called. Parent slash command for set. Never called.
""" """
@set.subcommand(description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself.") @set.subcommand(
description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself."
)
async def birthday( async def birthday(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."), month: int = nextcord.SlashOption(
day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."), required=True, min_value=0, max_value=12, description="Your birth month."
),
day: int = nextcord.SlashOption(
required=True, min_value=0, max_value=31, description="Your birth day."
),
): ):
""" """
Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list. Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list.
@@ -50,7 +56,9 @@ class setCog(commands.Cog, name="Set Commands"):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
if month == 0 and day == 0: if month == 0 and day == 0:
stmt = delete(UserBirthday).where(UserBirthday.UserId == interaction.user.id) stmt = delete(UserBirthday).where(
UserBirthday.UserId == interaction.user.id
)
with Session(engine) as session: with Session(engine) as session:
session.execute(stmt) session.execute(stmt)
session.commit() session.commit()
@@ -62,14 +70,18 @@ class setCog(commands.Cog, name="Set Commands"):
try: try:
birthday = datetime(today.year, month, day) birthday = datetime(today.year, month, day)
except ValueError: except ValueError:
await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True) await interaction.followup.send(
f"Invalid date: {month}/{day}.", ephemeral=True
)
return return
# If the birthday is today, add to birthday_list # If the birthday is today, add to birthday_list
if today.date() == birthday.date(): if today.date() == birthday.date():
birthday_list.append(interaction.user.id) birthday_list.append(interaction.user.id)
new_birthday = UserBirthday(UserId=interaction.user.id, Birthday=f"{month}/{day}") new_birthday = UserBirthday(
UserId=interaction.user.id, Birthday=f"{month}/{day}"
)
with Session(engine) as session: with Session(engine) as session:
try: try:
session.add(new_birthday) session.add(new_birthday)
@@ -77,9 +89,13 @@ class setCog(commands.Cog, name="Set Commands"):
except IntegrityError: except IntegrityError:
# User already has birthday, change it instead. # User already has birthday, change it instead.
session.rollback() session.rollback()
stmt = select(UserBirthday).where(UserBirthday.UserId == interaction.user.id) stmt = select(UserBirthday).where(
UserBirthday.UserId == interaction.user.id
)
new_birthday = session.scalar(stmt) new_birthday = session.scalar(stmt)
new_birthday.Birthday = f"{month}/{day}" new_birthday.Birthday = f"{month}/{day}"
session.commit() session.commit()
await interaction.followup.send(f"Birthday set to {month}/{day}.", ephemeral=True) await interaction.followup.send(
f"Birthday set to {month}/{day}.", ephemeral=True
)
+8 -8
View File
@@ -7,7 +7,7 @@ from nextcord.ext import application_checks, commands
from classes.election_cycle import ElectionCycle from classes.election_cycle import ElectionCycle
from config import DakinyConfig from config import DakinyConfig
from utils import get_current_cycle from utils import get_current_election_cycle
def is_me(): def is_me():
@@ -50,7 +50,7 @@ class TestCog(commands.Cog, name="Test commands."):
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
@@ -67,7 +67,7 @@ class TestCog(commands.Cog, name="Test commands."):
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
@@ -84,7 +84,7 @@ class TestCog(commands.Cog, name="Test commands."):
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
@@ -101,7 +101,7 @@ class TestCog(commands.Cog, name="Test commands."):
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
@@ -112,7 +112,7 @@ class TestCog(commands.Cog, name="Test commands."):
async def election_reminder(self, interaction: nextcord.Interaction): async def election_reminder(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
@@ -123,7 +123,7 @@ class TestCog(commands.Cog, name="Test commands."):
async def daily(self, interaction: nextcord.Interaction): async def daily(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
@@ -134,7 +134,7 @@ class TestCog(commands.Cog, name="Test commands."):
async def term_start(self, interaction: nextcord.Interaction): async def term_start(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(
get_current_cycle(config), get_current_election_cycle(config),
config, config,
self.bot.get_guild(config.params.guild_id), self.bot.get_guild(config.params.guild_id),
) )
+102 -2
View File
@@ -3,12 +3,13 @@ Commands for viewing information in the database.
""" """
import nextcord import nextcord
import pandas as pd
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from database_setup.sql_data_classes import ( from database_setup.sql_data_classes import (
ElectionInformation, ParticipationPoints,
Presences, Presences,
Votes, Votes,
engine, engine,
@@ -31,6 +32,77 @@ class ViewCog(commands.Cog, name="View Commands"):
Parent slash command for set. Never called. Parent slash command for set. Never called.
""" """
@view.subcommand(description="Show event points.")
async def event_points(
self,
interaction: nextcord.Interaction,
quarter_name: str = nextcord.SlashOption(
description="The quarter to view",
required=False,
),
):
# just in case
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
if quarter_name:
df = pd.read_sql_query(
sql=session.query(ParticipationPoints)
.where(ParticipationPoints.Cycle.startswith(quarter_name))
.statement,
con=engine,
)
else:
df = pd.read_sql_query(
sql=session.query(ParticipationPoints).statement,
con=engine,
)
if df.empty:
await interaction.followup.send(
f'Quarter "{quarter_name}" is invalid.', ephemeral=True
)
return
cycles = df.groupby("Cycle")
content = ""
for cycle in cycles.groups:
content += f"**{cycle}**\n"
unsorted_all = cycles.get_group(cycle).groupby("DiscordMember")
members = unsorted_all.get_group(True)
non_members = unsorted_all.get_group(False)
sorted_members = members.sort_values(
by="UserId",
key=lambda user_ids: (
[
interaction.guild.get_member(int(id)).display_name
for id in user_ids
]
),
)
sorted_non_members = non_members.sort_values(by="UserId")
sorted_all = pd.concat(
[sorted_members, sorted_non_members], ignore_index=True, sort=False
)
for row in sorted_all.itertuples():
userid = row.UserId
points = row.Points
discord_member = row.DiscordMember
content += (
f"<@{userid}>: {points}\n"
if discord_member
else f"{userid}: {points}\n"
)
content += "\n"
if len(content) > 2000:
await interaction.followup.send(
"Message length is too long, please select search to limit results."
)
return
await interaction.followup.send(content=content, ephemeral=True)
@view.subcommand(description="Show all possible presence choices.") @view.subcommand(description="Show all possible presence choices.")
async def presences( async def presences(
self, self,
@@ -121,7 +193,7 @@ class ViewCog(commands.Cog, name="View Commands"):
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(ElectionInformation.ElectionCycle) stmt = select(Votes.ElectionCycle).distinct()
elections = session.scalars(stmt) elections = session.scalars(stmt)
# if empty compare string # if empty compare string
@@ -135,3 +207,31 @@ class ViewCog(commands.Cog, name="View Commands"):
if election.lower().startswith(election_name.lower()) if election.lower().startswith(election_name.lower())
] ]
await interaction.response.send_autocomplete(autofilled_elections[:25]) await interaction.response.send_autocomplete(autofilled_elections[:25])
@event_points.on_autocomplete("quarter_name")
async def event_points_autocomplete(
self, interaction: nextcord.Interaction, quarter_name: str
):
"""
Collects all elections that start with the election string.
Args:
interaction (nextcord.Interaction): Interaction with command.
quarter_name (str): String to run autocomplete on.
"""
with Session(engine) as session:
stmt = select(ParticipationPoints.Cycle).distinct()
quarters = session.scalars(stmt)
# if empty compare string
if not quarter_name:
await interaction.response.send_autocomplete(list(quarters)[:25])
return
autofilled_quarters: "list[str]" = [
quarter
for quarter in quarters
if quarter.lower().startswith(quarter_name.lower())
]
await interaction.response.send_autocomplete(autofilled_quarters[:25])
+13 -5
View File
@@ -13,9 +13,9 @@ from sqlalchemy.orm import Session
from classes.election_cycle import ElectionCycle from classes.election_cycle import ElectionCycle
from config import DakinyConfig from config import DakinyConfig
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine from database_setup.sql_data_classes import UserBirthday, engine
from utils import get_current_cycle from main import birthday_list
from utils import get_current_election_cycle
class DailyLoop(commands.Cog, name="Daily Loop"): class DailyLoop(commands.Cog, name="Daily Loop"):
@@ -77,7 +77,11 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = get_current_cycle(config) cycle = get_current_election_cycle(config)
# prevent exception from using cycle as an Int in an array
if cycle is None:
return None
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id)) return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
@@ -107,10 +111,14 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
today = datetime.today() today = datetime.today()
guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id) guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id)
announce_channel: nextcord.TextChannel = guild.get_channel(self.bot.config.params.general_channel_id) announce_channel: nextcord.TextChannel = guild.get_channel(
self.bot.config.params.general_channel_id
)
cakedays: list[nextcord.Member] = [ cakedays: list[nextcord.Member] = [
human for human in guild.humans if human.joined_at.month == today.month and human.joined_at.day == today.day human
for human in guild.humans
if human.joined_at.month == today.month and human.joined_at.day == today.day
] ]
for member in cakedays: for member in cakedays:
+4 -2
View File
@@ -5,7 +5,7 @@ Defines cog for on message listener.
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from dakiny_bot import birthday_list from main import birthday_list
class SubmissionListener(commands.Cog, name="Message Listener"): class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -39,5 +39,7 @@ async def birthday_action(message: nextcord.Message) -> None:
message (nextcord.Message): The message to check for a birthday person. message (nextcord.Message): The message to check for a birthday person.
""" """
if message.author.id in birthday_list or bool(set(message.raw_mentions) & set(birthday_list)): if message.author.id in birthday_list or bool(
set(message.raw_mentions) & set(birthday_list)
):
await message.add_reaction("🎂") await message.add_reaction("🎂")
+6
View File
@@ -1,6 +1,7 @@
""" """
File contains classes defining the Hydra configuraton structure. File contains classes defining the Hydra configuraton structure.
""" """
from dataclasses import dataclass from dataclasses import dataclass
@@ -17,7 +18,12 @@ class Params:
clan_info_channel_id: int clan_info_channel_id: int
clan_member_role_id: int clan_member_role_id: int
discord_token: str discord_token: str
event_host_points: int
event_host_role_id: int
event_participate_points: int
event_post_channel_id: int
general_channel_id: int general_channel_id: int
git_branch: str
guild_id: int guild_id: int
high_council_role_id: int high_council_role_id: int
voting_parent_id: int voting_parent_id: int
+13
View File
@@ -38,4 +38,17 @@ CREATE TABLE UserBirthday (
CREATE TABLE Presences ( CREATE TABLE Presences (
Id integer PRIMARY KEY, Id integer PRIMARY KEY,
Presence varchar(100) not null Presence varchar(100) not null
);
CREATE TABLE ParticipationPoints (
Cycle varchar(30) not null,
UserId varchar(32) not null,
DiscordMember boolean not null,
Points int unsigned DEFAULT 0,
PRIMARY KEY (Cycle, UserId)
);
CREATE TABLE Messages (
Path varchar(100) PRIMARY KEY,
Id bigint unsigned not null
); );
+6 -3
View File
@@ -1,6 +1,7 @@
""" """
Simple file to set up the sqlalchemy connection to database. Simple file to set up the sqlalchemy connection to database.
""" """
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base from sqlalchemy.ext.automap import automap_base
@@ -11,8 +12,10 @@ Base = automap_base()
Base.prepare(autoload_with=engine) Base.prepare(autoload_with=engine)
# define the classes # define the classes
ElectionInformation = Base.classes.ElectionInformation
CouncilMembers = Base.classes.CouncilMembers CouncilMembers = Base.classes.CouncilMembers
Votes = Base.classes.Votes ElectionInformation = Base.classes.ElectionInformation
UserBirthday = Base.classes.UserBirthday Messages = Base.classes.Messages
ParticipationPoints = Base.classes.ParticipationPoints
Presences = Base.classes.Presences Presences = Base.classes.Presences
UserBirthday = Base.classes.UserBirthday
Votes = Base.classes.Votes
View File
+4 -1
View File
@@ -2,4 +2,7 @@ nextcord==2.4.2
python-dotenv==1.0.0 python-dotenv==1.0.0
pytz==2022.7.1 pytz==2022.7.1
hydra-core==1.3.2 hydra-core==1.3.2
sqlalchemy<2.0 sqlalchemy<2.0
pandas==1.5.2
fastapi==0.111.0
GitPython==3.1.43
+334 -13
View File
@@ -2,15 +2,25 @@
File contains util functions. File contains util functions.
""" """
import os
from datetime import datetime from datetime import datetime
from sqlalchemy import select import nextcord
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config import DakinyConfig from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, Votes, engine from database_setup.sql_data_classes import (
CouncilMembers,
Messages,
ParticipationPoints,
Votes,
engine,
)
NOMINEE_SPLITTER = "========================================\n\n" NOMINEE_SPLITTER = "========================================\n\n"
REPO_DIR = "./dakiny-messages"
MESSAGES_DIR = f"{REPO_DIR}/messages"
def get_datetime(day: str) -> datetime: def get_datetime(day: str) -> datetime:
@@ -31,6 +41,9 @@ def get_datetime(day: str) -> datetime:
if datetime.today().month == 12 and month < 12: if datetime.today().month == 12 and month < 12:
year += 1 year += 1
if datetime.today().month == 1 and month > 1:
year -= 1
return datetime(year, month, day) return datetime(year, month, day)
@@ -44,12 +57,25 @@ def get_election_cycle_name(config: DakinyConfig) -> str:
Returns: Returns:
str: the name of the election cycle str: the name of the election cycle
""" """
return f"vote-{get_cycle_name(config)}"
def get_cycle_name(config: DakinyConfig) -> str:
"""
Builds the name of the current cycle/quarter
Args:
config (DakinyConfig): the config used to know the election cycle.
Returns:
str: the name of the cycle.
"""
cycle = get_current_cycle(config) cycle = get_current_cycle(config)
current_cycle = config.election_cycles[cycle] current_cycle = config.election_cycles[cycle]
return ( return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}"
)
def get_dm_election_name(config: DakinyConfig) -> str: def get_dm_election_name(config: DakinyConfig) -> str:
@@ -80,7 +106,7 @@ def get_cl_election_name(config: DakinyConfig) -> str:
return f"{get_election_cycle_name(config)}-cl" return f"{get_election_cycle_name(config)}-cl"
def get_current_cycle(config: DakinyConfig) -> int: def get_current_election_cycle(config: DakinyConfig) -> int:
""" """
Returns index for active election. An election is considered active between the election start and the term start. Returns index for active election. An election is considered active between the election start and the term start.
@@ -88,7 +114,7 @@ def get_current_cycle(config: DakinyConfig) -> int:
config (DakinyConfig): Config object to use config (DakinyConfig): Config object to use
Returns: Returns:
int: cycle index for active election. -1 for no active cycle int: cycle index for active election. None for no active cycle
""" """
today = datetime.today() today = datetime.today()
@@ -100,15 +126,38 @@ def get_current_cycle(config: DakinyConfig) -> int:
): ):
return cycle return cycle
return -1 return None
def build_votes_str(config: DakinyConfig) -> str: def get_current_cycle(config: DakinyConfig) -> int:
"""
Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none
Args:
config (DakinyConfig): Config object to use
Returns:
int: cycle index for current cycle.
"""
today = datetime.today()
for cycle, election in enumerate(config.election_cycles):
if (
today.date() >= get_datetime(election.term_start).date()
and today.date() <= get_datetime(election.term_end).date()
):
return cycle
return None
def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
""" """
Creates a string that contains all nominees and their votes to be sent as a discord message. Creates a string that contains all nominees and their votes to be sent as a discord message.
Args: Args:
config (DakinyConfig): Daking config object config (DakinyConfig): Daking config object
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns: Returns:
str: String that contains all nominees and their votes. str: String that contains all nominees and their votes.
@@ -121,7 +170,10 @@ def build_votes_str(config: DakinyConfig) -> str:
CouncilMembers.ElectionCycle == get_election_cycle_name(config) CouncilMembers.ElectionCycle == get_election_cycle_name(config)
) )
for nominee in session.scalars(stmt): for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
votes[nominee.MemberId] = 0 votes[nominee.MemberId] = 0
stmt = select(Votes).where( stmt = select(Votes).where(
@@ -138,7 +190,7 @@ def build_votes_str(config: DakinyConfig) -> str:
return votes_str return votes_str
def build_highcouncil_votes_str(config: DakinyConfig) -> str: def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
""" """
Creates a string that contains all high council nominees and their votes to be sent as a discord message. Creates a string that contains all high council nominees and their votes to be sent as a discord message.
@@ -147,6 +199,7 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str:
Returns: Returns:
str: String that contains all nominees and their votes. str: String that contains all nominees and their votes.
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
""" """
cl_votes: dict[int, int] = {} cl_votes: dict[int, int] = {}
@@ -159,7 +212,10 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str:
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None)) .filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
) )
for nominee in session.scalars(stmt): for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
cl_votes[nominee.MemberId] = 0 cl_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config)) stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config))
@@ -174,7 +230,10 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str:
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None)) .filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
) )
for nominee in session.scalars(stmt): for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
dm_votes[nominee.MemberId] = 0 dm_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config)) stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config))
@@ -262,3 +321,265 @@ def decorate_dm_nomination(user_id: int, speech: str) -> str:
msg += speech msg += speech
return msg return msg
def record_event_participation(config: DakinyConfig, users: list[str], is_member: bool):
"""
Adds participation points to all users listed. Checks if they don't exist in order to make a new entry, otherwise
adds points to the existing value.
Args:
config (DakinyConfig): Dakiny config for getting points
users (list[int]): users that participated and need points.
is_member (bool): whether the users are a discord member or not
"""
cycle = get_cycle_name(config)
participation_point_value = config.params.event_participate_points
with Session(engine) as session:
for user in users:
stmt = (
select(ParticipationPoints)
.where(ParticipationPoints.UserId == user)
.where(ParticipationPoints.Cycle == cycle)
)
points = session.scalar(stmt)
if points is None:
new_points = ParticipationPoints(
Cycle=cycle,
UserId=user,
DiscordMember=is_member,
Points=participation_point_value,
)
session.add(new_points)
else:
points.Points += participation_point_value
session.commit()
def record_event_host(config: DakinyConfig, user: int):
"""
Adds host points to the given user. Checks if they don't exist in order to make a new entry, otherwise adds points
to the existing value.
Args:
config (DakinyConfig): Dakiny config for getting points
user (int): Id of user that hosted.
"""
cycle = get_cycle_name(config)
host_point_value = config.params.event_host_points
with Session(engine) as session:
stmt = (
select(ParticipationPoints)
.where(ParticipationPoints.UserId == str(user))
.where(ParticipationPoints.Cycle == cycle)
)
points = session.scalar(stmt)
if points is None:
new_points = ParticipationPoints(
Cycle=cycle,
UserId=str(user),
DiscordMember=True,
Points=host_point_value,
)
session.add(new_points)
else:
points.Points += host_point_value
session.commit()
async def update_clan_messages(guild: nextcord.Guild):
"""
Updates all clan messages to contents in MESSAGES_DIR.
Does not to any git work. Is expected to be called after git is pulled.
for ever directory, sort the files by number then edits/sends the message.
Channel fetching/management is handled by another function.
Message editing and sending is handled by another function.
Args:
guild (nextcord.Guild): Guild object that all channels and messages belong to.
"""
dirs = [
dir_name
for dir_name in os.listdir(MESSAGES_DIR)
if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}")
]
with Session(engine) as session:
for dir_name in dirs:
dir_path = f"{MESSAGES_DIR}/{dir_name}"
channel = await get_clan_message_channel(session, dir_name, guild)
sorted_files = sorted(
os.listdir(dir_path),
key=lambda filename: int(filename.split(".")[0]),
)
for file_name in sorted_files:
message_name = f"{dir_name}/{file_name}"
file_path = f"{dir_path}/{file_name}"
with open(file_path, "r", encoding="utf8") as reader:
await edit_clan_message(
session, channel, message_name, reader.read(2000)
)
async def get_clan_message_channel(
session: Session, channel_name: str, guild: nextcord.Guild
) -> nextcord.TextChannel:
"""
Used to obtain a channel from the guild object. Searches database for an id to get the channel. If that is not
found, then searches through all channels in the guild to find one with a matching name. If that also doesn't
turn up a channel, then creates it. Adds an entry into database for last two paths.
Assumes that there is an open session to the database.
Args:
session (Session): Open connection to database.
channel_name (str): Name of channel to obtain.
guild (nextcord.Guild): Guild object to get channel from.
Returns:
nextcord.TextChannel: Channel object with name channel_name.
"""
stmt = select(Messages.Id).where(Messages.Path == channel_name)
channel_id = session.scalar(stmt)
if channel_id is None:
ret_channel = None
# channel is not tracked. see if it exists
channels = await guild.fetch_channels()
for channel in channels:
if (
channel.name == channel_name
and channel.__class__ is nextcord.TextChannel
):
ret_channel = channel
if ret_channel is None:
# channel doesn't exist, we must create it
ret_channel = await guild.create_text_channel(
channel_name,
reason="Creating channel as required for by design of Dakiny-Messages repo.",
)
# record channel information
new_item = Messages(Path=channel_name, Id=ret_channel.id)
session.add(new_item)
session.commit()
return ret_channel
try:
return await guild.fetch_channel(channel_id)
except nextcord.NotFound:
# 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-Messages repo.",
)
stmt = (
update(Messages)
.where(Messages.Path == channel_name)
.values(Id=ret_channel.id)
)
session.execute(stmt)
session.commit()
return ret_channel
async def edit_clan_message(
session: Session,
channel: nextcord.TextChannel,
message_name: str,
message_content: str,
):
"""
Obtains a message id from database. If no id, then need to send a new message and record the id.
If there is a message, then edit the message. If that message is not found in discord, then need to
send a new message and record the id in the database.
Args:
session (Session): Open connection to database.
channel (nextcord.TextChannel): Channel object that message belongs to.
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.
"""
stmt = select(Messages.Id).where(Messages.Path == message_name)
msg_id = session.scalar(stmt)
if msg_id is None:
# don't have a message for this file
msg = await channel.send(message_content)
new_message = Messages(Path=message_name, Id=msg.id)
session.add(new_message)
session.commit()
else:
try:
msg: nextcord.Message = await channel.fetch_message(msg_id)
await msg.edit(message_content)
except nextcord.NotFound:
msg = await channel.send(message_content)
stmt = (
update(Messages).where(Messages.Path == message_name).values(Id=msg.id)
)
session.execute(stmt)
session.commit()
async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig):
"""
Gets the winners of the elections and updates the perminant message that states the current council.
Args:
guild (nextcord.Guild): Guild object that to use to get channel.
config (DakinyConfig): Configs
"""
clan_info_channel: nextcord.TextChannel = await guild.fetch_channel(
config.params.clan_info_channel_id
)
election = get_election_cycle_name(config)
with Session(engine) as session:
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == election)
.where(CouncilMembers.DiscordManager == true())
)
discord_manager = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == election)
.where(CouncilMembers.ClanLeader == true())
)
clan_leader = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == election)
.where(CouncilMembers.Elected == true())
)
council_members_strs = [
f"<@{id}>"
for id in session.scalars(stmt)
if id not in (discord_manager, clan_leader)
]
quarter = get_cycle_name(config)
with open(
"bot_messages/council_members.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
message = message.format(
quarter=quarter,
council="\n".join(council_members_strs),
clan_leader=clan_leader,
discord_manager=discord_manager,
)
await edit_clan_message(
session, clan_info_channel, "council_members.txt", message
)