Compare commits

..
3 Commits
Author SHA1 Message Date
imcompiling 1c8e8c4d5a Merge branch 'main' into mock-election
build_develop / build (push) Successful in 1m29s
2023-11-07 14:44:40 -05:00
imcompiling 0b5423a62b Merge branch 'main' into mock-election
build_develop / build (push) Successful in 1m22s
2023-11-06 19:47:39 -05:00
imcompiling dbbb7c1c62 Change votes to 2 for mock test
build_develop / build (push) Successful in 1m26s
2023-11-04 09:20:19 -04:00
15 changed files with 159 additions and 1238 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ From {highcouncil_election_announce} to {highcouncil_election_start}, you will b
Clan Leader Clan Leader
Discord Server Manager 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. Each member of the CCE can cast one for each category by using the emotes: <:Rank_Owner:479008325137072155> for Clan Leader and <:Rank_Deputy:479008295319896064> for Discord Server Manager.
If needed, the icon 🤷 can be used to abstain to vote in any or both categories and it should be applied to all candidates of that category.
Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}. Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
+57 -216
View File
@@ -6,28 +6,14 @@ 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
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from classes.nominate_highcouncil_views import NominateHighCouncilView
from classes.nominate_views import NominateView from classes.nominate_views import NominateView
from classes.voting_highcouncil_views import VoteHighcouncilView
from classes.voting_views import VoteView from classes.voting_views import VoteView
from config import DakinyConfig from config import DakinyConfig
from database_setup.sql_data_classes import ( from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
CouncilMembers, from utils import build_votes_str, get_datetime, get_election_cycle_name
ElectionInformation,
Votes,
engine,
)
from utils import (
build_highcouncil_votes_str,
build_votes_str,
get_cl_election_name,
get_datetime,
get_dm_election_name,
get_election_cycle_name,
)
class ElectionCycle: class ElectionCycle:
@@ -54,15 +40,9 @@ class ElectionCycle:
self.term_end: datetime = get_datetime(self.election_cycle.term_end) self.term_end: datetime = get_datetime(self.election_cycle.term_end)
self.voting_start: datetime = get_datetime(self.election_cycle.voting_start) self.voting_start: datetime = get_datetime(self.election_cycle.voting_start)
self.voting_end: datetime = get_datetime(self.election_cycle.voting_end) self.voting_end: datetime = get_datetime(self.election_cycle.voting_end)
self.highcouncil_election_announce: datetime = get_datetime( self.highcouncil_election_announce: datetime = get_datetime(self.election_cycle.highcouncil_election_announce)
self.election_cycle.highcouncil_election_announce self.highcouncil_election_start: datetime = get_datetime(self.election_cycle.highcouncil_election_start)
) self.highcouncil_election_end: datetime = get_datetime(self.election_cycle.highcouncil_election_end)
self.highcouncil_election_start: datetime = get_datetime(
self.election_cycle.highcouncil_election_start
)
self.highcouncil_election_end: datetime = get_datetime(
self.election_cycle.highcouncil_election_end
)
self.guild: nextcord.Guild = guild self.guild: nextcord.Guild = guild
self.election_over: bool = False self.election_over: bool = False
@@ -75,15 +55,11 @@ class ElectionCycle:
long. long.
""" """
with open( with open("bot_messages/election_announcement.txt", mode="r", encoding="utf8") as file:
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
) as file:
message = file.read() message = file.read()
next_election = get_datetime( next_election = get_datetime(
self.config.election_cycles[ self.config.election_cycles[(self.cycle + 1) % len(self.config.election_cycles)].election_start
(self.cycle + 1) % len(self.config.election_cycles)
].election_start
) )
message = message.format( message = message.format(
@@ -108,22 +84,14 @@ class ElectionCycle:
str: Message to send. str: Message to send.
""" """
with open( with open("bot_messages/highcouncil_election.txt", mode="r", encoding="utf8") as file:
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
) as file:
message = file.read() message = file.read()
return message.format( return message.format(
clan_council_elect_role_id=self.config.params.clan_council_elect_role_id, clan_council_elect_role_id=self.config.params.clan_council_elect_role_id,
highcouncil_election_announce=self.highcouncil_election_announce.strftime( highcouncil_election_announce=self.highcouncil_election_announce.strftime("%B %d"),
"%B %d" highcouncil_election_start=self.highcouncil_election_start.strftime("%B %d"),
), highcouncil_election_end=self.highcouncil_election_end.strftime("%B %d"),
highcouncil_election_start=self.highcouncil_election_start.strftime(
"%B %d"
),
highcouncil_election_end=self.highcouncil_election_end.strftime(
"%B %d"
),
term_start=self.term_start.strftime("%B %d"), term_start=self.term_start.strftime("%B %d"),
) )
@@ -135,9 +103,7 @@ class ElectionCycle:
str: Message to send. str: Message to send.
""" """
with open( with open("bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8") as file:
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
) as file:
message = file.read() message = file.read()
return message.format( return message.format(
@@ -155,9 +121,7 @@ class ElectionCycle:
with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file: with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file:
message = file.read() message = file.read()
vote_end: datetime = get_datetime( vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end)
self.config.election_cycles[self.cycle].voting_end
)
return message.format( return message.format(
clan_member_id=self.config.params.clan_member_role_id, clan_member_id=self.config.params.clan_member_role_id,
@@ -176,11 +140,7 @@ class ElectionCycle:
await self.election_start_action() await self.election_start_action()
elif today.date() == self.voting_start.date(): elif today.date() == self.voting_start.date():
await self.voting_start_action() await self.voting_start_action()
elif ( elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date():
(self.voting_end - timedelta(3)).date()
<= today.date()
< self.voting_end.date()
):
await self.election_reminder() await self.election_reminder()
elif today.date() == self.voting_end.date(): elif today.date() == self.voting_end.date():
await self.voting_end_action() await self.voting_end_action()
@@ -194,16 +154,17 @@ class ElectionCycle:
Performs required actions for when the election cycle starts. Performs required actions for when the election cycle starts.
""" """
# create new channel for election # create new channel for election
parent: nextcord.TextChannel = self.guild.get_channel( category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id)
self.config.params.voting_parent_id
)
quarter_name: str = get_election_cycle_name(self.config) quarter_name: str = get_election_cycle_name(self.config)
thread_msg: nextcord.Message = await parent.send(content=quarter_name) # create new entry into database.
election_channel: nextcord.TextChannel = await category.create_text_channel(quarter_name)
thread: nextcord.Thread = await parent.create_thread( clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
name=quarter_name, message=thread_msg await election_channel.set_permissions(clan_member_role, add_reactions=True)
) with Session(engine) as session:
new_cycle = ElectionInformation(ElectionCycle=quarter_name, ElectionChannel=election_channel.id)
session.add(new_cycle)
session.commit()
# post message announcing election # post message announcing election
# election start message is returned as an array of string because it is too long. send each string # election start message is returned as an array of string because it is too long. send each string
@@ -212,21 +173,9 @@ class ElectionCycle:
for index, message in enumerate(messages): for index, message in enumerate(messages):
# if last item, add the nominate button # if last item, add the nominate button
if index + 1 == len(messages): if index + 1 == len(messages):
nominate_msg: nextcord.Message = await thread.send( await election_channel.send(message, view=NominateView(self.config))
message, view=NominateView(self.config, self.guild)
)
else: else:
await thread.send(message) await election_channel.send(message)
# create new entry into database.
with Session(engine) as session:
new_cycle = ElectionInformation(
ElectionCycle=quarter_name,
ThreadId=thread.id,
CouncilNominationMsgId=nominate_msg.id,
)
session.add(new_cycle)
session.commit()
async def voting_start_action(self) -> None: async def voting_start_action(self) -> None:
""" """
@@ -234,28 +183,20 @@ class ElectionCycle:
Removes write permission from the general clan member role. Removes write permission from the general clan member role.
""" """
message = self.build_vote_start() message = self.build_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread( clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
self.get_election_info().ThreadId election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
) await election_channel.send(message)
await election_thread.send(message) vote_msg = await election_channel.send(build_votes_str(self.config), view=VoteView(self.config, self.guild))
vote_msg = await election_thread.send(
build_votes_str(self.config), view=VoteView(self.config, self.guild)
)
with Session(engine) as session: with Session(engine) as session:
stmt = select(ElectionInformation).where( stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
== get_election_cycle_name(self.config)
) )
info = session.scalar(stmt) info = session.scalar(stmt)
info.CouncilVoteMsgId = vote_msg.id info.CouncilVoteMsgId = vote_msg.id
session.commit() session.commit()
thread: nextcord.Thread = self.guild.get_thread(info.ThreadId) await election_channel.set_permissions(clan_member_role, send_messages=False)
nominate_msg: nextcord.Message = await thread.fetch_message(
info.CouncilNominationMsgId
)
await nominate_msg.edit(view=None)
async def voting_end_action(self) -> None: async def voting_end_action(self) -> None:
""" """
@@ -263,40 +204,27 @@ class ElectionCycle:
""" """
message = self.build_highcouncil_election_announce() message = self.build_highcouncil_election_announce()
council_elect_role: nextcord.Role = self.guild.get_role( council_elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id)
self.config.params.clan_council_elect_role_id election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
) clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
nominate_msg: nextcord.Message = await election_thread.send(
message, view=NominateHighCouncilView(self.config, self.guild)
)
election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
# remove vote button from vote message # remove vote button from vote message
stmt = select(ElectionInformation).where( election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
ElectionInformation.ElectionCycle == election_name ElectionInformation.ElectionCycle == election_name
) )
info = session.scalar(stmt) vote_msg_id = session.scalar(stmt)
vote_msg: nextcord.Message = await election_thread.fetch_message( vote_msg: nextcord.Message = await election_channel.fetch_message(vote_msg_id)
info.CouncilVoteMsgId
)
await vote_msg.edit(view=None) await vote_msg.edit(view=None)
# add the highcouncil nomination message to election information
info.HighCouncilNominationMsgId = nominate_msg.id
session.commit()
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database # add the highcouncil role to everyone with 3 or more votes. set elected to true in database
with Session(engine) as session:
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name) stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name)
vote_count = collections.Counter(session.scalars(stmt)) vote_count = collections.Counter(session.scalars(stmt))
for user_id, votes in vote_count.items(): for user_id, votes in vote_count.items():
if votes >= 3: if votes >= 2:
stmt = ( stmt = (
select(CouncilMembers) select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_name) .where(CouncilMembers.ElectionCycle == election_name)
@@ -306,12 +234,13 @@ class ElectionCycle:
council_member.Elected = True council_member.Elected = True
session.commit() session.commit()
member: nextcord.Member = self.guild.get_member(user_id) user: nextcord.Member = self.guild.get_member(user_id)
await member.add_roles( await user.add_roles(council_elect_role)
council_elect_role,
reason=f"{member.display_name} has won a nomination and become a Council Elect.", await election_channel.send(message)
) await election_channel.set_permissions(council_elect_role, send_messages=True, add_reactions=True)
await election_channel.set_permissions(clan_member_role, add_reactions=False)
async def highcouncil_election_start_action(self) -> None: async def highcouncil_election_start_action(self) -> None:
""" """
@@ -319,104 +248,19 @@ class ElectionCycle:
""" """
message = self.build_highcouncil_vote_start() message = self.build_highcouncil_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread( election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
self.get_election_info().ThreadId await election_channel.send(message)
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_highcouncil_votes_str(self.config),
view=VoteHighcouncilView(self.config, self.guild),
)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
)
info = session.scalar(stmt)
info.HighCouncilVoteMsgId = vote_msg.id
session.commit()
# remove highcouncil nomination view
nomination_msg: nextcord.Message = await election_thread.fetch_message(
info.HighCouncilNominationMsgId
)
await nomination_msg.edit(view=None)
async def term_start_action(self) -> None: async def term_start_action(self) -> None:
""" """
Marks the election as over. Moves the election channel into the archive category.
""" """
self.election_over = True self.election_over = True
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
archive: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.archive_channel_group_id)
elect_role: nextcord.Role = self.guild.get_role( await election_channel.move(beginning=True, category=archive)
self.config.params.clan_council_elect_role_id
)
council_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_role_id
)
with Session(engine) as session:
# role management
stmt = (
select(CouncilMembers.MemberId)
.where(
CouncilMembers.ElectionCycle == get_election_cycle_name(self.config)
)
.where(CouncilMembers.Elected == true())
)
for member_id in session.scalars(stmt):
member = self.guild.get_member(member_id)
await member.remove_roles(
elect_role,
reason=f"{member.display_name} no longer needs the elect role.",
)
await member.add_roles(
council_role,
reason=f"{member.display_name} has been elected to council.",
)
# announce high council members
# ties are undefined behavior.
dm_election = get_dm_election_name(self.config)
cl_election = get_cl_election_name(self.config)
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == dm_election)
dm_votes = collections.Counter(session.scalars(stmt))
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election)
cl_votes = collections.Counter(session.scalars(stmt))
try:
dm_winner = max(dm_votes, key=dm_votes.get)
except ValueError:
dm_winner = "No winner"
try:
cl_winner = max(cl_votes, key=cl_votes.get)
except ValueError:
cl_winner = "No winner"
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
# clear view from vote message
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
)
vote_msg_id = session.scalar(stmt)
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 += f"Discord Manager: <@{dm_winner}>"
await election_thread.send(highcouncil_msg)
await election_thread.send(
"This election cycle is now over. Thank you everyone for participating!"
)
await election_thread.edit(archived=True, locked=True)
def get_election_info(self) -> ElectionInformation: def get_election_info(self) -> ElectionInformation:
""" """
@@ -428,8 +272,7 @@ class ElectionCycle:
with Session(engine) as session: with Session(engine) as session:
stmt = select(ElectionInformation).where( stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
== get_election_cycle_name(self.config)
) )
return session.scalar(stmt) return session.scalar(stmt)
@@ -438,11 +281,9 @@ class ElectionCycle:
Sends a message to general chat reminder clan about election. Sends a message to general chat reminder clan about election.
""" """
general: nextcord.TextChannel = self.guild.get_channel( general: nextcord.TextChannel = self.guild.get_channel(self.config.params.general_channel_id)
self.config.params.general_channel_id
)
message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over " message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over "
message += f"to <#{self.get_election_info().ThreadId}> to cast your vote!" message += f"to <#{self.get_election_info().ElectionChannel}> to cast your vote!"
await general.send(content=message) await general.send(content=message)
-266
View File
@@ -1,266 +0,0 @@
"""
Contains various views to be used in the high council nomination process.
"""
import nextcord
from nextcord.interactions import Interaction
from sqlalchemy import select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import (
NOMINEE_SPLITTER,
decorate_cl_nomination,
decorate_dm_nomination,
get_election_cycle_name,
)
class NominateDMModal(nextcord.ui.Modal):
"""
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
"""
def __init__(
self,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__(
"Discord Manager Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.config: DakinyConfig = config
self.usr_nominated = usr_nominated
self.speech = nextcord.ui.TextInput(
label="Nomination Speech",
style=nextcord.TextInputStyle.paragraph,
placeholder="Please write your speech here.",
required=True,
max_length=1500,
default_value=speech_content,
)
self.add_item(self.speech)
async def callback(self, interaction: Interaction) -> None:
"""
Sends the nomination speech into the voting channel and enters the user into the voting database.
Args:
interaction (Interaction): Interaction with user.
"""
speech = decorate_dm_nomination(interaction.user.id, self.speech.value)
with Session(engine) as session:
# user already in table
cycle = get_election_cycle_name(self.config)
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == self.usr_id)
)
council_elect = session.scalar(stmt)
if not self.usr_nominated:
msg = await interaction.channel.send(speech)
council_elect.DiscordManagerSpeechId = msg.id
session.commit()
else:
msg: nextcord.Message = await interaction.channel.fetch_message(
council_elect.DiscordManagerSpeechId
)
await msg.edit(
content=decorate_dm_nomination(
interaction.user.id, self.speech.value
)
)
class NominateCLModal(nextcord.ui.Modal):
"""
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
"""
def __init__(
self,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__(
"Clan Leader Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.config: DakinyConfig = config
self.usr_nominated = usr_nominated
self.speech = nextcord.ui.TextInput(
label="Nomination Speech",
style=nextcord.TextInputStyle.paragraph,
placeholder="Please write your speech here.",
required=True,
max_length=1500,
default_value=speech_content,
)
self.add_item(self.speech)
async def callback(self, interaction: Interaction) -> None:
"""
Sends the nomination speech into the voting channel and enters the user into the voting database.
Args:
interaction (Interaction): Interaction with user.
"""
speech = decorate_cl_nomination(interaction.user.id, self.speech.value)
with Session(engine) as session:
# user already in table
cycle = get_election_cycle_name(self.config)
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == self.usr_id)
)
council_elect = session.scalar(stmt)
if not self.usr_nominated:
msg = await interaction.channel.send(speech)
council_elect.ClanLeaderSpeechId = msg.id
session.commit()
else:
msg: nextcord.Message = await interaction.channel.fetch_message(
council_elect.ClanLeaderSpeechId
)
await msg.edit(
content=decorate_cl_nomination(
interaction.user.id, self.speech.value
)
)
class NominateHighCouncilView(nextcord.ui.View):
"""
The buttons to press to start your High Council nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None)
self.config: DakinyConfig = config
self.guild: nextcord.Guild = guild
@nextcord.ui.button(
label="Nominate for Discord Manager",
style=nextcord.ButtonStyle.primary,
custom_id="nominate-dm",
)
async def nominate_dm(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
"""
Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if (
self.guild.get_role(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config)
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == interaction.user.id)
)
obj = session.scalar(stmt)
# user needs to exist to get here anyways...
if obj.DiscordManagerSpeechId is not None:
usr_nominated = True
msg = await interaction.channel.fetch_message(
obj.DiscordManagerSpeechId
)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(
NominateDMModal(
self.config, interaction.user.id, usr_nominated, speech_content
)
)
@nextcord.ui.button(
label="Nominate for Clan Leader",
style=nextcord.ButtonStyle.primary,
custom_id="nominate-cl",
)
async def nominate_cl(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
"""
Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if (
self.guild.get_role(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config)
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == interaction.user.id)
)
obj = session.scalar(stmt)
# user needs to exist to get here anyways...
if obj.ClanLeaderSpeechId is not None:
usr_nominated = True
msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(
NominateCLModal(
self.config, interaction.user.id, usr_nominated, speech_content
)
)
+28 -38
View File
@@ -9,7 +9,9 @@ from sqlalchemy.orm import Session
from config import DakinyConfig from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine from database_setup.sql_data_classes import CouncilMembers, engine
from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name from utils import get_election_cycle_name
NOMINEE_SPLITTER = "========================================\n\n"
class NominateModal(nextcord.ui.Modal): class NominateModal(nextcord.ui.Modal):
@@ -17,13 +19,7 @@ class NominateModal(nextcord.ui.Modal):
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
""" """
def __init__( def __init__(self, config: DakinyConfig, usr_id: int, usr_nominated: bool, speech_content: str):
self,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__( super().__init__(
"Nomination", "Nomination",
timeout=5 * 60, # 5 minutes timeout=5 * 60, # 5 minutes
@@ -74,12 +70,8 @@ class NominateModal(nextcord.ui.Modal):
) )
obj = session.scalar(stmt) obj = session.scalar(stmt)
msg: nextcord.Message = await interaction.channel.fetch_message( msg: nextcord.Message = await interaction.channel.fetch_message(obj.SpeechMsgId)
obj.SpeechMsgId await msg.edit(content=decorate_nomination(interaction.user.id, self.speech.value))
)
await msg.edit(
content=decorate_nomination(interaction.user.id, self.speech.value)
)
class NominateView(nextcord.ui.View): class NominateView(nextcord.ui.View):
@@ -87,19 +79,12 @@ class NominateView(nextcord.ui.View):
The button to press to start your nomination. The button to press to start your nomination.
""" """
def __init__(self, config: DakinyConfig, guild: nextcord.Guild): def __init__(self, config):
super().__init__(timeout=None) super().__init__(timeout=None)
self.config: DakinyConfig = config self.config: DakinyConfig = config
self.guild: nextcord.Guild = guild
@nextcord.ui.button( @nextcord.ui.button(label="Nominate Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate")
label="Nominate Yourself", async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
style=nextcord.ButtonStyle.primary,
custom_id="nominate",
)
async def nominate(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
""" """
Gathers previous information about the user if they have already nominated themselves, then launches Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech. the nomination modal to get their speech.
@@ -109,17 +94,6 @@ class NominateView(nextcord.ui.View):
interaction (nextcord.Interaction): Interaction of user who pressed the button interaction (nextcord.Interaction): Interaction of user who pressed the button
""" """
# enforce members only
if (
self.guild.get_role(self.config.params.clan_member_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config) cycle = get_election_cycle_name(self.config)
usr_nominated = False usr_nominated = False
speech_content = "" speech_content = ""
@@ -136,7 +110,23 @@ class NominateView(nextcord.ui.View):
speech_content = msg.content.split(NOMINEE_SPLITTER)[1] speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal( await interaction.response.send_modal(
NominateModal( NominateModal(self.config, interaction.user.id, usr_nominated, speech_content)
self.config, interaction.user.id, usr_nominated, speech_content
)
) )
def decorate_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Council Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
-253
View File
@@ -1,253 +0,0 @@
"""
Contains various views to be used while voting for high council. Controls the logic when performing the vote.
"""
from typing import List
import nextcord
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from utils import (
build_highcouncil_votes_str,
get_cl_election_name,
get_dm_election_name,
get_election_cycle_name,
)
class VoteSelect(nextcord.ui.StringSelect):
def __init__(
self, options: List["SelectOption"], config: DakinyConfig, election_type: str
) -> None:
super().__init__(
placeholder="Choose 1",
min_values=1,
max_values=1,
options=options,
)
self.config = config
self.election_type = election_type
async def callback(self, interaction: Interaction):
# decide on election type
match self.election_type:
case "cl":
election_name: str = get_cl_election_name(self.config)
case "dm":
election_name: str = get_dm_election_name(self.config)
case _:
# should never happen
raise ValueError('election type was not "cl" or "dm"')
cycle: str = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
ElectionInformation.ElectionCycle == cycle
)
vote_msg_id = session.scalar(stmt)
if self.values[0] == "Abstain":
voted_for = None
else:
voted_for = int(self.values[0])
new_vote = Votes(
ElectionCycle=election_name,
VoterId=interaction.user.id,
VotedForId=voted_for,
)
try:
session.add(new_vote)
session.commit()
except exc.IntegrityError:
session.rollback()
stmt = (
update(Votes)
.where(Votes.ElectionCycle == election_name)
.where(Votes.VoterId == interaction.user.id)
.values(VotedForId=voted_for)
)
session.execute(stmt)
session.commit()
vote_message_str = build_highcouncil_votes_str(self.config)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id
)
await vote_msg.edit(content=vote_message_str)
if voted_for is not None:
msg = f"You have voted for <@{voted_for}>"
else:
msg = "You have abstained from voting."
await interaction.edit(content=msg, view=None)
class VoteHighcouncilView(nextcord.ui.View):
"""
The button to press to start voting for HighCouncil.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None)
self.config = config
self.guild = guild
@nextcord.ui.button(
label="Vote for Clan Leader",
style=nextcord.ButtonStyle.primary,
custom_id="vote-cl",
)
async def vote_cl(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
"""
Enforces only council elect vote. Gets election cycle, creates voting
options for clan leader (checking if user has voted already),
then sends the voting view.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if (
self.guild.get_role(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle: str = get_election_cycle_name(self.config)
election: str = get_cl_election_name(self.config)
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionCycle == election)
.where(Votes.VoterId == interaction.user.id)
)
voted_for = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == cycle)
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
)
options = [
SelectOption(
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 session.scalars(stmt)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(
VoteSelect(options=options, config=self.config, election_type="cl")
)
await interaction.response.send_message(
content="Please vote for a Clan Leader!",
view=view,
ephemeral=True,
)
@nextcord.ui.button(
label="Vote for Discord Manager",
style=nextcord.ButtonStyle.primary,
custom_id="vote-dm",
)
async def vote_dm(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
"""
Enforces only council elect vote. Gets election cycle, creates voting
options for discord manager (checking if user has voted already),
then sends the voting view.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if (
self.guild.get_role(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle: str = get_election_cycle_name(self.config)
election: str = get_dm_election_name(self.config)
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionCycle == election)
.where(Votes.VoterId == interaction.user.id)
)
voted_for = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == cycle)
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
)
options = [
SelectOption(
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 session.scalars(stmt)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(
VoteSelect(options=options, config=self.config, election_type="dm")
)
await interaction.response.send_message(
content="Please vote for a Discord Manager!",
view=view,
ephemeral=True,
)
+16 -43
View File
@@ -7,16 +7,11 @@ from typing import List
import nextcord import nextcord
from nextcord import SelectOption from nextcord import SelectOption
from nextcord.interactions import Interaction from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update from sqlalchemy import exc, select
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 ( from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from utils import build_votes_str, get_election_cycle_name from utils import build_votes_str, get_election_cycle_name
@@ -43,11 +38,7 @@ class VoteSelect(nextcord.ui.StringSelect):
else: else:
voted_for = int(self.values[0]) voted_for = int(self.values[0])
new_vote = Votes( new_vote = Votes(ElectionCycle=election_name, VoterId=interaction.user.id, VotedForId=voted_for)
ElectionCycle=election_name,
VoterId=interaction.user.id,
VotedForId=voted_for,
)
try: try:
session.add(new_vote) session.add(new_vote)
@@ -56,18 +47,16 @@ class VoteSelect(nextcord.ui.StringSelect):
except exc.IntegrityError: except exc.IntegrityError:
session.rollback() session.rollback()
stmt = ( stmt = (
update(Votes) select(Votes)
.where(Votes.ElectionCycle == election_name) .where(Votes.ElectionCycle == election_name)
.where(Votes.VoterId == interaction.user.id) .where(Votes.VoterId == interaction.user.id)
.values(VotedForId=voted_for)
) )
session.execute(stmt) vote = session.scalar(stmt)
vote.VotedForId = voted_for
session.commit() session.commit()
vote_message_str = build_votes_str(self.config) vote_message_str = build_votes_str(self.config)
vote_msg: nextcord.Message = await interaction.channel.fetch_message( vote_msg: nextcord.Message = await interaction.channel.fetch_message(vote_msg_id)
vote_msg_id
)
await vote_msg.edit(content=vote_message_str) await vote_msg.edit(content=vote_message_str)
if voted_for is not None: if voted_for is not None:
@@ -88,15 +77,10 @@ class VoteView(nextcord.ui.View):
self.config = config self.config = config
self.guild = guild self.guild = guild
@nextcord.ui.button( @nextcord.ui.button(label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote")
label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote" async def vote(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
)
async def vote(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
""" """
Enforces only members vote. Gets election cycle, creates voting options (checking if user has voted already), TODO
then sends the voting view.
Args: Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator button (nextcord.ui.Button): Unused. idk why its needed in the decorator
@@ -104,13 +88,9 @@ class VoteView(nextcord.ui.View):
""" """
# enforce members only # enforce members only
if ( if self.guild.get_role(self.config.params.clan_member_role_id) not in interaction.user.roles:
self.guild.get_role(self.config.params.clan_member_role_id)
not in interaction.user.roles
):
await interaction.response.send_message( await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.", f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.", ephemeral=True
ephemeral=True,
) )
return return
@@ -124,25 +104,18 @@ class VoteView(nextcord.ui.View):
) )
voted_for = session.scalar(stmt) voted_for = session.scalar(stmt)
stmt = select(CouncilMembers.MemberId).where( stmt = select(CouncilMembers.MemberId).where(CouncilMembers.ElectionCycle == election)
CouncilMembers.ElectionCycle == election
)
options = [ options = [
SelectOption( SelectOption(
label=f"{self.guild.get_member(nominee).display_name} ☑️" label=f"{self.guild.get_member(nominee).nick} ☑️"
if voted_for == nominee if voted_for == nominee
else f"{self.guild.get_member(nominee).display_name}", else f"{self.guild.get_member(nominee).nick}",
value=str(nominee), value=str(nominee),
) )
for nominee in session.scalars(stmt) for nominee in session.scalars(stmt)
] ]
options.append( options.append(SelectOption(label="Abstain ☑️" if voted_for is None else "Abstain", value="Abstain"))
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View() view = nextcord.ui.View()
view.add_item(VoteSelect(options=options, config=self.config)) view.add_item(VoteSelect(options=options, config=self.config))
+9 -108
View File
@@ -4,10 +4,8 @@ Contains commands and views for request commands.
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy.orm import Session
from config import DakinyConfig from config import DakinyConfig
from database_setup.sql_data_classes import Presences, engine
class RequestCog(commands.Cog, name="Request Commands"): class RequestCog(commands.Cog, name="Request Commands"):
@@ -27,46 +25,12 @@ class RequestCog(commands.Cog, name="Request Commands"):
Parent slash command for help. Never called. Parent slash command for help. Never called.
""" """
@request.subcommand(
description="Request to add a presence to the bot's presence list."
)
async def add_presence(
self,
interaction: nextcord.Interaction,
presence: str = nextcord.SlashOption(
required=True,
min_length=1,
max_length=100,
description="The presence you want to add.",
),
):
"""
Sends a message to admins to request to add a presence.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
presence (str): The presence to add.
"""
config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to add presence" {presence}."'
view = PresenceChange(presence)
await self.bot.get_channel(config.params.admin_channel_id).send(
content=content, view=view
)
await interaction.response.send_message(
"Request sent to admins.", ephemeral=True
)
@request.subcommand(description="Request to change your Dakiny nickname") @request.subcommand(description="Request to change your Dakiny nickname")
async def change_nickname( async def change_nickname(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
new_nickname: str = nextcord.SlashOption( new_nickname: str = nextcord.SlashOption(
required=True, required=True, min_length=2, max_length=32, description="Your requested new nickname."
min_length=2,
max_length=32,
description="Your requested new nickname.",
), ),
): ):
""" """
@@ -80,12 +44,8 @@ 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 change their username to "{new_nickname}."' content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."'
view = UsernameRequest(interaction.user.id, new_nickname) view = UsernameRequest(interaction.user.id, new_nickname)
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 await interaction.response.send_message("Request sent to admins.", ephemeral=True)
)
await interaction.response.send_message(
"Request sent to admins.", ephemeral=True
)
class UsernameRequest(nextcord.ui.View): class UsernameRequest(nextcord.ui.View):
@@ -93,19 +53,13 @@ class UsernameRequest(nextcord.ui.View):
View provides approve and deny buttons and action for a username change request. View provides approve and deny buttons and action for a username change request.
""" """
def __init__( def __init__(self, user_id: int, new_nickname: str, *, timeout: float | None = 180, auto_defer: bool = True):
self, super().__init__(timeout=timeout, auto_defer=auto_defer)
user_id: int,
new_nickname: str,
):
super().__init__(timeout=None, auto_defer=True)
self.user_id = user_id self.user_id = user_id
self.new_nickname = new_nickname self.new_nickname = new_nickname
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green) @nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve( async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
""" """
Approve button for a nickname change request. Changes the target's nickname to self.new_nickname. Approve button for a nickname change request. Changes the target's nickname to self.new_nickname.
Catches an error if the target as a higher role than the bot. Catches an error if the target as a higher role than the bot.
@@ -119,10 +73,7 @@ class UsernameRequest(nextcord.ui.View):
if user is not None: if user is not None:
try: try:
await user.edit(nick=self.new_nickname) await user.edit(nick=self.new_nickname)
content = ( content = interaction.message.content + f"\nApproved by <@{interaction.user.id}>."
interaction.message.content
+ f"\nApproved by <@{interaction.user.id}>."
)
await interaction.response.edit_message(content=content, view=None) await interaction.response.edit_message(content=content, view=None)
except nextcord.errors.Forbidden: except nextcord.errors.Forbidden:
await interaction.response.send_message( await interaction.response.send_message(
@@ -133,13 +84,10 @@ class UsernameRequest(nextcord.ui.View):
interaction.message.content interaction.message.content
+ "\n\nFailed. User has too high permissions for bot to change nickname." + "\n\nFailed. User has too high permissions for bot to change nickname."
) )
await interaction.followup.edit_message( await interaction.followup.edit_message(interaction.message.id, content=content, view=None)
interaction.message.id, content=content, view=None
)
else: else:
await interaction.response.edit_message( await interaction.response.edit_message(
content="An error occured while changing the user's nickname.", content="An error occured while changing the user's nickname.", view=None
view=None,
) )
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red) @nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
@@ -153,50 +101,3 @@ class UsernameRequest(nextcord.ui.View):
""" """
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>." content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None) await interaction.response.edit_message(content=content, view=None)
class PresenceChange(nextcord.ui.View):
"""
View provides approve and deny buttons and action for a username change request.
"""
def __init__(
self,
presence: str,
):
super().__init__(timeout=None, auto_defer=True)
self.presence = presence
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
Approve button for a presence add request. Adds the presence to the database.
Removes the view from the interaction message.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
with Session(engine) as session:
session.add(Presences(Presence=self.presence))
session.commit()
content = (
interaction.message.content + f"\nApproved by <@{interaction.user.id}>."
)
await interaction.response.edit_message(content=content, view=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Deny button for a presence add request. Just removes the view and logs who denied the request.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
+30 -70
View File
@@ -10,17 +10,6 @@ from config import DakinyConfig
from utils import get_current_cycle from utils import get_current_cycle
def is_me():
"""
predicate that makes sure only I can run the test commands.
"""
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849
return application_checks.check(predicate)
class TestCog(commands.Cog, name="Test commands."): class TestCog(commands.Cog, name="Test commands."):
""" """
Class holds commands for testing. Class holds commands for testing.
@@ -32,14 +21,37 @@ class TestCog(commands.Cog, name="Test commands."):
""" """
self.bot = bot self.bot = bot
@nextcord.slash_command(guild_ids=[1000247361639030804]) def is_me():
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849
return application_checks.check(predicate)
@nextcord.slash_command()
@is_me()
async def test(self, interaction: nextcord.Interaction): async def test(self, interaction: nextcord.Interaction):
""" """
Parent slash command for help. Never called. Parent slash command for help. Never called.
""" """
@test.subcommand() @test.subcommand()
@is_me() async def msg_builder(self, interaction: nextcord.Interaction):
"""
Tests the message builder.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
cycle = ElectionCycle(0, self.bot)
message = cycle.build_election_announcement()
# doesn't tell discord that the message was responded to. Is a test command so I don't really care atm.
for m in message:
await interaction.channel.send(content=m)
@test.subcommand()
async def election_start(self, interaction: nextcord.Interaction): async def election_start(self, interaction: nextcord.Interaction):
""" """
idc idc
@@ -49,15 +61,10 @@ 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), config, self.bot.get_guild(config.params.guild_id))
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.election_start_action() await cycle.election_start_action()
@test.subcommand() @test.subcommand()
@is_me()
async def voting_start(self, interaction: nextcord.Interaction): async def voting_start(self, interaction: nextcord.Interaction):
""" """
tests the voting start action tests the voting start action
@@ -66,15 +73,10 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): _description_
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.voting_start_action() await cycle.voting_start_action()
@test.subcommand() @test.subcommand()
@is_me()
async def voting_end(self, interaction: nextcord.Interaction): async def voting_end(self, interaction: nextcord.Interaction):
""" """
tests the voting start action tests the voting start action
@@ -83,59 +85,17 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): _description_
""" """
config: DakinyConfig = self.bot.config config: DakinyConfig = self.bot.config
cycle = ElectionCycle( cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.voting_end_action() await cycle.voting_end_action()
@test.subcommand() @test.subcommand()
@is_me()
async def highcouncil_vote(self, interaction: nextcord.Interaction):
"""
tests the voting start action
Args:
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.highcouncil_election_start_action()
@test.subcommand()
@is_me()
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), config, self.bot.get_guild(config.params.guild_id))
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.election_reminder() await cycle.election_reminder()
@test.subcommand() @test.subcommand()
@is_me()
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), config, self.bot.get_guild(config.params.guild_id))
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.check_date() await cycle.check_date()
@test.subcommand()
@is_me()
async def term_start(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.term_start_action()
+1 -3
View File
@@ -4,8 +4,7 @@ Initializing code for all loops.
from nextcord.ext import commands from nextcord.ext import commands
from cogs.loops.bot_presence import BotPresence from .daily_loop import DailyLoop
from cogs.loops.daily_loop import DailyLoop
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
@@ -13,5 +12,4 @@ def setup(bot: commands.Bot):
Adds functions to bot. Adds functions to bot.
""" """
bot.add_cog(BotPresence(bot))
bot.add_cog(DailyLoop(bot)) bot.add_cog(DailyLoop(bot))
-48
View File
@@ -1,48 +0,0 @@
"""
Functions for controlling the bot's presence. Also contains the loop.
"""
import nextcord
from nextcord.ext import commands, tasks
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import Presences, engine
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(
self, guild: nextcord.Guild
): # pylint: disable=unused-argument
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=5)
async def periodic_change_presence(self):
"""Loops every 5 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence."""
with Session(engine) as session:
presence = session.scalar(
select(Presences.Presence).order_by(func.random())
)
await self.bot.change_presence(
activity=nextcord.CustomActivity(name="Custom Status", state=presence)
)
+2 -1
View File
@@ -10,8 +10,10 @@ class Params:
Class defines the various configuration parameters. Class defines the various configuration parameters.
""" """
active_category_id: int
admin_channel_id: int # might want to rename this... admin_channel_id: int # might want to rename this...
announcement_category_id: int announcement_category_id: int
archive_channel_group_id: int
clan_council_elect_role_id: int clan_council_elect_role_id: int
clan_council_role_id: int clan_council_role_id: int
clan_info_channel_id: int clan_info_channel_id: int
@@ -20,7 +22,6 @@ class Params:
general_channel_id: int general_channel_id: int
guild_id: int guild_id: int
high_council_role_id: int high_council_role_id: int
voting_parent_id: int
@dataclass @dataclass
+2 -8
View File
@@ -8,9 +8,7 @@ from hydra.core.config_store import ConfigStore
from nextcord.ext import commands from nextcord.ext import commands
import database_setup.create_database as create_database import database_setup.create_database as create_database
from classes.nominate_highcouncil_views import NominateHighCouncilView
from classes.nominate_views import NominateView from classes.nominate_views import NominateView
from classes.voting_highcouncil_views import VoteHighcouncilView
from classes.voting_views import VoteView from classes.voting_views import VoteView
from config import DakinyConfig from config import DakinyConfig
@@ -22,9 +20,7 @@ birthday_list: list[int] = []
logger = logging.getLogger("nextcord") logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w") handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter( handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")
)
logger.addHandler(handler) logger.addHandler(handler)
@@ -55,10 +51,8 @@ def main(cfg: DakinyConfig):
"""When Discord is connected""" """When Discord is connected"""
assert bot.user is not None assert bot.user is not None
create_database # pylint: disable=pointless-statement create_database # pylint: disable=pointless-statement
bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(NominateView(cfg))
bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!") print(f"{bot.user.name} has connected to Discord!")
# Run Discord bot # Run Discord bot
+2 -11
View File
@@ -1,10 +1,7 @@
CREATE TABLE ElectionInformation( CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique, ElectionCycle varchar(30) not null unique,
ThreadId bigint unsigned not null, ElectionChannel bigint unsigned not null,
CouncilVoteMsgId bigint unsigned, CouncilVoteMsgId bigint unsigned,
HighCouncilVoteMsgId bigint unsigned,
CouncilNominationMsgId bigint unsigned,
HighCouncilNominationMsgId bigint unsigned,
PRIMARY KEY (ElectionCycle) PRIMARY KEY (ElectionCycle)
); );
@@ -13,10 +10,9 @@ CREATE TABLE CouncilMembers(
MemberId bigint unsigned not null, MemberId bigint unsigned not null,
SpeechMsgId bigint unsigned not null, SpeechMsgId bigint unsigned not null,
Elected boolean DEFAULT false, Elected boolean DEFAULT false,
HighCouncilElect boolean DEFAULT false,
DiscordManager boolean DEFAULT false, DiscordManager boolean DEFAULT false,
ClanLeader boolean DEFAULT false, ClanLeader boolean DEFAULT false,
DiscordManagerSpeechId bigint unsigned,
ClanLeaderSpeechId bigint unsigned,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle), FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, MemberId) PRIMARY KEY (ElectionCycle, MemberId)
); );
@@ -34,8 +30,3 @@ CREATE TABLE UserBirthday (
Birthday varchar(5) not null, Birthday varchar(5) not null,
PRIMARY KEY (UserId) PRIMARY KEY (UserId)
); );
CREATE TABLE Presences (
Id integer PRIMARY KEY,
Presence varchar(100) not null
);
-1
View File
@@ -15,4 +15,3 @@ ElectionInformation = Base.classes.ElectionInformation
CouncilMembers = Base.classes.CouncilMembers CouncilMembers = Base.classes.CouncilMembers
Votes = Base.classes.Votes Votes = Base.classes.Votes
UserBirthday = Base.classes.UserBirthday UserBirthday = Base.classes.UserBirthday
Presences = Base.classes.Presences
+8 -169
View File
@@ -10,8 +10,6 @@ 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, Votes, engine
NOMINEE_SPLITTER = "========================================\n\n"
def get_datetime(day: str) -> datetime: def get_datetime(day: str) -> datetime:
""" """
@@ -28,7 +26,7 @@ def get_datetime(day: str) -> datetime:
day = int(day.split("/")[1]) day = int(day.split("/")[1])
# ensure that year rolls over if current month is December and the next month is less than 12. # ensure that year rolls over if current month is December and the next month is less than 12.
if datetime.today().month == 12 and month < 12: if datetime.today().month == 12 & month < 12:
year += 1 year += 1
return datetime(year, month, day) return datetime(year, month, day)
@@ -38,46 +36,13 @@ def get_election_cycle_name(config: DakinyConfig) -> str:
""" """
Builds the unique name of the current election cycle Builds the unique name of the current election cycle
Args:
config (DakinyConfig): the config used to know the election cycle.
Returns: Returns:
str: the name of the election cycle str: the name of the election 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"vote-{get_datetime(current_cycle.election_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:
"""
creates the name of the discord manager election
Args:
config (DakinyConfig): Config sent to get_election_cycle_name
Returns:
str: the name of the election
"""
return f"{get_election_cycle_name(config)}-dm"
def get_cl_election_name(config: DakinyConfig) -> str:
"""
creates the name of the clan leader election
Args:
config (DakinyConfig): Config sent to get_election_cycle_name
Returns:
str: the name of the election
"""
return f"{get_election_cycle_name(config)}-cl"
def get_current_cycle(config: DakinyConfig) -> int: def get_current_cycle(config: DakinyConfig) -> int:
@@ -117,148 +82,22 @@ def build_votes_str(config: DakinyConfig) -> str:
votes: dict[int, int] = {} votes: dict[int, int] = {}
with Session(engine) as session: with Session(engine) as session:
stmt = select(CouncilMembers).where( stmt = select(CouncilMembers).where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
CouncilMembers.ElectionCycle == get_election_cycle_name(config)
)
for nominee in session.scalars(stmt): for nominee in session.scalars(stmt):
votes[nominee.MemberId] = 0 votes[nominee.MemberId] = 0
stmt = select(Votes).where( stmt = select(Votes).where(Votes.ElectionCycle == get_election_cycle_name(config))
Votes.ElectionCycle == get_election_cycle_name(config)
)
for vote in session.scalars(stmt): for vote in session.scalars(stmt):
if vote.VotedForId is not None: if vote.VotedForId is not None:
votes[vote.VotedForId] += 1 votes[vote.VotedForId] += 1
votes_str: str = "====================" votes_str: str = "===================="
votes_str += user_vote_str(votes)
return votes_str
def build_highcouncil_votes_str(config: DakinyConfig) -> str:
"""
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
Args:
config (DakinyConfig): Daking config object
Returns:
str: String that contains all nominees and their votes.
"""
cl_votes: dict[int, int] = {}
dm_votes: dict[int, int] = {}
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
)
for nominee in session.scalars(stmt):
cl_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config))
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
cl_votes[vote.VotedForId] += 1
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
)
for nominee in session.scalars(stmt):
dm_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config))
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
dm_votes[vote.VotedForId] += 1
# build the votes string
votes_str: str = "Clan Leader Votes\n===================="
votes_str += user_vote_str(cl_votes)
votes_str += "\n\nDiscord Manager Votes\n===================="
votes_str += user_vote_str(dm_votes)
return votes_str
def user_vote_str(votes: dict[int, int]) -> str:
"""
Creates the string representing how many votes each user has.
Args:
votes (dict[int, int]): Dict of all users and how many votes they have.
Returns:
str: The string showing names and number of votes.
"""
ret_str: str = ""
for user_id, num_votes in votes.items(): for user_id, num_votes in votes.items():
ret_str += f"\n<@{user_id}>: {'☑️' * num_votes}" votes_str += f"\n<@{user_id}>: "
return ret_str
for num in range(num_votes):
votes_str += "☑️"
def decorate_nomination(user_id: int, speech: str) -> str: return votes_str
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Council Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
def decorate_cl_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Leader Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
def decorate_dm_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Discord Manager Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg