Compare commits

...
Author SHA1 Message Date
imcompiling d149caafea Add election reminder.
build_develop / build (push) Successful in 1m26s
Happens daily starting 3 days before election end.
2023-11-03 17:31:52 -04:00
imcompiling 000cb9b257 Merge branch '11-council-member-vote-automation'
build_latest / build (push) Successful in 1m23s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 48s
2023-11-03 15:10:34 -04:00
imcompiling 3fb65a7b0a Process voting end.
build_develop / build (push) Successful in 1m41s
Give the elect role and set elected in database.
2023-11-03 15:06:52 -04:00
imcompiling 66e7159022 Enforce having clan member role for voting.
build_develop / build (push) Successful in 1m31s
2023-11-03 14:27:21 -04:00
imcompiling 5803c336c0 Display nickname while voting. 2023-11-03 13:39:42 -04:00
imcompiling 80c4f053bc Add abstaining from voting. 2023-11-03 13:38:23 -04:00
imcompiling 289b752678 Voting interacts with database. 2023-11-03 13:33:12 -04:00
imcompiling 181b1da879 Don't need voting emote anymore. 2023-11-03 13:24:34 -04:00
imcompiling dcea911564 Fix bug getting ElectionInformation object correctly.
build_develop / build (push) Successful in 1m31s
2023-11-03 12:06:27 -04:00
imcompiling 81e2885e8d Change newlines in votes string.
build_develop / build (push) Successful in 1m26s
2023-11-03 11:56:47 -04:00
imcompiling 14f2e13772 General rework to the voting views.
build_develop / build (push) Successful in 1m31s
2023-11-03 11:55:57 -04:00
imcompiling fa3284b9e3 Pull the vote count message out of vote start.
Make its own builder function in utils.
2023-11-03 11:55:21 -04:00
imcompiling a5ef579767 ElectionName is not unique in the other tables. 2023-11-03 11:53:47 -04:00
imcompiling 9c588a18af Need to track the message where votes are presented. 2023-11-03 11:00:55 -04:00
imcompiling ff68e7df1c Allow for abstain votes.
build_develop / build (push) Successful in 1m24s
2023-11-02 19:57:54 -04:00
imcompiling 5b1b4cfecb Change modal to a view. Still need to fill out functionality.
Funtionality needed: put into database, check on view creation of the user has voted already.
If so, then change the content to show they already voted and who they voted for.
Alert that they can change their vote.
2023-11-02 19:52:20 -04:00
imcompiling 626cca8b81 Start work on vote selection. Modal does not work.
build_develop / build (push) Successful in 1m25s
2023-11-02 17:26:05 -04:00
imcompiling e7bd608017 Add nominees and voting total to voting message. 2023-11-02 17:01:19 -04:00
imcompiling a5f0fb71e0 Update to the election rules.
build_latest / build (push) Successful in 1m26s
2023-11-02 16:36:16 -04:00
imcompiling a0f160c801 Rework to allow users to edit their nomination message.
build_develop / build (push) Successful in 1m27s
2023-11-02 16:22:02 -04:00
imcompiling 19baf84c80 Add speech msg id to database, add the id when submitting a nomination. 2023-11-02 15:12:28 -04:00
imcompiling 133ff79b94 Pass config along so that nominate can add to database with election cycle name. 2023-11-02 15:05:48 -04:00
imcompiling 647af0353d move functions to get election cycle index and name into utils.
build_develop / build (push) Successful in 1m52s
2023-11-02 14:44:26 -04:00
imcompiling 63e3b3edb2 Middle of adding user to council member table. Need to move function for getting election name to utils.
build_develop / build (push) Successful in 1m33s
2023-10-25 15:20:03 -04:00
imcompiling ad58f3a9cb Correct typo. Remove invalid docstring sentence. 2023-10-25 15:11:36 -04:00
imcompiling c013c85f80 Cleaned up nomination message. 2023-10-25 14:51:37 -04:00
imcompiling f86225849c add admin perms to testing command...
build_develop / build (push) Successful in 1m21s
2023-10-24 16:50:49 -04:00
imcompiling a8def8fd69 First start on nomination modal.
build_develop / build (push) Successful in 1m30s
2023-10-24 16:49:54 -04:00
imcompiling e5adfdb16f Changed name of channels to match what was already being used.
build_latest / build (push) Successful in 46s
build_develop / build (push) Successful in 47s
2023-10-24 15:37:27 -04:00
imcompiling ee19f32be9 Merge branch '27-supporting-first-live-action'
build_latest / build (push) Successful in 1m58s
build_develop / build (push) Successful in 47s
2023-10-24 10:35:21 -04:00
imcompiling 8ff53d9a13 Grammar corrections.
build_develop / build (push) Has been cancelled
2023-10-05 14:26:15 -04:00
imcompiling 9609837b1d Control user reactions in election channel. 2023-10-05 14:24:33 -04:00
imcompiling 8cf66351dd Allow clan members to send messages to new channel.
build_develop / build (push) Has been cancelled
2023-09-20 20:14:32 -04:00
imcompiling a4c02b2f77 Change develop build to make a branch specific image.
build_latest / build (push) Successful in 1m27s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 45s
2023-09-15 09:53:45 -04:00
imcompiling 8d8af9ab35 Change cakeday to be put in general channel. 2023-09-15 09:42:58 -04:00
imcompiling f70a7989ae Updated election_cycle init docstring.
build_develop / build (push) Successful in 1m24s
build_latest / build (push) Successful in 43s
2023-09-14 11:55:08 -04:00
imcompiling fb909c009a Quick bugfix for finding next election. Wouldn't happen in live, but better than hard coding. 2023-09-14 11:52:33 -04:00
imcompiling bdefa035ff Merge branch '25-cakeday'
build_latest / build (push) Successful in 44s
build_develop / build (push) Successful in 45s
build_release / build (push) Successful in 43s
2023-09-14 11:34:14 -04:00
imcompiling 85eb52d2e1 add word to message string.
build_develop / build (push) Successful in 1m31s
2023-09-14 11:33:27 -04:00
imcompiling 7d1b2f7cac Create cakedays function. 2023-09-14 11:24:25 -04:00
imcompiling c5c79015a9 Correct spelling. 2023-09-14 11:22:53 -04:00
13 changed files with 526 additions and 68 deletions
+9 -3
View File
@@ -1,6 +1,10 @@
name: build_develop
on: [push]
on:
push:
branches:
- '*'
- '!main'
jobs:
build:
@@ -18,7 +22,9 @@ jobs:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: set env
run: echo "GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:develop .
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_BRANCH}} .
- name: Push Docker Image
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:develop
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_BRANCH}}
+8 -4
View File
@@ -14,10 +14,14 @@ Each quarter, a channel is created here on Discord for clan members to cast thei
**Only clan members can vote and all clan members are granted exactly one equally weighted vote.** If you vote twice for different people, your vote will be annulled.
All clan members are permitted to run for a seat on the Clan Council as long as they:
**i) **have been in the clan for enough time to have participated in at least one election;
**ii)** have no warnings under their name
and
**iii)** have been active (hosted 1 clan event or participate in 1 clan event) in the clan in the past 12 weeks before current election.
**i)** have been in the clan for at least 13 weeks (enough time to have participated in at least one election);
**ii)** have no warnings under their name in the last 6 months and
**iii)** have been active in the clan for the past 13 weeks prior to the first day of elections. To be considered active in that timeframe, one must fulfill the following criteria: hosted/participated in at least three clan events, each one in a different week;
**iv)** are not running under aliases that hide their true identity.
Candidates are permitted to vote for themselves. Once a candidate reaches 3 votes from clan members, they have earned their seat, meaning no more votes can be cast for them.
+2 -2
View File
@@ -8,6 +8,6 @@ From {highcouncil_election_announce} to {highcouncil_election_start}, you will b
Discord Server Manager
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 apply to all candidates of that category.
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 the {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}.
+1 -1
View File
@@ -1,7 +1,7 @@
====================
<@&{clan_member_id}>, it's time to vote for clan council.
React with <{voting_emote_id}> to vote.
Press the Vote button to vote!
Voting ends next {voting_end_day}, {voting_end}.
Results will then be announced.
+92 -36
View File
@@ -2,15 +2,18 @@
Contains the election cycle class
"""
from datetime import datetime
import collections
from datetime import datetime, timedelta
import nextcord
from sqlalchemy import select
from sqlalchemy.orm import Session
from classes.nominate_views import NominateView
from classes.voting_views import VoteView
from config import DakinyConfig
from database_setup.sql_data_classes import ElectionInformation, engine
from utils import get_datetime
from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
from utils import build_votes_str, get_datetime, get_election_cycle_name
class ElectionCycle:
@@ -20,27 +23,26 @@ class ElectionCycle:
"""
def __init__(self, cycle: int, config: DakinyConfig, guild: nextcord.Guild) -> None:
# TODO update
"""
Generates all dates relevant to the election cycle from input election start, term start and end dates.
Generates all dates relevant to the election cycle determined by the config and cycle.
Args:
election_start (datetime): Date the election cycle starts.
term_start (datetime): Date the term being elected starts.
term_end (datetime): Date the term being elected ends.
bot: the discord bot instance
cycle (int): Which election cycle this cycle is.
config (DakinyConfig): The entire config object.
guild (nextcord.Guild): The guild that this election is running in.
"""
self.config: DakinyConfig = config
self.cycle = cycle
self.elecion_cycle = self.config.election_cycles[self.cycle]
self.election_start: datetime = get_datetime(self.elecion_cycle.election_start)
self.term_start: datetime = get_datetime(self.elecion_cycle.term_start)
self.term_end: datetime = get_datetime(self.elecion_cycle.term_end)
self.voting_start: datetime = get_datetime(self.elecion_cycle.voting_start)
self.voting_end: datetime = get_datetime(self.elecion_cycle.voting_end)
self.highcouncil_election_announce: datetime = get_datetime(self.elecion_cycle.highcouncil_election_announce)
self.highcouncil_election_start: datetime = get_datetime(self.elecion_cycle.highcouncil_election_start)
self.highcouncil_election_end: datetime = get_datetime(self.elecion_cycle.highcouncil_election_end)
self.election_cycle = self.config.election_cycles[self.cycle]
self.election_start: datetime = get_datetime(self.election_cycle.election_start)
self.term_start: datetime = get_datetime(self.election_cycle.term_start)
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.guild: nextcord.Guild = guild
self.election_over: bool = False
@@ -56,7 +58,9 @@ class ElectionCycle:
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) % 4].election_start)
next_election = get_datetime(
self.config.election_cycles[(self.cycle + 1) % len(self.config.election_cycles)].election_start
)
message = message.format(
clan_council_id=self.config.params.clan_council_role_id,
@@ -121,7 +125,6 @@ class ElectionCycle:
return message.format(
clan_member_id=self.config.params.clan_member_role_id,
voting_emote_id=self.config.params.voting_emote_id,
voting_end=vote_end.strftime("%B %d"),
voting_end_day=vote_end.strftime("%A"),
)
@@ -137,6 +140,8 @@ 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():
await self.election_reminder()
elif today.date() == self.voting_end.date():
await self.voting_end_action()
elif today.date() == self.highcouncil_election_start.date():
@@ -150,10 +155,12 @@ class ElectionCycle:
"""
# create new channel for election
category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id)
quarter_name: str = self.get_election_cycle_name()
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)
@@ -163,30 +170,77 @@ class ElectionCycle:
# election start message is returned as an array of string because it is too long. send each string
# individually.
messages = self.build_election_announcement()
for message in messages:
await election_channel.send(message)
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))
else:
await election_channel.send(message)
async def voting_start_action(self) -> None:
"""
Sends message informing members that voting has started.
Removes write permission from the general clan member role.
Adds the voting emote to all messages not sent by the vote. (This is temporary until voting is managed.)
"""
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))
with Session(engine) as session:
stmt = select(ElectionInformation).where(
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)
async def voting_end_action(self) -> None:
"""
Performs required actions for when voting time has ended.
"""
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)
# remove vote button from vote message
election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.CouncilVoteMsgId).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)
# 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)
vote_count = collections.Counter(session.scalars(stmt))
for user_id, votes in vote_count.items():
if votes >= 3:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_name)
.where(CouncilMembers.MemberId == user_id)
)
council_member = session.scalar(stmt)
council_member.Elected = True
session.commit()
user: 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)
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:
"""
@@ -208,16 +262,6 @@ class ElectionCycle:
await election_channel.move(beginning=True, category=archive)
def get_election_cycle_name(self) -> str:
"""
Builds the unique name of the current election cycle
Returns:
str: the name of the election cycle
"""
return f"{self.election_start.year} {self.elecion_cycle.name} election"
def get_election_info(self) -> ElectionInformation:
"""
Gets information for current election.
@@ -228,6 +272,18 @@ class ElectionCycle:
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == self.get_election_cycle_name()
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
)
return session.scalar(stmt)
async def election_reminder(self) -> None:
"""
Sends a message to general chat reminder clan about election.
"""
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!"
await general.send(content=message)
+133
View File
@@ -0,0 +1,133 @@
"""
Contains various views to be used in the 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 get_election_cycle_name
NOMINEE_SPLITTER = "========================================================\n\n"
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):
super().__init__(
"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_nomination(interaction.user.id, self.speech.value)
with Session(engine) as session:
if not self.usr_nominated:
msg = await interaction.channel.send(speech)
council_member = CouncilMembers(
ElectionCycle=get_election_cycle_name(self.config),
MemberId=interaction.user.id,
SpeechMsgId=msg.id,
)
session.add(council_member)
session.commit()
else:
# user already nominated, just need to update the election speech
cycle = get_election_cycle_name(self.config)
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == self.usr_id)
)
obj = session.scalar(stmt)
obj.SpeechMsgContents = 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):
"""
The button to press to start your nomination.
"""
def __init__(self, config):
super().__init__(timeout=None)
self.config: DakinyConfig = config
@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.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
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)
if obj is not None:
usr_nominated = True
msg = await interaction.channel.fetch_message(obj.SpeechMsgId)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(
NominateModal(self.config, interaction.user.id, usr_nominated, speech_content)
)
def decorate_nomination(user_id, speech) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (_type_): ID of user sending the speech
speech (_type_): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Council Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
+127
View File
@@ -0,0 +1,127 @@
"""
Contains various views to be used while voting. 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
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
from utils import build_votes_str, get_election_cycle_name
class VoteSelect(nextcord.ui.StringSelect):
def __init__(self, options: List["SelectOption"], config: DakinyConfig) -> None:
super().__init__(
placeholder="Choose 1",
min_values=1,
max_values=1,
options=options,
)
self.config = config
async def callback(self, interaction: Interaction):
election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
ElectionInformation.ElectionCycle == election_name
)
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 = (
select(Votes)
.where(Votes.ElectionCycle == election_name)
.where(Votes.VoterId == interaction.user.id)
)
vote = session.scalar(stmt)
vote.VotedForId = voted_for
session.commit()
vote_message_str = build_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 VoteView(nextcord.ui.View):
"""
The button to press to start your nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None)
self.config = config
self.guild = guild
@nextcord.ui.button(label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote")
async def vote(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
"""
TODO
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
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
election: str = get_election_cycle_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 == election)
options = [
SelectOption(
label=f"{self.guild.get_member(nominee).nick} ☑️"
if voted_for == nominee
else f"{self.guild.get_member(nominee).nick}",
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))
await interaction.response.send_message(
content="Please vote for a clan member!",
view=view,
ephemeral=True,
)
+38 -3
View File
@@ -3,9 +3,11 @@ Commands for testing.
"""
import nextcord
from nextcord.ext import commands
from nextcord.ext import application_checks, commands
from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from utils import get_current_cycle
class TestCog(commands.Cog, name="Test commands."):
@@ -19,7 +21,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()
async def test(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
@@ -51,7 +60,8 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_
"""
cycle = ElectionCycle(0, self.bot)
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.election_start_action()
@test.subcommand()
@@ -62,5 +72,30 @@ class TestCog(commands.Cog, name="Test commands."):
Args:
interaction (nextcord.Interaction): _description_
"""
cycle = ElectionCycle(0, self.bot)
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.voting_start_action()
@test.subcommand()
async def voting_end(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.voting_end_action()
@test.subcommand()
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))
await cycle.election_reminder()
@test.subcommand()
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))
await cycle.check_date()
+28 -14
View File
@@ -15,7 +15,7 @@ from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine
from utils import get_datetime
from utils import get_current_cycle
class DailyLoop(commands.Cog, name="Daily Loop"):
@@ -40,7 +40,7 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
"""
if self.current_election is None:
self.current_election = self.get_current_cycle()
self.current_election = self.get_election_cycle()
if self.current_election is not None:
await self.current_election.check_date()
@@ -54,6 +54,8 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
kandy: nextcord.User = await self.bot.fetch_user(239547993106284544)
await kandy.send('Happy "Birthday"!')
await self.cakedays()
@daily_loop.before_loop
async def before_loop(self):
"""
@@ -63,9 +65,9 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
await self.bot.wait_until_ready()
# Bot needs to know guilds to get the current cycle.
self.current_election = self.get_current_cycle()
self.current_election = self.get_election_cycle()
def get_current_cycle(self) -> ElectionCycle:
def get_election_cycle(self) -> ElectionCycle:
"""
Creates an object for the current election cycle if one is active. An election is considered active
between the election start and the term start.
@@ -74,18 +76,10 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
ElectionCycle: Current election cycle, None if there is no election happening.
"""
today = datetime.today()
config: DakinyConfig = self.bot.config
current_election = None
cycle = get_current_cycle(config)
for cycle, election in enumerate(config.election_cycles):
if (
today.date() >= get_datetime(election.election_start).date()
and today.date() <= get_datetime(election.term_start).date()
):
current_election = ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
return current_election
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
def update_birthdays(self) -> None:
"""
@@ -104,3 +98,23 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
with Session(engine) as session:
to_add = session.scalars(stmt)
birthday_list.extend(to_add.all())
async def cakedays(self) -> None:
"""
Loops through the guild's humans checking for joined_at dates that match today's date. Sends a message
saying happy cakeday.
"""
today = datetime.today()
guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id)
announce_channel: nextcord.TextChannel = guild.get_channel(self.bot.config.params.general_channel_id)
cakedays: list[nextcord.Member] = [
human for human in guild.humans if human.joined_at.month == today.month and human.joined_at.day == today.day
]
for member in cakedays:
years = today.year - member.joined_at.year
year_str = "years" if years > 1 else "year"
msg = f"Happy Cake day! <@{member.id}> joined the discord {years} {year_str} ago!"
await announce_channel.send(msg)
+2 -2
View File
@@ -12,16 +12,16 @@ class Params:
active_category_id: int
admin_channel_id: int # might want to rename this...
annoucement_category_id: int
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
clan_member_role_id: int
discord_token: str
general_channel_id: int
guild_id: int
high_council_role_id: int
voting_emote_id: str
@dataclass
+4
View File
@@ -8,6 +8,8 @@ from hydra.core.config_store import ConfigStore
from nextcord.ext import commands
import database_setup.create_database as create_database
from classes.nominate_views import NominateView
from classes.voting_views import VoteView
from config import DakinyConfig
cs = ConfigStore.instance()
@@ -49,6 +51,8 @@ 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(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!")
# Run Discord bot
+5 -3
View File
@@ -1,12 +1,14 @@
CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique,
ElectionChannel bigint unsigned not null,
CouncilVoteMsgId bigint unsigned,
PRIMARY KEY (ElectionCycle)
);
CREATE TABLE CouncilMembers(
ElectionCycle varchar(30) not null unique,
ElectionCycle varchar(30) not null,
MemberId bigint unsigned not null,
SpeechMsgId bigint unsigned not null,
Elected boolean DEFAULT false,
HighCouncilElect boolean DEFAULT false,
DiscordManager boolean DEFAULT false,
@@ -16,9 +18,9 @@ CREATE TABLE CouncilMembers(
);
CREATE TABLE Votes(
ElectionCycle varchar(30) not null unique,
ElectionCycle varchar(30) not null,
VoterId bigint unsigned not null,
VotedForId bigint unsigned not null,
VotedForId bigint unsigned,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, VoterId)
);
+77
View File
@@ -4,6 +4,12 @@ File contains util functions.
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, Votes, engine
def get_datetime(day: str) -> datetime:
"""
@@ -24,3 +30,74 @@ def get_datetime(day: str) -> datetime:
year += 1
return datetime(year, month, day)
def get_election_cycle_name(config: DakinyConfig) -> str:
"""
Builds the unique name of the current 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}"
def get_current_cycle(config: DakinyConfig) -> int:
"""
Returns index for active election. An election is considered active between the election start and the term start.
Args:
config (DakinyConfig): Config object to use
Returns:
int: cycle index for active election. -1 for no active cycle
"""
today = datetime.today()
for cycle, election in enumerate(config.election_cycles):
if (
today.date() >= get_datetime(election.election_start).date()
and today.date() <= get_datetime(election.term_start).date()
):
return cycle
return -1
def build_votes_str(config: DakinyConfig) -> str:
"""
Creates a string that contains all 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.
"""
votes: dict[int, int] = {}
with Session(engine) as session:
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))
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 += "☑️"
return votes_str