Compare commits

...
Author SHA1 Message Date
imcompiling 3890c30d1f Found niche bug when testing. Need to get election cycle here because election cycle might be different than current cycle.
build_develop / build (push) Failing after 19s
2024-05-16 16:41:36 -04:00
imcompiling 1e1407c918 Update templates to be used from dakiny messages repo. 2024-05-16 16:25:50 -04:00
imcompiling be408307aa Remove template messages from repo. 2024-05-16 16:22:18 -04:00
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
21 changed files with 422 additions and 123 deletions
+3 -2
View File
@@ -130,7 +130,7 @@ dmypy.json
# logging
nextcord.log
# data file for bot
# bot files
highscores_data.json
highscores_config.json
deleted_backups/
@@ -139,4 +139,5 @@ TODO.txt
sql_tester.py
conf
outputs
database
database
dakiny-messages/
+4 -7
View File
@@ -1,7 +1,7 @@
FROM python:3.10.6-slim
# install dependencies for mysqlclient
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
# install dependencies
RUN apt-get update && apt-get -y upgrade && apt-get -y install git
RUN pip install --upgrade pip
@@ -10,7 +10,7 @@ WORKDIR /home
COPY requirements.txt requirements.txt
COPY config.py config.py
COPY dakiny_bot.py dakiny_bot.py
COPY main.py main.py
COPY utils.py utils.py
COPY bot_messages bot_messages/
@@ -20,7 +20,4 @@ COPY database_setup database_setup/
RUN pip install -r requirements.txt
# remove build deps that aren't needed to run
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "-u", "./dakiny_bot.py"]
CMD ["python3", "-u", "./main.py"]
+5 -1
View File
@@ -1,5 +1,9 @@
# 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
exist in the database.
-7
View File
@@ -1,7 +0,0 @@
Here's the {quarter} Clan Council:
Dakiny Council:
{council}
Dakiny High Council:
{high_council}
-54
View File
@@ -1,54 +0,0 @@
```fix
Dakiny's Clan Council election
```
Dakiny is an organization built by and for its members. We are a community of people who come together to support each other in the creation of this magical and unique experience. To sustain what we've created here, and to grow and involve into the future, we vote in our leadership every 3 months to provide an opportunity for new clan members to represent their faction of Dakiny in the Clan Council.
This clan is about human interaction, and the Clan Council positions serve to prop up certain voices within our community, in an effort to shape it as a force for good. Our mission here is to spread love. The more loving Council Members (CMs) we have on the team, the greater the community will grow for all of us to enjoy. There are no term limits, but if you go inactive on RS or become uninterested in pouring your effort into this community, request to step down so that others may contribute where you are no longer able to.
We want all CMs to grow and succeed. Please, reach out for assistance and advice if you find yourself going even a few days without taking real, actionable steps towards the prosperity of Dakiny. Recruiting, hosting events, participating in the Clan Council Discord, and demonstrating the Core Elements of the position are required from each and every CM to grow a happy and healthy clan.
__So here's how it works:__
---message_break---
Each quarter, a channel is created here on Discord for clan members to cast their vote to determine the Clan Council for that 3-month period.
**Only clan members can vote and all clan members are granted exactly one equally weighted vote.** If you vote twice for different people, your vote will be annulled.
All clan members are permitted to run for a seat on the Clan Council as long as they:
**i)** have been in the clan for at least 13 weeks (enough time to have participated in at least one election);
**ii)** have no warnings under their name in the last 6 months and
**iii)** have been active in the clan for the past 13 weeks prior to the first day of elections. To be considered active in that timeframe, one must fulfill the following criteria: hosted/participated in at least three clan events, each one in a different week;
**iv)** are not running under aliases that hide their true identity.
Candidates are permitted to vote for themselves. Once a candidate reaches 3 votes from clan members, they have earned their seat, meaning no more votes can be cast for them.
Then, the new Clan Council Elect will come together to select the 2 High Council positions:
RuneScape 3 Clan Leader
Discord Server Manager
The Clan Council will be denoted by the Discord ranks of <@&{clan_council_id}> and <@&{high_council_id}> and in-game with the following ranks.
<:Rank_Admin:479008185240518656> Admin
<:Rank_Organiser:479008215024009216> Organizer
<:Rank_Coordinator:479008238654849045> Coordinator
<:Rank_Overseer:479008269382451201> Overseer
<:Rank_Deputy:479008295319896064> Deputy Owner
<:Rank_Owner:479008325137072155> Clan Leader
---message_break---
__Here's the timeline:__
Election begins: {election_start}
Term: {term_start} - {term_end}
**{election_start}**: Post here to announce candidacy
**{voting_start}**: Voting begins
**{voting_end}**: Votes are tallied and winners are announced
**{highcouncil_election}**: The new Clan Council votes who among them shall receive the two High Council positions of: Clan Leader and Discord Server Manager.
**{term_start}**: The new Clan Council is officially inaugurated and a drop party (or a giveaway event) begins to commemorate the new leadership.
This process will begin again next quarter on {next_election_start} with the inauguration scheduled for {term_end}.
---message_break---
**You may now announce your candidacy for a seat on the Dakiny Clan Council!
**
You may include a speech with your announcement, as well as specific pertinent facts such as total recruits, time in the clan, past Clan Council experience, and donations made. This data is available upon request via <@&{clan_council_id}>.
-12
View File
@@ -1,12 +0,0 @@
`Leadership Elections - candidacy time`
<@&{clan_council_elect_role_id}>, welcome. We are starting the second part of our elections. On {highcouncil_election_announce} this channel will be open to write for all of you. Everyone else will be able to read this channel, but not write in it. Only members of the Clan Council Elected (CCE) can vote now. Any votes from others will be annulled.
From {highcouncil_election_announce} to {highcouncil_election_start}, you will be able to announce your candidacy for High Council. The positions are:
Clan Leader
Discord Server Manager
Each member of the CCE can vote once in each category (Discord Server Manager and Clan Leader). Alternatively, you may choose to intentionally abstain.
Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
-1
View File
@@ -1 +0,0 @@
<@&{clan_council_elect_role_id}> it is now time to vote for the high council! You have until {term_start} to complete the vote.
-14
View File
@@ -1,14 +0,0 @@
====================
<@&{clan_member_id}>, it's time to vote for clan council.
Press the Vote button to vote!
Voting ends next {voting_end_day}, {voting_end}.
Results will then be announced.
After a candidate receives 3 votes, their spot is secured in the new council, but you can still vote for them to show your support.
**Just a reminder:** only the people listed below are eligible to vote, and each clan member may **only vote for one person**. Votes from guests, diplomats, or allies will be annulled.
Votes for multiple candidates from the same person will not be counted.
RS3 Clan Members
OSRS Clan Members
+28 -6
View File
@@ -6,7 +6,7 @@ import collections
from datetime import datetime, timedelta
import nextcord
from sqlalchemy import select, true
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session
from classes.nominate_highcouncil_views import NominateHighCouncilView
@@ -21,8 +21,10 @@ from database_setup.sql_data_classes import (
engine,
)
from utils import (
TEMPLATES_DIR,
build_highcouncil_votes_str,
build_votes_str,
edit_clan_council_info,
get_cl_election_name,
get_datetime,
get_dm_election_name,
@@ -76,7 +78,7 @@ class ElectionCycle:
"""
with open(
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
f"{TEMPLATES_DIR}/election_announcement.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
@@ -109,7 +111,7 @@ class ElectionCycle:
"""
with open(
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
f"{TEMPLATES_DIR}/highcouncil_election.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
@@ -136,7 +138,7 @@ class ElectionCycle:
"""
with open(
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
f"{TEMPLATES_DIR}/highcouncil_vote.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
@@ -153,7 +155,7 @@ class ElectionCycle:
str: Message to send
"""
with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file:
with open(f"{TEMPLATES_DIR}/vote_start.txt", mode="r", encoding="utf8") as file:
message = file.read()
vote_end: datetime = get_datetime(
self.config.election_cycles[self.cycle].voting_end
@@ -388,15 +390,32 @@ class ElectionCycle:
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election)
cl_votes = collections.Counter(session.scalars(stmt))
election_cycle = get_election_cycle_name(self.config)
try:
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:
dm_winner = "No winner"
try:
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:
cl_winner = "No winner"
session.commit()
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
@@ -409,7 +428,9 @@ class ElectionCycle:
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
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}>"
await election_thread.send(highcouncil_msg)
@@ -418,6 +439,7 @@ class ElectionCycle:
)
await election_thread.edit(archived=True, locked=True)
await edit_clan_council_info(self.guild, self.config)
def get_election_info(self) -> ElectionInformation:
"""
+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))
+2
View File
@@ -5,6 +5,7 @@ Cog loader. Adds all command cogs to the bot.
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.request import RequestCog
from cogs.commands.set import setCog
@@ -20,6 +21,7 @@ def setup(bot: commands.Bot):
"""
bot.add_cog(RecordCog(bot))
bot.add_cog(RefreshCog(bot))
bot.add_cog(RemoveCog())
bot.add_cog(RequestCog(bot))
bot.add_cog(TestCog(bot))
+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
)
+25 -9
View File
@@ -10,8 +10,8 @@ from sqlalchemy import delete, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list
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.
"""
@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(
self,
interaction: nextcord.Interaction,
month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."),
day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."),
month: int = nextcord.SlashOption(
required=True, min_value=0, max_value=12, description="Your birth month."
),
day: int = nextcord.SlashOption(
required=True, min_value=0, max_value=31, description="Your birth day."
),
):
"""
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)
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:
session.execute(stmt)
session.commit()
@@ -62,14 +70,18 @@ class setCog(commands.Cog, name="Set Commands"):
try:
birthday = datetime(today.year, month, day)
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
# If the birthday is today, add to birthday_list
if today.date() == birthday.date():
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:
try:
session.add(new_birthday)
@@ -77,9 +89,13 @@ class setCog(commands.Cog, name="Set Commands"):
except IntegrityError:
# User already has birthday, change it instead.
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.Birthday = f"{month}/{day}"
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
)
+1 -1
View File
@@ -13,8 +13,8 @@ from sqlalchemy.orm import Session
from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list
from utils import get_current_election_cycle
+4 -2
View File
@@ -5,7 +5,7 @@ Defines cog for on message listener.
import nextcord
from nextcord.ext import commands
from dakiny_bot import birthday_list
from main import birthday_list
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.
"""
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("🎂")
+1
View File
@@ -23,6 +23,7 @@ class Params:
event_participate_points: int
event_post_channel_id: int
general_channel_id: int
git_branch: str
guild_id: int
high_council_role_id: int
voting_parent_id: int
+5
View File
@@ -46,4 +46,9 @@ CREATE TABLE ParticipationPoints (
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
);
+5 -4
View File
@@ -12,9 +12,10 @@ Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
ElectionInformation = Base.classes.ElectionInformation
CouncilMembers = Base.classes.CouncilMembers
Votes = Base.classes.Votes
UserBirthday = Base.classes.UserBirthday
Presences = Base.classes.Presences
ElectionInformation = Base.classes.ElectionInformation
Messages = Base.classes.Messages
ParticipationPoints = Base.classes.ParticipationPoints
Presences = Base.classes.Presences
UserBirthday = Base.classes.UserBirthday
Votes = Base.classes.Votes
View File
+3 -1
View File
@@ -3,4 +3,6 @@ python-dotenv==1.0.0
pytz==2022.7.1
hydra-core==1.3.2
sqlalchemy<2.0
pandas==1.5.2
pandas==1.5.2
fastapi==0.111.0
GitPython==3.1.43
+200 -2
View File
@@ -2,21 +2,26 @@
File contains util functions.
"""
import os
from datetime import datetime
import nextcord
from sqlalchemy import select
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import (
CouncilMembers,
Messages,
ParticipationPoints,
Votes,
engine,
)
NOMINEE_SPLITTER = "========================================\n\n"
REPO_DIR = "./dakiny-messages"
MESSAGES_DIR = f"{REPO_DIR}/messages"
TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
def get_datetime(day: str) -> datetime:
@@ -53,9 +58,9 @@ def get_election_cycle_name(config: DakinyConfig) -> str:
Returns:
str: the name of the election cycle
"""
cycle = get_current_election_cycle(config)
current_cycle = config.election_cycles[cycle]
return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
@@ -388,3 +393,196 @@ def record_event_host(config: DakinyConfig, user: int):
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(
f"{TEMPLATES_DIR}/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
)