From 3761d3bb773c965d4fdbec301b5bf3223244a45d Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 16 May 2024 18:01:52 -0400 Subject: [PATCH 1/4] Add BaseModel class for the gitea webhook packet. --- classes/gitea_webhook.py | 195 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 classes/gitea_webhook.py diff --git a/classes/gitea_webhook.py b/classes/gitea_webhook.py new file mode 100644 index 0000000..2e587ab --- /dev/null +++ b/classes/gitea_webhook.py @@ -0,0 +1,195 @@ +# generated by datamodel-codegen: +# filename: gitea_webhook.json +# timestamp: 2024-05-16T21:15:27+00:00 + +from __future__ import annotations + +from typing import List + +from pydantic import BaseModel + + +class Author(BaseModel): + name: str + email: str + username: str + + +class Committer(BaseModel): + name: str + email: str + username: str + + +class Commit(BaseModel): + id: str + message: str + url: str + author: Author + committer: Committer + verification: None + timestamp: str + added: List[str] + removed: List[str] + modified: List[str] + + +class HeadCommit(BaseModel): + id: str + message: str + url: str + author: Author + committer: Committer + verification: None + timestamp: str + added: List[str] + removed: List[str] + modified: List[str] + + +class Owner(BaseModel): + id: int + login: str + login_name: str + full_name: str + email: str + avatar_url: str + language: str + is_admin: bool + last_login: str + created: str + restricted: bool + active: bool + prohibit_login: bool + location: str + website: str + description: str + visibility: str + followers_count: int + following_count: int + starred_repos_count: int + username: str + + +class Permissions(BaseModel): + admin: bool + push: bool + pull: bool + + +class Repository(BaseModel): + id: int + owner: Owner + name: str + full_name: str + description: str + empty: bool + private: bool + fork: bool + template: bool + parent: None + mirror: bool + size: int + language: str + languages_url: str + html_url: str + url: str + link: str + ssh_url: str + clone_url: str + original_url: str + website: str + stars_count: int + forks_count: int + watchers_count: int + open_issues_count: int + open_pr_counter: int + release_counter: int + default_branch: str + archived: bool + created_at: str + updated_at: str + archived_at: str + permissions: Permissions + has_issues: bool + has_wiki: bool + has_pull_requests: bool + has_projects: bool + has_releases: bool + has_packages: bool + has_actions: bool + ignore_whitespace_conflicts: bool + allow_merge_commits: bool + allow_rebase: bool + allow_rebase_explicit: bool + allow_squash_merge: bool + allow_rebase_update: bool + default_delete_branch_after_merge: bool + default_merge_style: str + default_allow_maintainer_edit: bool + avatar_url: str + internal: bool + mirror_interval: str + mirror_updated: str + repo_transfer: None + + +class Pusher(BaseModel): + id: int + login: str + login_name: str + full_name: str + email: str + avatar_url: str + language: str + is_admin: bool + last_login: str + created: str + restricted: bool + active: bool + prohibit_login: bool + location: str + website: str + description: str + visibility: str + followers_count: int + following_count: int + starred_repos_count: int + username: str + + +class Sender(BaseModel): + id: int + login: str + login_name: str + full_name: str + email: str + avatar_url: str + language: str + is_admin: bool + last_login: str + created: str + restricted: bool + active: bool + prohibit_login: bool + location: str + website: str + description: str + visibility: str + followers_count: int + following_count: int + starred_repos_count: int + username: str + + +class GiteaWebhook(BaseModel): + ref: str + before: str + after: str + compare_url: str + commits: List[Commit] + total_commits: int + head_commit: HeadCommit + repository: Repository + pusher: Pusher + sender: Sender From 04c44bc470d578e824fa05b3d05c6ded75991291 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 16 May 2024 21:37:10 -0400 Subject: [PATCH 2/4] only trigger updating clan messages in updated directories. Update clan info message if the template was updated. --- cogs/clan_messages/__init__.py | 29 ++++++++++++++++++++++++++--- utils.py | 5 +++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/cogs/clan_messages/__init__.py b/cogs/clan_messages/__init__.py index 209e27b..3db7212 100644 --- a/cogs/clan_messages/__init__.py +++ b/cogs/clan_messages/__init__.py @@ -10,8 +10,9 @@ from fastapi import FastAPI from git import Repo from nextcord.ext import commands +from classes.gitea_webhook import GiteaWebhook from config import DakinyConfig -from utils import REPO_DIR, update_clan_messages +from utils import REPO_DIR, edit_clan_council_info, update_clan_messages app = FastAPI() REPO_URL = "https://gitea.cheezers.org/imcompiling/dakiny-messages.git" @@ -38,16 +39,38 @@ class ClanMessages(commands.Cog, name="Clan Messages"): server = uvicorn.Server(config) await server.serve() - async def update_messages(self): + async def update_messages(self, hook_info: GiteaWebhook): """ Pulls from git and then calls method to update all the discord messages as described by the files in the git repo. """ + # Get all modified/added files + modified_files: list[str] = [ + file + for list in [commit.modified + commit.added for commit in hook_info.commits] + for file in list + ] + + 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 + } + self.origin.pull() guild: nextcord.Guild = self.bot.get_guild(self.config.params.guild_id) - await update_clan_messages(guild) + await update_clan_messages(guild, channel_updates) + + # if the council members template has changed, update the message + if "council_members.txt" in template_updates: + await edit_clan_council_info( + self.bot.get_guild(self.config.params.guild_id), self.config + ) return {"message": "Successfully updated all discord messages."} diff --git a/utils.py b/utils.py index d10796a..d42a605 100644 --- a/utils.py +++ b/utils.py @@ -4,6 +4,7 @@ File contains util functions. import os from datetime import datetime +from typing import Iterable import nextcord from sqlalchemy import select, true, update @@ -395,7 +396,7 @@ def record_event_host(config: DakinyConfig, user: int): session.commit() -async def update_clan_messages(guild: nextcord.Guild): +async def update_clan_messages(guild: nextcord.Guild, channel_updates: Iterable[str]): """ Updates all clan messages to contents in MESSAGES_DIR. Does not to any git work. Is expected to be called after git is pulled. @@ -411,7 +412,7 @@ async def update_clan_messages(guild: nextcord.Guild): dirs = [ dir_name - for dir_name in os.listdir(MESSAGES_DIR) + for dir_name in channel_updates if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}") ] From 00c8227b8e98a1c5dd28109166bbd9901a451598 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 16 May 2024 21:45:36 -0400 Subject: [PATCH 3/4] Update refresh messages command to be able to choose a channel. --- cogs/commands/refresh.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/cogs/commands/refresh.py b/cogs/commands/refresh.py index cadc1a5..ba92dd0 100644 --- a/cogs/commands/refresh.py +++ b/cogs/commands/refresh.py @@ -2,10 +2,12 @@ Contained commands and views for refresh commands. """ +import os + import nextcord from nextcord.ext import commands -from utils import edit_clan_council_info, update_clan_messages +from utils import MESSAGES_DIR, edit_clan_council_info, update_clan_messages class RefreshCog(commands.Cog, name="Refresh Commands"): @@ -29,15 +31,33 @@ class RefreshCog(commands.Cog, name="Refresh Commands"): """ @refresh.subcommand(description="Force refresh all of the bot managed messages.") - async def messages(self, interaction: nextcord.Interaction): + async def messages( + self, + interaction: nextcord.Interaction, + channel: nextcord.TextChannel = nextcord.SlashOption( + description="Channel to refresh messages in.", + required=False, + ), + ): """ Calls update_clan_messages() to update all messages. Args: interaction (nextcord.Interaction): Interaction with the command. + channel (nextcord.TextChannel): channel to refresh messages in. """ await interaction.response.defer(ephemeral=True) - await update_clan_messages(interaction.guild) + + if channel is None: + channel_updates = [ + dir_name + for dir_name in os.listdir(MESSAGES_DIR) + if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}") + ] + else: + channel_updates = [channel.name] + + await update_clan_messages(interaction.guild, channel_updates) await interaction.followup.send("Messages have been refreshed.", ephemeral=True) @refresh.subcommand(description="Updates the current council info message.") From 0b27c60bea7aa1c9db536464e7bdbe63b2718039 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 16 May 2024 22:41:27 -0400 Subject: [PATCH 4/4] Clarify some method names. current election cycle shoud return quarter cycle if it can't find a cycle. --- classes/election_cycle.py | 23 ++++++++----------- classes/nominate_highcouncil_views.py | 10 ++++---- classes/nominate_views.py | 8 +++---- classes/voting_highcouncil_views.py | 24 +++++++++++-------- classes/voting_views.py | 14 +++++++----- utils.py | 33 +++++++++++++-------------- 6 files changed, 56 insertions(+), 56 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 9080e7d..fe845d5 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -28,7 +28,7 @@ from utils import ( get_cl_election_name, get_datetime, get_dm_election_name, - get_election_cycle_name, + get_election_name, ) @@ -199,7 +199,7 @@ class ElectionCycle: parent: nextcord.TextChannel = self.guild.get_channel( self.config.params.voting_parent_id ) - quarter_name: str = get_election_cycle_name(self.config) + quarter_name: str = get_election_name(self.config) thread_msg: nextcord.Message = await parent.send(content=quarter_name) @@ -247,8 +247,7 @@ class ElectionCycle: with Session(engine) as session: stmt = select(ElectionInformation).where( - ElectionInformation.ElectionCycle - == get_election_cycle_name(self.config) + ElectionInformation.ElectionCycle == get_election_name(self.config) ) info = session.scalar(stmt) info.CouncilVoteMsgId = vote_msg.id @@ -277,7 +276,7 @@ class ElectionCycle: message, view=NominateHighCouncilView(self.config, self.guild) ) - election_name = get_election_cycle_name(self.config) + election_name = get_election_name(self.config) with Session(engine) as session: # remove vote button from vote message @@ -333,8 +332,7 @@ class ElectionCycle: with Session(engine) as session: stmt = select(ElectionInformation).where( - ElectionInformation.ElectionCycle - == get_election_cycle_name(self.config) + ElectionInformation.ElectionCycle == get_election_name(self.config) ) info = session.scalar(stmt) info.HighCouncilVoteMsgId = vote_msg.id @@ -364,9 +362,7 @@ class ElectionCycle: # role management stmt = ( select(CouncilMembers.MemberId) - .where( - CouncilMembers.ElectionCycle == get_election_cycle_name(self.config) - ) + .where(CouncilMembers.ElectionCycle == get_election_name(self.config)) .where(CouncilMembers.Elected == true()) ) @@ -390,7 +386,7 @@ 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) + election_cycle = get_election_name(self.config) try: dm_winner = max(dm_votes, key=dm_votes.get) stmt = ( @@ -422,7 +418,7 @@ class ElectionCycle: # clear view from vote message stmt = select(ElectionInformation.HighCouncilVoteMsgId).where( - ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) + ElectionInformation.ElectionCycle == get_election_name(self.config) ) vote_msg_id = session.scalar(stmt) vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id) @@ -451,8 +447,7 @@ class ElectionCycle: with Session(engine) as session: stmt = select(ElectionInformation).where( - ElectionInformation.ElectionCycle - == get_election_cycle_name(self.config) + ElectionInformation.ElectionCycle == get_election_name(self.config) ) return session.scalar(stmt) diff --git a/classes/nominate_highcouncil_views.py b/classes/nominate_highcouncil_views.py index 98e841c..9a32fa8 100644 --- a/classes/nominate_highcouncil_views.py +++ b/classes/nominate_highcouncil_views.py @@ -13,7 +13,7 @@ from utils import ( NOMINEE_SPLITTER, decorate_cl_nomination, decorate_dm_nomination, - get_election_cycle_name, + get_election_name, ) @@ -60,7 +60,7 @@ class NominateDMModal(nextcord.ui.Modal): with Session(engine) as session: # user already in table - cycle = get_election_cycle_name(self.config) + cycle = get_election_name(self.config) stmt = ( select(CouncilMembers) .where(CouncilMembers.ElectionCycle == cycle) @@ -127,7 +127,7 @@ class NominateCLModal(nextcord.ui.Modal): with Session(engine) as session: # user already in table - cycle = get_election_cycle_name(self.config) + cycle = get_election_name(self.config) stmt = ( select(CouncilMembers) .where(CouncilMembers.ElectionCycle == cycle) @@ -189,7 +189,7 @@ class NominateHighCouncilView(nextcord.ui.View): ) return - cycle = get_election_cycle_name(self.config) + cycle = get_election_name(self.config) usr_nominated = False speech_content = "" with Session(engine) as session: @@ -242,7 +242,7 @@ class NominateHighCouncilView(nextcord.ui.View): ) return - cycle = get_election_cycle_name(self.config) + cycle = get_election_name(self.config) usr_nominated = False speech_content = "" with Session(engine) as session: diff --git a/classes/nominate_views.py b/classes/nominate_views.py index 177ae76..cc26bb6 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import Session from config import DakinyConfig from database_setup.sql_data_classes import CouncilMembers, engine -from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name +from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name class NominateModal(nextcord.ui.Modal): @@ -58,7 +58,7 @@ class NominateModal(nextcord.ui.Modal): msg = await interaction.channel.send(speech) council_member = CouncilMembers( - ElectionCycle=get_election_cycle_name(self.config), + ElectionCycle=get_election_name(self.config), MemberId=interaction.user.id, SpeechMsgId=msg.id, ) @@ -66,7 +66,7 @@ class NominateModal(nextcord.ui.Modal): session.commit() else: # user already nominated, just need to update the election speech - cycle = get_election_cycle_name(self.config) + cycle = get_election_name(self.config) stmt = ( select(CouncilMembers) .where(CouncilMembers.ElectionCycle == cycle) @@ -120,7 +120,7 @@ class NominateView(nextcord.ui.View): ) return - cycle = get_election_cycle_name(self.config) + cycle = get_election_name(self.config) usr_nominated = False speech_content = "" with Session(engine) as session: diff --git a/classes/voting_highcouncil_views.py b/classes/voting_highcouncil_views.py index 385ee7a..3899b57 100644 --- a/classes/voting_highcouncil_views.py +++ b/classes/voting_highcouncil_views.py @@ -21,7 +21,7 @@ from utils import ( build_highcouncil_votes_str, get_cl_election_name, get_dm_election_name, - get_election_cycle_name, + get_election_name, ) @@ -49,7 +49,7 @@ class VoteSelect(nextcord.ui.StringSelect): # should never happen raise ValueError('election type was not "cl" or "dm"') - cycle: str = get_election_cycle_name(self.config) + cycle: str = get_election_name(self.config) with Session(engine) as session: stmt = select(ElectionInformation.HighCouncilVoteMsgId).where( ElectionInformation.ElectionCycle == cycle @@ -135,7 +135,7 @@ class VoteHighcouncilView(nextcord.ui.View): ) return - cycle: str = get_election_cycle_name(self.config) + cycle: str = get_election_name(self.config) election: str = get_cl_election_name(self.config) with Session(engine) as session: @@ -154,9 +154,11 @@ class VoteHighcouncilView(nextcord.ui.View): options = [ SelectOption( - label=f"{self.guild.get_member(nominee).display_name} ☑️" - if voted_for == nominee - else f"{self.guild.get_member(nominee).display_name}", + label=( + f"{self.guild.get_member(nominee).display_name} ☑️" + if voted_for == nominee + else f"{self.guild.get_member(nominee).display_name}" + ), value=str(nominee), ) for nominee in sorted( @@ -211,7 +213,7 @@ class VoteHighcouncilView(nextcord.ui.View): ) return - cycle: str = get_election_cycle_name(self.config) + cycle: str = get_election_name(self.config) election: str = get_dm_election_name(self.config) with Session(engine) as session: @@ -230,9 +232,11 @@ class VoteHighcouncilView(nextcord.ui.View): options = [ SelectOption( - label=f"{self.guild.get_member(nominee).display_name} ☑️" - if voted_for == nominee - else f"{self.guild.get_member(nominee).display_name}", + label=( + f"{self.guild.get_member(nominee).display_name} ☑️" + if voted_for == nominee + else f"{self.guild.get_member(nominee).display_name}" + ), value=str(nominee), ) for nominee in sorted( diff --git a/classes/voting_views.py b/classes/voting_views.py index ef75720..6d1694d 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -17,7 +17,7 @@ from database_setup.sql_data_classes import ( Votes, engine, ) -from utils import build_votes_str, get_election_cycle_name +from utils import build_votes_str, get_election_name class VoteSelect(nextcord.ui.StringSelect): @@ -31,7 +31,7 @@ class VoteSelect(nextcord.ui.StringSelect): self.config = config async def callback(self, interaction: Interaction): - election_name = get_election_cycle_name(self.config) + election_name = get_election_name(self.config) with Session(engine) as session: stmt = select(ElectionInformation.CouncilVoteMsgId).where( ElectionInformation.ElectionCycle == election_name @@ -114,7 +114,7 @@ class VoteView(nextcord.ui.View): ) return - election: str = get_election_cycle_name(self.config) + election: str = get_election_name(self.config) with Session(engine) as session: stmt = ( @@ -130,9 +130,11 @@ class VoteView(nextcord.ui.View): options = [ SelectOption( - label=f"{self.guild.get_member(nominee).display_name} ☑️" - if voted_for == nominee - else f"{self.guild.get_member(nominee).display_name}", + label=( + f"{self.guild.get_member(nominee).display_name} ☑️" + if voted_for == nominee + else f"{self.guild.get_member(nominee).display_name}" + ), value=str(nominee), ) for nominee in sorted( diff --git a/utils.py b/utils.py index d42a605..75539cc 100644 --- a/utils.py +++ b/utils.py @@ -49,7 +49,7 @@ def get_datetime(day: str) -> datetime: return datetime(year, month, day) -def get_election_cycle_name(config: DakinyConfig) -> str: +def get_election_name(config: DakinyConfig) -> str: """ Builds the unique name of the current election cycle @@ -65,7 +65,7 @@ def get_election_cycle_name(config: DakinyConfig) -> str: return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}" -def get_cycle_name(config: DakinyConfig) -> str: +def get_quarter_name(config: DakinyConfig) -> str: """ Builds the name of the current cycle/quarter @@ -76,7 +76,7 @@ def get_cycle_name(config: DakinyConfig) -> str: str: the name of the cycle. """ - cycle = get_current_cycle(config) + cycle = get_quarter_cycle(config) current_cycle = config.election_cycles[cycle] return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}" @@ -93,7 +93,7 @@ def get_dm_election_name(config: DakinyConfig) -> str: str: the name of the election """ - return f"{get_election_cycle_name(config)}-dm" + return f"{get_election_name(config)}-dm" def get_cl_election_name(config: DakinyConfig) -> str: @@ -107,7 +107,7 @@ def get_cl_election_name(config: DakinyConfig) -> str: str: the name of the election """ - return f"{get_election_cycle_name(config)}-cl" + return f"{get_election_name(config)}-cl" def get_current_election_cycle(config: DakinyConfig) -> int: @@ -130,10 +130,11 @@ def get_current_election_cycle(config: DakinyConfig) -> int: ): return cycle - return None + # It isn't a current election, get the quarter + return get_quarter_cycle(config) -def get_current_cycle(config: DakinyConfig) -> int: +def get_quarter_cycle(config: DakinyConfig) -> int: """ Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none @@ -171,7 +172,7 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str: with Session(engine) as session: stmt = select(CouncilMembers).where( - CouncilMembers.ElectionCycle == get_election_cycle_name(config) + CouncilMembers.ElectionCycle == get_election_name(config) ) for nominee in sorted( @@ -180,9 +181,7 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str: ): votes[nominee.MemberId] = 0 - stmt = select(Votes).where( - Votes.ElectionCycle == get_election_cycle_name(config) - ) + stmt = select(Votes).where(Votes.ElectionCycle == get_election_name(config)) for vote in session.scalars(stmt): if vote.VotedForId is not None: @@ -212,7 +211,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> with Session(engine) as session: stmt = ( select(CouncilMembers) - .where(CouncilMembers.ElectionCycle == get_election_cycle_name(config)) + .where(CouncilMembers.ElectionCycle == get_election_name(config)) .filter(CouncilMembers.ClanLeaderSpeechId.is_not(None)) ) @@ -230,7 +229,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> stmt = ( select(CouncilMembers) - .where(CouncilMembers.ElectionCycle == get_election_cycle_name(config)) + .where(CouncilMembers.ElectionCycle == get_election_name(config)) .filter(CouncilMembers.DiscordManagerSpeechId.is_not(None)) ) @@ -338,7 +337,7 @@ def record_event_participation(config: DakinyConfig, users: list[str], is_member is_member (bool): whether the users are a discord member or not """ - cycle = get_cycle_name(config) + cycle = get_quarter_name(config) participation_point_value = config.params.event_participate_points with Session(engine) as session: for user in users: @@ -372,7 +371,7 @@ def record_event_host(config: DakinyConfig, user: int): config (DakinyConfig): Dakiny config for getting points user (int): Id of user that hosted. """ - cycle = get_cycle_name(config) + cycle = get_quarter_name(config) host_point_value = config.params.event_host_points with Session(engine) as session: stmt = ( @@ -545,7 +544,7 @@ async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig): config.params.clan_info_channel_id ) - election = get_election_cycle_name(config) + election = get_election_name(config) with Session(engine) as session: stmt = ( select(CouncilMembers.MemberId) @@ -571,7 +570,7 @@ async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig): if id not in (discord_manager, clan_leader) ] - quarter = get_cycle_name(config) + quarter = get_quarter_name(config) with open( f"{TEMPLATES_DIR}/council_members.txt", mode="r", encoding="utf8"