Compare commits

..
Author SHA1 Message Date
imcompiling 45b304308b Merge branch '35-remove-nomination-views'
build_latest / build (push) Successful in 45s
build_release / build (push) Successful in 45s
2023-12-17 15:36:50 -05:00
imcompiling 43ef19fd82 Cover highcouncil edge case.
build_develop / build (push) Successful in 1m29s
2023-12-17 15:36:04 -05:00
imcompiling 757d2e5c19 Stored nomination message in wrong place. 2023-12-17 15:27:54 -05:00
imcompiling 8394cdd6fd Remove nomination views once they are done. 2023-12-17 15:06:19 -05:00
imcompiling a9c3719c29 Add two new fields to election information. 2023-12-17 14:44:32 -05:00
imcompiling 250de5baf8 Merge branch '18-highcouncil-election'
build_latest / build (push) Successful in 1m39s
build_develop / build (push) Successful in 48s
2023-12-17 14:36:57 -05:00
imcompiling d449dc6d1a Clear voting buttons for highcouncil on end.
build_develop / build (push) Successful in 46s
2023-12-17 14:29:03 -05:00
imcompiling b8e35b5e90 Fix bugs with the voting results string builder. 2023-12-17 14:17:35 -05:00
imcompiling 644eb1dc31 Fix none query issue. 2023-12-17 14:11:04 -05:00
imcompiling 09ad45c897 Correct election name mixup. Use the highcouncil build string. 2023-12-17 13:52:57 -05:00
imcompiling 55259a2653 Add comment about ties in highcouncil elect. 2023-12-17 13:01:12 -05:00
imcompiling d75c64ba28 Update message decorator for each type of highcouncil elect. 2023-12-17 12:49:44 -05:00
imcompiling 08cf614a60 Add new test command. 2023-12-17 12:45:41 -05:00
imcompiling 18d433ce8c Add highcouncil winners message. 2023-12-17 12:33:05 -05:00
imcompiling a1896416fc Make highcouncil vote string builder.
build_develop / build (push) Successful in 1m36s
Break out repeated code into helper function.
2023-12-16 23:14:23 -05:00
imcompiling a7eafdd52a Create highcouncil voting views. 2023-12-16 23:01:51 -05:00
imcompiling 8e27e03709 Need a message to track highcouncil votes. 2023-12-16 22:32:34 -05:00
imcompiling be507f3970 Add highcouncil nominate view to message. 2023-12-16 22:21:35 -05:00
imcompiling 88d8e6178c Need to have one view. 2023-12-16 22:20:36 -05:00
imcompiling ee13019a0f Create clan leader nomination views.
build_develop / build (push) Successful in 1m41s
2023-12-16 22:11:52 -05:00
imcompiling 6e8f999611 Create discord manager nominate views. 2023-12-16 22:06:39 -05:00
imcompiling 505f27110d Don't need booleans, use ids as booleans. 2023-12-16 21:59:21 -05:00
imcompiling 1c33139f00 Modify database for highcouncil elections. 2023-12-16 21:53:11 -05:00
imcompiling b292f956e8 Add functions for different election names within same cycle. 2023-12-16 21:52:56 -05:00
imcompiling 654c74e622 Move a function to utils. 2023-12-16 21:52:14 -05:00
imcompiling 969880fdb6 Merge commit 'f9da9fe7ef1b4d134884bba202af6a8ae5b06203'
build_latest / build (push) Successful in 1m10s
build_release / build (push) Successful in 49s
2023-12-13 17:36:01 -05:00
imcompiling f9da9fe7ef Merge branch 'main' into 33-fixes-from-mock-election
build_develop / build (push) Successful in 1m24s
2023-11-08 22:53:40 -05:00
imcompiling b5ea709c3a Add council role management to term start.
build_develop / build (push) Successful in 1m29s
2023-11-08 22:50:42 -05:00
imcompiling 9980c49764 Need to use display name in case someone doesn't have a nickname.
build_develop / build (push) Successful in 1m27s
2023-11-08 22:22:32 -05:00
imcompiling 8d02e28271 clean up test file a little.
build_latest / build (push) Successful in 1m26s
2023-11-08 16:31:38 -05:00
imcompiling 3a5add3c60 Remove unneeded config items.
build_latest / build (push) Successful in 1m29s
2023-11-08 16:28:07 -05:00
imcompiling dd1c304522 Add role check to nominate button.
build_develop / build (push) Successful in 1m24s
2023-11-08 16:17:16 -05:00
imcompiling 619ae92d03 Merge branch '30-threads'
build_develop / build (push) Successful in 46s
build_release / build (push) Successful in 44s
build_latest / build (push) Successful in 45s
2023-11-08 16:12:07 -05:00
imcompiling 431e9c0b35 Filled out term start action.
build_develop / build (push) Successful in 1m32s
2023-11-08 16:11:47 -05:00
imcompiling 27f90bbaef Add test for term start. 2023-11-08 15:51:06 -05:00
imcompiling c98bb5888c Give a message for thread to attach to. Lets it be public. 2023-11-08 15:44:19 -05:00
imcompiling c38632ca21 Remove another permission setting.
name a parameter.
2023-11-08 15:43:58 -05:00
imcompiling 75fc59ed5e Removed references to ElectionChannel.
Removed permission managing as threads don't have permissions.
2023-11-08 15:30:05 -05:00
imcompiling 8d2f2f7dba Thread create ini election start.
build_develop / build (push) Successful in 1m35s
Modified config and database to use threads.
2023-11-07 15:36:33 -05:00
imcompiling ab88652cdb Automatic reformat. Guessing from vscode update. 2023-11-07 15:35:43 -05:00
10 changed files with 1030 additions and 132 deletions
+216 -57
View File
@@ -6,14 +6,28 @@ import collections
from datetime import datetime, timedelta
import nextcord
from sqlalchemy import select
from sqlalchemy import select, true
from sqlalchemy.orm import Session
from classes.nominate_highcouncil_views import NominateHighCouncilView
from classes.nominate_views import NominateView
from classes.voting_highcouncil_views import VoteHighcouncilView
from classes.voting_views import VoteView
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
from utils import build_votes_str, get_datetime, get_election_cycle_name
from database_setup.sql_data_classes import (
CouncilMembers,
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:
@@ -40,9 +54,15 @@ class ElectionCycle:
self.term_end: datetime = get_datetime(self.election_cycle.term_end)
self.voting_start: datetime = get_datetime(self.election_cycle.voting_start)
self.voting_end: datetime = get_datetime(self.election_cycle.voting_end)
self.highcouncil_election_announce: datetime = get_datetime(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_announce: datetime = get_datetime(
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.guild: nextcord.Guild = guild
self.election_over: bool = False
@@ -55,11 +75,15 @@ class ElectionCycle:
long.
"""
with open("bot_messages/election_announcement.txt", mode="r", encoding="utf8") as file:
with open(
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
next_election = get_datetime(
self.config.election_cycles[(self.cycle + 1) % len(self.config.election_cycles)].election_start
self.config.election_cycles[
(self.cycle + 1) % len(self.config.election_cycles)
].election_start
)
message = message.format(
@@ -84,14 +108,22 @@ class ElectionCycle:
str: Message to send.
"""
with open("bot_messages/highcouncil_election.txt", mode="r", encoding="utf8") as file:
with open(
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
return message.format(
clan_council_elect_role_id=self.config.params.clan_council_elect_role_id,
highcouncil_election_announce=self.highcouncil_election_announce.strftime("%B %d"),
highcouncil_election_start=self.highcouncil_election_start.strftime("%B %d"),
highcouncil_election_end=self.highcouncil_election_end.strftime("%B %d"),
highcouncil_election_announce=self.highcouncil_election_announce.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"),
)
@@ -103,7 +135,9 @@ class ElectionCycle:
str: Message to send.
"""
with open("bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8") as file:
with open(
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
return message.format(
@@ -121,7 +155,9 @@ class ElectionCycle:
with open("bot_messages/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)
vote_end: datetime = get_datetime(
self.config.election_cycles[self.cycle].voting_end
)
return message.format(
clan_member_id=self.config.params.clan_member_role_id,
@@ -140,7 +176,11 @@ class ElectionCycle:
await self.election_start_action()
elif today.date() == self.voting_start.date():
await self.voting_start_action()
elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date():
elif (
(self.voting_end - timedelta(3)).date()
<= today.date()
< self.voting_end.date()
):
await self.election_reminder()
elif today.date() == self.voting_end.date():
await self.voting_end_action()
@@ -154,17 +194,16 @@ class ElectionCycle:
Performs required actions for when the election cycle starts.
"""
# create new channel for election
category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id)
parent: nextcord.TextChannel = self.guild.get_channel(
self.config.params.voting_parent_id
)
quarter_name: str = get_election_cycle_name(self.config)
# create new entry into database.
election_channel: nextcord.TextChannel = await category.create_text_channel(quarter_name)
clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
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()
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
thread: nextcord.Thread = await parent.create_thread(
name=quarter_name, message=thread_msg
)
# post message announcing election
# election start message is returned as an array of string because it is too long. send each string
@@ -173,9 +212,21 @@ class ElectionCycle:
for index, message in enumerate(messages):
# if last item, add the nominate button
if index + 1 == len(messages):
await election_channel.send(message, view=NominateView(self.config))
nominate_msg: nextcord.Message = await thread.send(
message, view=NominateView(self.config, self.guild)
)
else:
await election_channel.send(message)
await thread.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:
"""
@@ -183,20 +234,28 @@ class ElectionCycle:
Removes write permission from the general clan member role.
"""
message = self.build_vote_start()
clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
await election_channel.send(message)
vote_msg = await election_channel.send(build_votes_str(self.config), view=VoteView(self.config, self.guild))
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_votes_str(self.config), view=VoteView(self.config, self.guild)
)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
)
info = session.scalar(stmt)
info.CouncilVoteMsgId = vote_msg.id
session.commit()
await election_channel.set_permissions(clan_member_role, send_messages=False)
thread: nextcord.Thread = self.guild.get_thread(info.ThreadId)
nominate_msg: nextcord.Message = await thread.fetch_message(
info.CouncilNominationMsgId
)
await nominate_msg.edit(view=None)
async def voting_end_action(self) -> None:
"""
@@ -204,22 +263,35 @@ class ElectionCycle:
"""
message = self.build_highcouncil_election_announce()
council_elect_role: nextcord.Role = self.guild.get_role(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)
council_elect_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_elect_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)
)
# remove vote button from vote message
election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
# remove vote button from vote message
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == election_name
)
vote_msg_id = session.scalar(stmt)
vote_msg: nextcord.Message = await election_channel.fetch_message(vote_msg_id)
await vote_msg.edit(view=None)
info = session.scalar(stmt)
vote_msg: nextcord.Message = await election_thread.fetch_message(
info.CouncilVoteMsgId
)
await vote_msg.edit(view=None)
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database
with Session(engine) as session:
# 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
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name)
vote_count = collections.Counter(session.scalars(stmt))
@@ -234,13 +306,12 @@ class ElectionCycle:
council_member.Elected = True
session.commit()
user: nextcord.Member = self.guild.get_member(user_id)
member: nextcord.Member = self.guild.get_member(user_id)
await user.add_roles(council_elect_role)
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)
await member.add_roles(
council_elect_role,
reason=f"{member.display_name} has won a nomination and become a Council Elect.",
)
async def highcouncil_election_start_action(self) -> None:
"""
@@ -248,19 +319,104 @@ class ElectionCycle:
"""
message = self.build_highcouncil_vote_start()
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
await election_channel.send(message)
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_highcouncil_votes_str(self.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:
"""
Moves the election channel into the archive category.
Marks the election as over.
"""
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)
await election_channel.move(beginning=True, category=archive)
elect_role: nextcord.Role = self.guild.get_role(
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:
"""
@@ -272,7 +428,8 @@ class ElectionCycle:
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
)
return session.scalar(stmt)
@@ -281,9 +438,11 @@ class ElectionCycle:
Sends a message to general chat reminder clan about election.
"""
general: nextcord.TextChannel = self.guild.get_channel(self.config.params.general_channel_id)
general: nextcord.TextChannel = self.guild.get_channel(
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"to <#{self.get_election_info().ElectionChannel}> to cast your vote!"
message += f"to <#{self.get_election_info().ThreadId}> to cast your vote!"
await general.send(content=message)
+266
View File
@@ -0,0 +1,266 @@
"""
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
)
)
+38 -28
View File
@@ -9,9 +9,7 @@ from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import get_election_cycle_name
NOMINEE_SPLITTER = "========================================\n\n"
from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name
class NominateModal(nextcord.ui.Modal):
@@ -19,7 +17,13 @@ class NominateModal(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):
def __init__(
self,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__(
"Nomination",
timeout=5 * 60, # 5 minutes
@@ -70,8 +74,12 @@ class NominateModal(nextcord.ui.Modal):
)
obj = session.scalar(stmt)
msg: nextcord.Message = await interaction.channel.fetch_message(obj.SpeechMsgId)
await msg.edit(content=decorate_nomination(interaction.user.id, self.speech.value))
msg: nextcord.Message = await interaction.channel.fetch_message(
obj.SpeechMsgId
)
await msg.edit(
content=decorate_nomination(interaction.user.id, self.speech.value)
)
class NominateView(nextcord.ui.View):
@@ -79,12 +87,19 @@ class NominateView(nextcord.ui.View):
The button to press to start your nomination.
"""
def __init__(self, config):
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 Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate")
async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
@nextcord.ui.button(
label="Nominate Yourself",
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
the nomination modal to get their speech.
@@ -94,6 +109,17 @@ class NominateView(nextcord.ui.View):
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)
usr_nominated = False
speech_content = ""
@@ -110,23 +136,7 @@ class NominateView(nextcord.ui.View):
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(
NominateModal(self.config, interaction.user.id, usr_nominated, speech_content)
NominateModal(
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
@@ -0,0 +1,253 @@
"""
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,
)
+2 -2
View File
@@ -130,9 +130,9 @@ class VoteView(nextcord.ui.View):
options = [
SelectOption(
label=f"{self.guild.get_member(nominee).nick} ☑️"
label=f"{self.guild.get_member(nominee).display_name} ☑️"
if voted_for == nominee
else f"{self.guild.get_member(nominee).nick}",
else f"{self.guild.get_member(nominee).display_name}",
value=str(nominee),
)
for nominee in session.scalars(stmt)
+70 -30
View File
@@ -10,6 +10,17 @@ from config import DakinyConfig
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 holds commands for testing.
@@ -21,37 +32,14 @@ class TestCog(commands.Cog, name="Test commands."):
"""
self.bot = bot
def is_me():
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849
return application_checks.check(predicate)
@nextcord.slash_command()
@is_me()
@nextcord.slash_command(guild_ids=[1000247361639030804])
async def test(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
"""
@test.subcommand()
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()
@is_me()
async def election_start(self, interaction: nextcord.Interaction):
"""
idc
@@ -61,10 +49,15 @@ class TestCog(commands.Cog, name="Test commands."):
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.election_start_action()
@test.subcommand()
@is_me()
async def voting_start(self, interaction: nextcord.Interaction):
"""
tests the voting start action
@@ -73,10 +66,15 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.voting_start_action()
@test.subcommand()
@is_me()
async def voting_end(self, interaction: nextcord.Interaction):
"""
tests the voting start action
@@ -85,17 +83,59 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.voting_end_action()
@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):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.election_reminder()
@test.subcommand()
@is_me()
async def daily(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
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 -2
View File
@@ -10,10 +10,8 @@ class Params:
Class defines the various configuration parameters.
"""
active_category_id: int
admin_channel_id: int # might want to rename this...
announcement_category_id: int
archive_channel_group_id: int
clan_council_elect_role_id: int
clan_council_role_id: int
clan_info_channel_id: int
@@ -22,6 +20,7 @@ class Params:
general_channel_id: int
guild_id: int
high_council_role_id: int
voting_parent_id: int
@dataclass
+8 -2
View File
@@ -8,7 +8,9 @@ from hydra.core.config_store import ConfigStore
from nextcord.ext import commands
import database_setup.create_database as create_database
from classes.nominate_highcouncil_views import NominateHighCouncilView
from classes.nominate_views import NominateView
from classes.voting_highcouncil_views import VoteHighcouncilView
from classes.voting_views import VoteView
from config import DakinyConfig
@@ -20,7 +22,9 @@ birthday_list: list[int] = []
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
handler.setFormatter(
logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")
)
logger.addHandler(handler)
@@ -51,8 +55,10 @@ def main(cfg: DakinyConfig):
"""When Discord is connected"""
assert bot.user is not None
create_database # pylint: disable=pointless-statement
bot.add_view(NominateView(cfg))
bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id)))
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(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!")
# Run Discord bot
+7 -3
View File
@@ -1,18 +1,22 @@
CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique,
ElectionChannel bigint unsigned not null,
ThreadId bigint unsigned not null,
CouncilVoteMsgId bigint unsigned,
HighCouncilVoteMsgId bigint unsigned,
CouncilNominationMsgId bigint unsigned,
HighCouncilNominationMsgId bigint unsigned,
PRIMARY KEY (ElectionCycle)
);
CREATE TABLE CouncilMembers(
ElectionCycle varchar(30) not null,
MemberId bigint unsigned not null,
SpeechMsgId bigint unsigned not null,
SpeechMsgId bigint unsigned not null,
Elected boolean DEFAULT false,
HighCouncilElect boolean DEFAULT false,
DiscordManager boolean DEFAULT false,
ClanLeader boolean DEFAULT false,
DiscordManagerSpeechId bigint unsigned,
ClanLeaderSpeechId bigint unsigned,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, MemberId)
);
+169 -8
View File
@@ -10,6 +10,8 @@ from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, Votes, engine
NOMINEE_SPLITTER = "========================================\n\n"
def get_datetime(day: str) -> datetime:
"""
@@ -36,13 +38,46 @@ def get_election_cycle_name(config: DakinyConfig) -> str:
"""
Builds the unique name of the current election cycle
Args:
config (DakinyConfig): the config used to know the election cycle.
Returns:
str: the name of the election cycle
"""
cycle = get_current_cycle(config)
current_cycle = config.election_cycles[cycle]
return f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}"
return (
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:
@@ -82,22 +117,148 @@ def build_votes_str(config: DakinyConfig) -> str:
votes: dict[int, int] = {}
with Session(engine) as session:
stmt = select(CouncilMembers).where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
stmt = select(CouncilMembers).where(
CouncilMembers.ElectionCycle == get_election_cycle_name(config)
)
for nominee in session.scalars(stmt):
votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_election_cycle_name(config))
stmt = select(Votes).where(
Votes.ElectionCycle == get_election_cycle_name(config)
)
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
votes[vote.VotedForId] += 1
votes_str: str = "===================="
for user_id, num_votes in votes.items():
votes_str += f"\n<@{user_id}>: "
for num in range(num_votes):
votes_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():
ret_str += f"\n<@{user_id}>: {'☑️' * num_votes}"
return ret_str
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
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