Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbfd6e20bc | ||
|
|
34da664af0 | ||
|
|
c6c44afb76 | ||
|
|
c2d8239911 | ||
|
|
bec6494697 | ||
|
|
29a6b000ac | ||
|
|
dbd2863aab | ||
|
|
426f009ca7 | ||
|
|
ed07b2e231 | ||
|
|
0caa41cc90 | ||
|
|
e93f37dcf0 | ||
|
|
44c3961c46 | ||
|
|
0dcdd038b8 | ||
|
|
eb5d465ac9 | ||
|
|
eb1c87fbf4 | ||
|
|
cbd73a104e | ||
|
|
95ad47368c | ||
|
|
dea3f7cabf | ||
|
|
6a71e9ab1d | ||
|
|
bc56830396 | ||
|
|
1d14f8171d | ||
|
|
2efd380a7e | ||
|
|
475170025a | ||
|
|
f4f3bfe103 | ||
|
|
6827658b4a | ||
|
|
85a3ea0418 | ||
|
|
0b27c60bea | ||
|
|
1a6f373e03 | ||
|
|
0ff13453be | ||
|
|
6b29e26140 | ||
|
|
bbcb4e4dff | ||
|
|
00c8227b8e | ||
|
|
04c44bc470 | ||
|
|
3761d3bb77 | ||
|
|
a76bc02d4a | ||
|
|
3890c30d1f | ||
|
|
1e1407c918 | ||
|
|
be408307aa | ||
|
|
069dafe9ae | ||
|
|
7d54a0c30b | ||
|
|
39fd55146e | ||
|
|
a8323c9a88 | ||
|
|
51d6713940 | ||
|
|
65ed5e7b59 | ||
|
|
2059814394 | ||
|
|
063baa07ee | ||
|
|
24ffb02869 | ||
|
|
370bd71154 | ||
|
|
cb3eb4b09b | ||
|
|
c39e2c137b | ||
|
|
eedaf1c71f | ||
|
|
70e4a84db3 | ||
|
|
443b92dfd3 | ||
|
|
e1be3218b3 | ||
|
|
dfd4ef9347 | ||
|
|
094a22f5e7 | ||
|
|
7333100b1d | ||
|
|
e5d885e2a8 | ||
|
|
592819e028 | ||
|
|
f822ed2e2d | ||
|
|
5726875c59 | ||
|
|
52ee85f17c | ||
|
|
cf5b5f8068 | ||
|
|
ee6b301791 | ||
|
|
b9362f7eca | ||
|
|
36c738bfaa | ||
|
|
a1f955abbe | ||
|
|
9ab4d9e339 | ||
|
|
f51f66697f | ||
|
|
a3e195925a | ||
|
|
9d78070c45 | ||
|
|
4ae31159dc | ||
|
|
3fec5fab41 | ||
|
|
b12e2427a3 | ||
|
|
967d41b8ef | ||
|
|
4776b1857b | ||
|
|
01687ae8e8 | ||
|
|
3089da7282 | ||
|
|
24ae0fe21f | ||
|
|
ad3da6f2c6 | ||
|
|
5057e9a844 | ||
|
|
fea590bf9a | ||
|
|
058bdfef50 | ||
|
|
257a7eccd7 | ||
|
|
443609f5ba | ||
|
|
fde17edf09 | ||
|
|
084c9df65c | ||
|
|
4f9524a4c1 | ||
|
|
1495fb7ee6 | ||
|
|
fa312eec87 | ||
|
|
eb3cf2b77f | ||
|
|
c08cd84bed | ||
|
|
905314cfb5 | ||
|
|
bd3debf025 | ||
|
|
3a3c4b7995 | ||
|
|
f465117435 | ||
|
|
0777b71578 | ||
|
|
a2afdc0295 | ||
|
|
3b6bef3ff0 | ||
|
|
a963171b16 | ||
|
|
e1787f68d5 | ||
|
|
a8697b0187 |
+4
-2
@@ -105,6 +105,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -130,7 +131,7 @@ dmypy.json
|
|||||||
# logging
|
# logging
|
||||||
nextcord.log
|
nextcord.log
|
||||||
|
|
||||||
# data file for bot
|
# bot files
|
||||||
highscores_data.json
|
highscores_data.json
|
||||||
highscores_config.json
|
highscores_config.json
|
||||||
deleted_backups/
|
deleted_backups/
|
||||||
@@ -139,4 +140,5 @@ TODO.txt
|
|||||||
sql_tester.py
|
sql_tester.py
|
||||||
conf
|
conf
|
||||||
outputs
|
outputs
|
||||||
database
|
database
|
||||||
|
dakiny-messages/
|
||||||
+7
-14
@@ -1,26 +1,19 @@
|
|||||||
FROM python:3.10.6-slim
|
FROM python:3.11.9-slim
|
||||||
|
|
||||||
# install dependencies for mysqlclient
|
# install dependencies
|
||||||
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
|
RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
|
||||||
|
|
||||||
WORKDIR /home
|
WORKDIR /home
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
|
||||||
|
|
||||||
COPY config.py config.py
|
COPY config.py config.py
|
||||||
COPY dakiny_bot.py dakiny_bot.py
|
COPY main.py main.py
|
||||||
COPY utils.py utils.py
|
COPY utils.py utils.py
|
||||||
|
COPY pyproject.toml pyproject.toml
|
||||||
|
|
||||||
COPY bot_messages bot_messages/
|
|
||||||
COPY classes classes/
|
COPY classes classes/
|
||||||
COPY cogs cogs/
|
COPY cogs cogs/
|
||||||
COPY database_setup database_setup/
|
COPY database_setup database_setup/
|
||||||
|
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install --upgrade pip && pip install poetry && poetry export -f requirements.txt --output requirements.txt && pip install -r requirements.txt
|
||||||
|
|
||||||
# remove build deps that aren't needed to run
|
CMD ["python3", "-u", "./main.py"]
|
||||||
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
|
|
||||||
|
|
||||||
CMD ["python3", "-u", "./dakiny_bot.py"]
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# Dakiny Bot
|
# Dakiny Bot
|
||||||
|
|
||||||
Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined.
|
Dakiny clan's own discord bot!
|
||||||
|
|
||||||
|
|
||||||
|
### Assumptions
|
||||||
|
- For Elections: Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined.
|
||||||
Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already
|
Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already
|
||||||
exist in the database.
|
exist in the database.
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Here's the {quarter} Clan Council:
|
|
||||||
|
|
||||||
Dakiny Council:
|
|
||||||
{council}
|
|
||||||
|
|
||||||
Dakiny High Council:
|
|
||||||
{high_council}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
```fix
|
|
||||||
Dakiny's Clan Council election
|
|
||||||
```
|
|
||||||
Dakiny is an organization built by and for its members. We are a community of people who come together to support each other in the creation of this magical and unique experience. To sustain what we've created here, and to grow and involve into the future, we vote in our leadership every 3 months to provide an opportunity for new clan members to represent their faction of Dakiny in the Clan Council.
|
|
||||||
|
|
||||||
This clan is about human interaction, and the Clan Council positions serve to prop up certain voices within our community, in an effort to shape it as a force for good. Our mission here is to spread love. The more loving Council Members (CMs) we have on the team, the greater the community will grow for all of us to enjoy. There are no term limits, but if you go inactive on RS or become uninterested in pouring your effort into this community, request to step down so that others may contribute where you are no longer able to.
|
|
||||||
|
|
||||||
We want all CMs to grow and succeed. Please, reach out for assistance and advice if you find yourself going even a few days without taking real, actionable steps towards the prosperity of Dakiny. Recruiting, hosting events, participating in the Clan Council Discord, and demonstrating the Core Elements of the position are required from each and every CM to grow a happy and healthy clan.
|
|
||||||
|
|
||||||
__So here's how it works:__
|
|
||||||
---message_break---
|
|
||||||
Each quarter, a channel is created here on Discord for clan members to cast their vote to determine the Clan Council for that 3-month period.
|
|
||||||
|
|
||||||
**Only clan members can vote and all clan members are granted exactly one equally weighted vote.** If you vote twice for different people, your vote will be annulled.
|
|
||||||
|
|
||||||
All clan members are permitted to run for a seat on the Clan Council as long as they:
|
|
||||||
|
|
||||||
**i)** have been in the clan for at least 13 weeks (enough time to have participated in at least one election);
|
|
||||||
|
|
||||||
**ii)** have no warnings under their name in the last 6 months and
|
|
||||||
|
|
||||||
**iii)** have been active in the clan for the past 13 weeks prior to the first day of elections. To be considered active in that timeframe, one must fulfill the following criteria: hosted/participated in at least three clan events, each one in a different week;
|
|
||||||
|
|
||||||
**iv)** are not running under aliases that hide their true identity.
|
|
||||||
|
|
||||||
Candidates are permitted to vote for themselves. Once a candidate reaches 3 votes from clan members, they have earned their seat, meaning no more votes can be cast for them.
|
|
||||||
|
|
||||||
Then, the new Clan Council Elect will come together to select the 2 High Council positions:
|
|
||||||
RuneScape 3 Clan Leader
|
|
||||||
Discord Server Manager
|
|
||||||
|
|
||||||
The Clan Council will be denoted by the Discord ranks of <@&{clan_council_id}> and <@&{high_council_id}> and in-game with the following ranks.
|
|
||||||
<:Rank_Admin:479008185240518656> Admin
|
|
||||||
<:Rank_Organiser:479008215024009216> Organizer
|
|
||||||
<:Rank_Coordinator:479008238654849045> Coordinator
|
|
||||||
<:Rank_Overseer:479008269382451201> Overseer
|
|
||||||
<:Rank_Deputy:479008295319896064> Deputy Owner
|
|
||||||
<:Rank_Owner:479008325137072155> Clan Leader
|
|
||||||
---message_break---
|
|
||||||
__Here's the timeline:__
|
|
||||||
Election begins: {election_start}
|
|
||||||
Term: {term_start} - {term_end}
|
|
||||||
|
|
||||||
**{election_start}**: Post here to announce candidacy
|
|
||||||
**{voting_start}**: Voting begins
|
|
||||||
**{voting_end}**: Votes are tallied and winners are announced
|
|
||||||
**{highcouncil_election}**: The new Clan Council votes who among them shall receive the two High Council positions of: Clan Leader and Discord Server Manager.
|
|
||||||
**{term_start}**: The new Clan Council is officially inaugurated and a drop party (or a giveaway event) begins to commemorate the new leadership.
|
|
||||||
|
|
||||||
This process will begin again next quarter on {next_election_start} with the inauguration scheduled for {term_end}.
|
|
||||||
---message_break---
|
|
||||||
**You may now announce your candidacy for a seat on the Dakiny Clan Council!
|
|
||||||
**
|
|
||||||
You may include a speech with your announcement, as well as specific pertinent facts such as total recruits, time in the clan, past Clan Council experience, and donations made. This data is available upon request via <@&{clan_council_id}>.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
`Leadership Elections - candidacy time`
|
|
||||||
|
|
||||||
|
|
||||||
<@&{clan_council_elect_role_id}>, welcome. We are starting the second part of our elections. On {highcouncil_election_announce} this channel will be open to write for all of you. Everyone else will be able to read this channel, but not write in it. Only members of the Clan Council Elected (CCE) can vote now. Any votes from others will be annulled.
|
|
||||||
|
|
||||||
From {highcouncil_election_announce} to {highcouncil_election_start}, you will be able to announce your candidacy for High Council. The positions are:
|
|
||||||
Clan Leader
|
|
||||||
Discord Server Manager
|
|
||||||
|
|
||||||
Each member of the CCE can vote once in each category (Discord Server Manager and Clan Leader). Alternatively, you may choose to intentionally abstain.
|
|
||||||
|
|
||||||
Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<@&{clan_council_elect_role_id}> it is now time to vote for the high council! You have until {term_start} to complete the vote.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
====================
|
|
||||||
<@&{clan_member_id}>, it's time to vote for clan council.
|
|
||||||
|
|
||||||
Press the Vote button to vote!
|
|
||||||
Voting ends next {voting_end_day}, {voting_end}.
|
|
||||||
Results will then be announced.
|
|
||||||
|
|
||||||
After a candidate receives 3 votes, their spot is secured in the new council, but you can still vote for them to show your support.
|
|
||||||
|
|
||||||
**Just a reminder:** only the people listed below are eligible to vote, and each clan member may **only vote for one person**. Votes from guests, diplomats, or allies will be annulled.
|
|
||||||
Votes for multiple candidates from the same person will not be counted.
|
|
||||||
|
|
||||||
RS3 Clan Members
|
|
||||||
OSRS Clan Members
|
|
||||||
+53
-26
@@ -6,7 +6,7 @@ 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, true, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from classes.nominate_highcouncil_views import NominateHighCouncilView
|
from classes.nominate_highcouncil_views import NominateHighCouncilView
|
||||||
@@ -21,12 +21,13 @@ from database_setup.sql_data_classes import (
|
|||||||
engine,
|
engine,
|
||||||
)
|
)
|
||||||
from utils import (
|
from utils import (
|
||||||
|
TEMPLATES_DIR,
|
||||||
|
VoteType,
|
||||||
build_highcouncil_votes_str,
|
build_highcouncil_votes_str,
|
||||||
build_votes_str,
|
build_votes_str,
|
||||||
get_cl_election_name,
|
edit_clan_council_info,
|
||||||
get_datetime,
|
get_datetime,
|
||||||
get_dm_election_name,
|
get_election_name,
|
||||||
get_election_cycle_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ class ElectionCycle:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
|
f"{TEMPLATES_DIR}/election_announcement.txt", mode="r", encoding="utf8"
|
||||||
) as file:
|
) as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ class ElectionCycle:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
|
f"{TEMPLATES_DIR}/highcouncil_election.txt", mode="r", encoding="utf8"
|
||||||
) as file:
|
) as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ class ElectionCycle:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
|
f"{TEMPLATES_DIR}/highcouncil_vote.txt", mode="r", encoding="utf8"
|
||||||
) as file:
|
) as file:
|
||||||
message = file.read()
|
message = file.read()
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ class ElectionCycle:
|
|||||||
str: Message to send
|
str: Message to send
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file:
|
with open(f"{TEMPLATES_DIR}/vote_start.txt", mode="r", encoding="utf8") as file:
|
||||||
message = file.read()
|
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
|
||||||
@@ -197,7 +198,7 @@ class ElectionCycle:
|
|||||||
parent: nextcord.TextChannel = self.guild.get_channel(
|
parent: nextcord.TextChannel = self.guild.get_channel(
|
||||||
self.config.params.voting_parent_id
|
self.config.params.voting_parent_id
|
||||||
)
|
)
|
||||||
quarter_name: str = get_election_cycle_name(self.config)
|
quarter_name: str = get_election_name(self.config)
|
||||||
|
|
||||||
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
|
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
|
||||||
|
|
||||||
@@ -245,8 +246,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_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
|
||||||
@@ -275,7 +275,7 @@ class ElectionCycle:
|
|||||||
message, view=NominateHighCouncilView(self.config, self.guild)
|
message, view=NominateHighCouncilView(self.config, self.guild)
|
||||||
)
|
)
|
||||||
|
|
||||||
election_name = get_election_cycle_name(self.config)
|
election_name = get_election_name(self.config)
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# remove vote button from vote message
|
# remove vote button from vote message
|
||||||
@@ -293,7 +293,11 @@ class ElectionCycle:
|
|||||||
session.commit()
|
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
|
||||||
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name)
|
stmt = (
|
||||||
|
select(Votes.VotedForId)
|
||||||
|
.where(Votes.ElectionCycle == election_name)
|
||||||
|
.where(Votes.Type == VoteType.COUNCIL)
|
||||||
|
)
|
||||||
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():
|
||||||
@@ -331,8 +335,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_name(self.config)
|
||||||
== get_election_cycle_name(self.config)
|
|
||||||
)
|
)
|
||||||
info = session.scalar(stmt)
|
info = session.scalar(stmt)
|
||||||
info.HighCouncilVoteMsgId = vote_msg.id
|
info.HighCouncilVoteMsgId = vote_msg.id
|
||||||
@@ -358,13 +361,13 @@ class ElectionCycle:
|
|||||||
self.config.params.clan_council_role_id
|
self.config.params.clan_council_role_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
election_cycle = get_election_name(self.config)
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# role management
|
# role management
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(CouncilMembers.MemberId)
|
||||||
.where(
|
.where(CouncilMembers.ElectionCycle == election_cycle)
|
||||||
CouncilMembers.ElectionCycle == get_election_cycle_name(self.config)
|
|
||||||
)
|
|
||||||
.where(CouncilMembers.Elected == true())
|
.where(CouncilMembers.Elected == true())
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -381,35 +384,59 @@ class ElectionCycle:
|
|||||||
|
|
||||||
# announce high council members
|
# announce high council members
|
||||||
# ties are undefined behavior.
|
# ties are undefined behavior.
|
||||||
dm_election = get_dm_election_name(self.config)
|
stmt = (
|
||||||
cl_election = get_cl_election_name(self.config)
|
select(Votes.VotedForId)
|
||||||
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == dm_election)
|
.where(Votes.ElectionCycle == election_cycle)
|
||||||
|
.where(Votes.Type == VoteType.DISCORDMANAGER)
|
||||||
|
)
|
||||||
dm_votes = collections.Counter(session.scalars(stmt))
|
dm_votes = collections.Counter(session.scalars(stmt))
|
||||||
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election)
|
stmt = (
|
||||||
|
select(Votes.VotedForId)
|
||||||
|
.where(Votes.ElectionCycle == election_cycle)
|
||||||
|
.where(Votes.Type == VoteType.CLANLEADER)
|
||||||
|
)
|
||||||
cl_votes = collections.Counter(session.scalars(stmt))
|
cl_votes = collections.Counter(session.scalars(stmt))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dm_winner = max(dm_votes, key=dm_votes.get)
|
dm_winner = max(dm_votes, key=dm_votes.get)
|
||||||
|
stmt = (
|
||||||
|
update(CouncilMembers)
|
||||||
|
.where(CouncilMembers.ElectionCycle == election_cycle)
|
||||||
|
.where(CouncilMembers.MemberId == dm_winner)
|
||||||
|
.values(DiscordManager=True)
|
||||||
|
)
|
||||||
|
session.execute(stmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
dm_winner = "No winner"
|
dm_winner = "No winner"
|
||||||
try:
|
try:
|
||||||
cl_winner = max(cl_votes, key=cl_votes.get)
|
cl_winner = max(cl_votes, key=cl_votes.get)
|
||||||
|
stmt = (
|
||||||
|
update(CouncilMembers)
|
||||||
|
.where(CouncilMembers.ElectionCycle == election_cycle)
|
||||||
|
.where(CouncilMembers.MemberId == cl_winner)
|
||||||
|
.values(ClanLeader=True)
|
||||||
|
)
|
||||||
|
session.execute(stmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
cl_winner = "No winner"
|
cl_winner = "No winner"
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
election_thread: nextcord.Thread = self.guild.get_thread(
|
election_thread: nextcord.Thread = self.guild.get_thread(
|
||||||
self.get_election_info().ThreadId
|
self.get_election_info().ThreadId
|
||||||
)
|
)
|
||||||
|
|
||||||
# clear view from vote message
|
# clear view from vote message
|
||||||
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
||||||
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
|
ElectionInformation.ElectionCycle == get_election_name(self.config)
|
||||||
)
|
)
|
||||||
vote_msg_id = session.scalar(stmt)
|
vote_msg_id = session.scalar(stmt)
|
||||||
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
|
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
|
||||||
await vote_msg.edit(view=None)
|
await vote_msg.edit(view=None)
|
||||||
|
|
||||||
highcouncil_msg: str = f"Congratulations for our high council members!\n Clan Leader: <@{cl_winner}>\n"
|
highcouncil_msg: str = (
|
||||||
|
f"Congratulations for our high council members!\nClan Leader: <@{cl_winner}>\n"
|
||||||
|
)
|
||||||
highcouncil_msg += f"Discord Manager: <@{dm_winner}>"
|
highcouncil_msg += f"Discord Manager: <@{dm_winner}>"
|
||||||
await election_thread.send(highcouncil_msg)
|
await election_thread.send(highcouncil_msg)
|
||||||
|
|
||||||
@@ -418,6 +445,7 @@ class ElectionCycle:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await election_thread.edit(archived=True, locked=True)
|
await election_thread.edit(archived=True, locked=True)
|
||||||
|
await edit_clan_council_info(self.guild, self.config)
|
||||||
|
|
||||||
def get_election_info(self) -> ElectionInformation:
|
def get_election_info(self) -> ElectionInformation:
|
||||||
"""
|
"""
|
||||||
@@ -429,8 +457,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_name(self.config)
|
||||||
== get_election_cycle_name(self.config)
|
|
||||||
)
|
)
|
||||||
return session.scalar(stmt)
|
return session.scalar(stmt)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: gitea_webhook.json
|
||||||
|
# timestamp: 2024-05-16T21:15:27+00:00
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Author(BaseModel):
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class Committer(BaseModel):
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class Commit(BaseModel):
|
||||||
|
id: str
|
||||||
|
message: str
|
||||||
|
url: str
|
||||||
|
author: Author
|
||||||
|
committer: Committer
|
||||||
|
verification: None
|
||||||
|
timestamp: str
|
||||||
|
added: List[str]
|
||||||
|
removed: List[str]
|
||||||
|
modified: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class HeadCommit(BaseModel):
|
||||||
|
id: str
|
||||||
|
message: str
|
||||||
|
url: str
|
||||||
|
author: Author
|
||||||
|
committer: Committer
|
||||||
|
verification: None
|
||||||
|
timestamp: str
|
||||||
|
added: List[str]
|
||||||
|
removed: List[str]
|
||||||
|
modified: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Owner(BaseModel):
|
||||||
|
id: int
|
||||||
|
login: str
|
||||||
|
login_name: str
|
||||||
|
full_name: str
|
||||||
|
email: str
|
||||||
|
avatar_url: str
|
||||||
|
language: str
|
||||||
|
is_admin: bool
|
||||||
|
last_login: str
|
||||||
|
created: str
|
||||||
|
restricted: bool
|
||||||
|
active: bool
|
||||||
|
prohibit_login: bool
|
||||||
|
location: str
|
||||||
|
website: str
|
||||||
|
description: str
|
||||||
|
visibility: str
|
||||||
|
followers_count: int
|
||||||
|
following_count: int
|
||||||
|
starred_repos_count: int
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class Permissions(BaseModel):
|
||||||
|
admin: bool
|
||||||
|
push: bool
|
||||||
|
pull: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Repository(BaseModel):
|
||||||
|
id: int
|
||||||
|
owner: Owner
|
||||||
|
name: str
|
||||||
|
full_name: str
|
||||||
|
description: str
|
||||||
|
empty: bool
|
||||||
|
private: bool
|
||||||
|
fork: bool
|
||||||
|
template: bool
|
||||||
|
parent: None
|
||||||
|
mirror: bool
|
||||||
|
size: int
|
||||||
|
language: str
|
||||||
|
languages_url: str
|
||||||
|
html_url: str
|
||||||
|
url: str
|
||||||
|
link: str
|
||||||
|
ssh_url: str
|
||||||
|
clone_url: str
|
||||||
|
original_url: str
|
||||||
|
website: str
|
||||||
|
stars_count: int
|
||||||
|
forks_count: int
|
||||||
|
watchers_count: int
|
||||||
|
open_issues_count: int
|
||||||
|
open_pr_counter: int
|
||||||
|
release_counter: int
|
||||||
|
default_branch: str
|
||||||
|
archived: bool
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
archived_at: str
|
||||||
|
permissions: Permissions
|
||||||
|
has_issues: bool
|
||||||
|
has_wiki: bool
|
||||||
|
has_pull_requests: bool
|
||||||
|
has_projects: bool
|
||||||
|
has_releases: bool
|
||||||
|
has_packages: bool
|
||||||
|
has_actions: bool
|
||||||
|
ignore_whitespace_conflicts: bool
|
||||||
|
allow_merge_commits: bool
|
||||||
|
allow_rebase: bool
|
||||||
|
allow_rebase_explicit: bool
|
||||||
|
allow_squash_merge: bool
|
||||||
|
allow_rebase_update: bool
|
||||||
|
default_delete_branch_after_merge: bool
|
||||||
|
default_merge_style: str
|
||||||
|
default_allow_maintainer_edit: bool
|
||||||
|
avatar_url: str
|
||||||
|
internal: bool
|
||||||
|
mirror_interval: str
|
||||||
|
mirror_updated: str
|
||||||
|
repo_transfer: None
|
||||||
|
|
||||||
|
|
||||||
|
class Pusher(BaseModel):
|
||||||
|
id: int
|
||||||
|
login: str
|
||||||
|
login_name: str
|
||||||
|
full_name: str
|
||||||
|
email: str
|
||||||
|
avatar_url: str
|
||||||
|
language: str
|
||||||
|
is_admin: bool
|
||||||
|
last_login: str
|
||||||
|
created: str
|
||||||
|
restricted: bool
|
||||||
|
active: bool
|
||||||
|
prohibit_login: bool
|
||||||
|
location: str
|
||||||
|
website: str
|
||||||
|
description: str
|
||||||
|
visibility: str
|
||||||
|
followers_count: int
|
||||||
|
following_count: int
|
||||||
|
starred_repos_count: int
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class Sender(BaseModel):
|
||||||
|
id: int
|
||||||
|
login: str
|
||||||
|
login_name: str
|
||||||
|
full_name: str
|
||||||
|
email: str
|
||||||
|
avatar_url: str
|
||||||
|
language: str
|
||||||
|
is_admin: bool
|
||||||
|
last_login: str
|
||||||
|
created: str
|
||||||
|
restricted: bool
|
||||||
|
active: bool
|
||||||
|
prohibit_login: bool
|
||||||
|
location: str
|
||||||
|
website: str
|
||||||
|
description: str
|
||||||
|
visibility: str
|
||||||
|
followers_count: int
|
||||||
|
following_count: int
|
||||||
|
starred_repos_count: int
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaWebhook(BaseModel):
|
||||||
|
ref: str
|
||||||
|
before: str
|
||||||
|
after: str
|
||||||
|
compare_url: str
|
||||||
|
commits: List[Commit]
|
||||||
|
total_commits: int
|
||||||
|
head_commit: HeadCommit
|
||||||
|
repository: Repository
|
||||||
|
pusher: Pusher
|
||||||
|
sender: Sender
|
||||||
@@ -13,7 +13,7 @@ from utils import (
|
|||||||
NOMINEE_SPLITTER,
|
NOMINEE_SPLITTER,
|
||||||
decorate_cl_nomination,
|
decorate_cl_nomination,
|
||||||
decorate_dm_nomination,
|
decorate_dm_nomination,
|
||||||
get_election_cycle_name,
|
get_election_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class NominateDMModal(nextcord.ui.Modal):
|
|||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# user already in table
|
# user already in table
|
||||||
cycle = get_election_cycle_name(self.config)
|
cycle = get_election_name(self.config)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMembers)
|
||||||
.where(CouncilMembers.ElectionCycle == cycle)
|
.where(CouncilMembers.ElectionCycle == cycle)
|
||||||
@@ -127,7 +127,7 @@ class NominateCLModal(nextcord.ui.Modal):
|
|||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# user already in table
|
# user already in table
|
||||||
cycle = get_election_cycle_name(self.config)
|
cycle = get_election_name(self.config)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMembers)
|
||||||
.where(CouncilMembers.ElectionCycle == cycle)
|
.where(CouncilMembers.ElectionCycle == cycle)
|
||||||
@@ -189,7 +189,7 @@ class NominateHighCouncilView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cycle = get_election_cycle_name(self.config)
|
cycle = get_election_name(self.config)
|
||||||
usr_nominated = False
|
usr_nominated = False
|
||||||
speech_content = ""
|
speech_content = ""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
@@ -242,7 +242,7 @@ class NominateHighCouncilView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cycle = get_election_cycle_name(self.config)
|
cycle = get_election_name(self.config)
|
||||||
usr_nominated = False
|
usr_nominated = False
|
||||||
speech_content = ""
|
speech_content = ""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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 NOMINEE_SPLITTER, decorate_nomination, get_election_name
|
||||||
|
|
||||||
|
|
||||||
class NominateModal(nextcord.ui.Modal):
|
class NominateModal(nextcord.ui.Modal):
|
||||||
@@ -58,7 +58,7 @@ class NominateModal(nextcord.ui.Modal):
|
|||||||
msg = await interaction.channel.send(speech)
|
msg = await interaction.channel.send(speech)
|
||||||
|
|
||||||
council_member = CouncilMembers(
|
council_member = CouncilMembers(
|
||||||
ElectionCycle=get_election_cycle_name(self.config),
|
ElectionCycle=get_election_name(self.config),
|
||||||
MemberId=interaction.user.id,
|
MemberId=interaction.user.id,
|
||||||
SpeechMsgId=msg.id,
|
SpeechMsgId=msg.id,
|
||||||
)
|
)
|
||||||
@@ -66,7 +66,7 @@ class NominateModal(nextcord.ui.Modal):
|
|||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
# user already nominated, just need to update the election speech
|
# user already nominated, just need to update the election speech
|
||||||
cycle = get_election_cycle_name(self.config)
|
cycle = get_election_name(self.config)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMembers)
|
||||||
.where(CouncilMembers.ElectionCycle == cycle)
|
.where(CouncilMembers.ElectionCycle == cycle)
|
||||||
@@ -120,7 +120,7 @@ class NominateView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cycle = get_election_cycle_name(self.config)
|
cycle = get_election_name(self.config)
|
||||||
usr_nominated = False
|
usr_nominated = False
|
||||||
speech_content = ""
|
speech_content = ""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
|||||||
@@ -17,12 +17,7 @@ from database_setup.sql_data_classes import (
|
|||||||
Votes,
|
Votes,
|
||||||
engine,
|
engine,
|
||||||
)
|
)
|
||||||
from utils import (
|
from utils import VoteType, build_highcouncil_votes_str, get_election_name
|
||||||
build_highcouncil_votes_str,
|
|
||||||
get_cl_election_name,
|
|
||||||
get_dm_election_name,
|
|
||||||
get_election_cycle_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VoteSelect(nextcord.ui.StringSelect):
|
class VoteSelect(nextcord.ui.StringSelect):
|
||||||
@@ -39,20 +34,10 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
self.election_type = election_type
|
self.election_type = election_type
|
||||||
|
|
||||||
async def callback(self, interaction: Interaction):
|
async def callback(self, interaction: Interaction):
|
||||||
# decide on election type
|
election_name: str = get_election_name(self.config)
|
||||||
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:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
||||||
ElectionInformation.ElectionCycle == cycle
|
ElectionInformation.ElectionCycle == election_name
|
||||||
)
|
)
|
||||||
vote_msg_id = session.scalar(stmt)
|
vote_msg_id = session.scalar(stmt)
|
||||||
|
|
||||||
@@ -65,6 +50,7 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
ElectionCycle=election_name,
|
ElectionCycle=election_name,
|
||||||
VoterId=interaction.user.id,
|
VoterId=interaction.user.id,
|
||||||
VotedForId=voted_for,
|
VotedForId=voted_for,
|
||||||
|
Type=self.election_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -72,11 +58,13 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
except exc.IntegrityError:
|
except exc.IntegrityError:
|
||||||
|
# user already has a vote in the election and election type
|
||||||
session.rollback()
|
session.rollback()
|
||||||
stmt = (
|
stmt = (
|
||||||
update(Votes)
|
update(Votes)
|
||||||
.where(Votes.ElectionCycle == election_name)
|
.where(Votes.ElectionCycle == election_name)
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(Votes.VoterId == interaction.user.id)
|
||||||
|
.where(Votes.Type == self.election_type)
|
||||||
.values(VotedForId=voted_for)
|
.values(VotedForId=voted_for)
|
||||||
)
|
)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
@@ -135,28 +123,30 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cycle: str = get_election_cycle_name(self.config)
|
election_name: str = get_election_name(self.config)
|
||||||
election: str = get_cl_election_name(self.config)
|
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(Votes.VotedForId)
|
||||||
.where(Votes.ElectionCycle == election)
|
.where(Votes.ElectionCycle == election_name)
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(Votes.VoterId == interaction.user.id)
|
||||||
|
.where(Votes.Type == VoteType.CLANLEADER)
|
||||||
)
|
)
|
||||||
voted_for = session.scalar(stmt)
|
voted_for = session.scalar(stmt)
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(CouncilMembers.MemberId)
|
||||||
.where(CouncilMembers.ElectionCycle == cycle)
|
.where(CouncilMembers.ElectionCycle == election_name)
|
||||||
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
||||||
)
|
)
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOption(
|
SelectOption(
|
||||||
label=f"{self.guild.get_member(nominee).display_name} ☑️"
|
label=(
|
||||||
if voted_for == nominee
|
f"{self.guild.get_member(nominee).display_name} ☑️"
|
||||||
else f"{self.guild.get_member(nominee).display_name}",
|
if voted_for == nominee
|
||||||
|
else f"{self.guild.get_member(nominee).display_name}"
|
||||||
|
),
|
||||||
value=str(nominee),
|
value=str(nominee),
|
||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
@@ -173,7 +163,9 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
|
|
||||||
view = nextcord.ui.View()
|
view = nextcord.ui.View()
|
||||||
view.add_item(
|
view.add_item(
|
||||||
VoteSelect(options=options, config=self.config, election_type="cl")
|
VoteSelect(
|
||||||
|
options=options, config=self.config, election_type=VoteType.CLANLEADER
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
@@ -211,28 +203,30 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cycle: str = get_election_cycle_name(self.config)
|
election_name: str = get_election_name(self.config)
|
||||||
election: str = get_dm_election_name(self.config)
|
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(Votes.VotedForId)
|
||||||
.where(Votes.ElectionCycle == election)
|
.where(Votes.ElectionCycle == election_name)
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(Votes.VoterId == interaction.user.id)
|
||||||
|
.where(Votes.Type == VoteType.DISCORDMANAGER)
|
||||||
)
|
)
|
||||||
voted_for = session.scalar(stmt)
|
voted_for = session.scalar(stmt)
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers.MemberId)
|
select(CouncilMembers.MemberId)
|
||||||
.where(CouncilMembers.ElectionCycle == cycle)
|
.where(CouncilMembers.ElectionCycle == election_name)
|
||||||
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
||||||
)
|
)
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOption(
|
SelectOption(
|
||||||
label=f"{self.guild.get_member(nominee).display_name} ☑️"
|
label=(
|
||||||
if voted_for == nominee
|
f"{self.guild.get_member(nominee).display_name} ☑️"
|
||||||
else f"{self.guild.get_member(nominee).display_name}",
|
if voted_for == nominee
|
||||||
|
else f"{self.guild.get_member(nominee).display_name}"
|
||||||
|
),
|
||||||
value=str(nominee),
|
value=str(nominee),
|
||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
@@ -249,7 +243,11 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
|
|
||||||
view = nextcord.ui.View()
|
view = nextcord.ui.View()
|
||||||
view.add_item(
|
view.add_item(
|
||||||
VoteSelect(options=options, config=self.config, election_type="dm")
|
VoteSelect(
|
||||||
|
options=options,
|
||||||
|
config=self.config,
|
||||||
|
election_type=VoteType.DISCORDMANAGER,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
|
|||||||
+11
-6
@@ -17,7 +17,7 @@ from database_setup.sql_data_classes import (
|
|||||||
Votes,
|
Votes,
|
||||||
engine,
|
engine,
|
||||||
)
|
)
|
||||||
from utils import build_votes_str, get_election_cycle_name
|
from utils import VoteType, build_votes_str, get_election_name
|
||||||
|
|
||||||
|
|
||||||
class VoteSelect(nextcord.ui.StringSelect):
|
class VoteSelect(nextcord.ui.StringSelect):
|
||||||
@@ -31,7 +31,7 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
async def callback(self, interaction: Interaction):
|
async def callback(self, interaction: Interaction):
|
||||||
election_name = get_election_cycle_name(self.config)
|
election_name = get_election_name(self.config)
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
|
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
|
||||||
ElectionInformation.ElectionCycle == election_name
|
ElectionInformation.ElectionCycle == election_name
|
||||||
@@ -47,6 +47,7 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
ElectionCycle=election_name,
|
ElectionCycle=election_name,
|
||||||
VoterId=interaction.user.id,
|
VoterId=interaction.user.id,
|
||||||
VotedForId=voted_for,
|
VotedForId=voted_for,
|
||||||
|
Type=VoteType.COUNCIL,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -59,6 +60,7 @@ class VoteSelect(nextcord.ui.StringSelect):
|
|||||||
update(Votes)
|
update(Votes)
|
||||||
.where(Votes.ElectionCycle == election_name)
|
.where(Votes.ElectionCycle == election_name)
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(Votes.VoterId == interaction.user.id)
|
||||||
|
.where(Votes.Type == VoteType.COUNCIL)
|
||||||
.values(VotedForId=voted_for)
|
.values(VotedForId=voted_for)
|
||||||
)
|
)
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
@@ -114,13 +116,14 @@ class VoteView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
election: str = get_election_cycle_name(self.config)
|
election: str = get_election_name(self.config)
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Votes.VotedForId)
|
select(Votes.VotedForId)
|
||||||
.where(Votes.ElectionCycle == election)
|
.where(Votes.ElectionCycle == election)
|
||||||
.where(Votes.VoterId == interaction.user.id)
|
.where(Votes.VoterId == interaction.user.id)
|
||||||
|
.where(Votes.Type == VoteType.COUNCIL)
|
||||||
)
|
)
|
||||||
voted_for = session.scalar(stmt)
|
voted_for = session.scalar(stmt)
|
||||||
|
|
||||||
@@ -130,9 +133,11 @@ class VoteView(nextcord.ui.View):
|
|||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOption(
|
SelectOption(
|
||||||
label=f"{self.guild.get_member(nominee).display_name} ☑️"
|
label=(
|
||||||
if voted_for == nominee
|
f"{self.guild.get_member(nominee).display_name} ☑️"
|
||||||
else f"{self.guild.get_member(nominee).display_name}",
|
if voted_for == nominee
|
||||||
|
else f"{self.guild.get_member(nominee).display_name}"
|
||||||
|
),
|
||||||
value=str(nominee),
|
value=str(nominee),
|
||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"""
|
||||||
|
File contains logic for creating a webhook listener, pulling from git, and updating all messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import nextcord
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from git import Repo
|
||||||
|
from nextcord.ext import commands
|
||||||
|
|
||||||
|
from classes.gitea_webhook import GiteaWebhook
|
||||||
|
from config import DakinyConfig
|
||||||
|
from utils import REPO_DIR, edit_clan_council_info, update_clan_messages
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
REPO_URL = "https://gitea.cheezers.org/imcompiling/dakiny-messages.git"
|
||||||
|
|
||||||
|
|
||||||
|
class ClanMessages(commands.Cog, name="Clan Messages"):
|
||||||
|
"""
|
||||||
|
Cog to be loaded into the bot. Used to be able to have a fastapi post listener and access to the bot at
|
||||||
|
the same time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config: DakinyConfig = self.bot.config
|
||||||
|
self.repo_setup()
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
"""
|
||||||
|
Triggers when the bot is in ready state. Adds the webhook listener to FastAPI and serves.
|
||||||
|
"""
|
||||||
|
app.add_api_route("/messages/", self.update_messages, methods=["POST"])
|
||||||
|
config = uvicorn.Config(app=app, host="0.0.0.0", port=5000, log_level="info")
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
|
async def update_messages(self, hook_info: GiteaWebhook):
|
||||||
|
"""
|
||||||
|
Pulls from git and then calls method to update all the discord messages as described by the
|
||||||
|
files in the git repo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get all modified/added files
|
||||||
|
modified_files: list[str] = [
|
||||||
|
file
|
||||||
|
for list in [commit.modified + commit.added for commit in hook_info.commits]
|
||||||
|
for file in list
|
||||||
|
]
|
||||||
|
|
||||||
|
template_updates = {
|
||||||
|
file.split("/")[1] for file in modified_files if "message_templates" in file
|
||||||
|
}
|
||||||
|
channel_updates = {
|
||||||
|
file.split("/")[1]
|
||||||
|
for file in modified_files
|
||||||
|
if file.split("/")[1] not in template_updates
|
||||||
|
}
|
||||||
|
|
||||||
|
self.origin.pull()
|
||||||
|
guild: nextcord.Guild = self.bot.get_guild(self.config.params.guild_id)
|
||||||
|
|
||||||
|
await update_clan_messages(guild, channel_updates)
|
||||||
|
|
||||||
|
# if the council members template has changed, update the message
|
||||||
|
if "council_members.txt" in template_updates:
|
||||||
|
await edit_clan_council_info(
|
||||||
|
self.bot.get_guild(self.config.params.guild_id), self.config
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "Successfully updated all discord messages."}
|
||||||
|
|
||||||
|
def repo_setup(self):
|
||||||
|
"""
|
||||||
|
Checks if the repo exists and creates repo object, clones if it does not.
|
||||||
|
"""
|
||||||
|
# check if repo has been cloned. open repo object
|
||||||
|
if os.path.isdir(REPO_DIR):
|
||||||
|
self.repo = Repo(REPO_DIR)
|
||||||
|
else:
|
||||||
|
self.repo = Repo.clone_from(REPO_URL, REPO_DIR)
|
||||||
|
|
||||||
|
self.origin = self.repo.remote("origin")
|
||||||
|
|
||||||
|
if self.config.params.git_branch == "test":
|
||||||
|
self.repo.create_head("test", self.origin.refs.test).set_tracking_branch(
|
||||||
|
self.origin.refs.test
|
||||||
|
).checkout()
|
||||||
|
else:
|
||||||
|
self.repo.create_head("main", self.origin.refs.main).set_tracking_branch(
|
||||||
|
self.origin.refs.main
|
||||||
|
).checkout()
|
||||||
|
|
||||||
|
self.origin.pull()
|
||||||
|
|
||||||
|
|
||||||
|
# This function will be called when this extension is loaded.
|
||||||
|
# It is necessary to add these functions to the bot.
|
||||||
|
def setup(bot: commands.Bot):
|
||||||
|
"""Adds function to bot"""
|
||||||
|
bot.add_cog(ClanMessages(bot))
|
||||||
@@ -4,6 +4,8 @@ Cog loader. Adds all command cogs to the bot.
|
|||||||
|
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
|
from cogs.commands.record import RecordCog
|
||||||
|
from cogs.commands.refresh import RefreshCog
|
||||||
from cogs.commands.remove import RemoveCog
|
from cogs.commands.remove import RemoveCog
|
||||||
from cogs.commands.request import RequestCog
|
from cogs.commands.request import RequestCog
|
||||||
from cogs.commands.set import setCog
|
from cogs.commands.set import setCog
|
||||||
@@ -18,6 +20,8 @@ def setup(bot: commands.Bot):
|
|||||||
Adds all command classes to bot.
|
Adds all command classes to bot.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
bot.add_cog(RecordCog(bot))
|
||||||
|
bot.add_cog(RefreshCog(bot))
|
||||||
bot.add_cog(RemoveCog())
|
bot.add_cog(RemoveCog())
|
||||||
bot.add_cog(RequestCog(bot))
|
bot.add_cog(RequestCog(bot))
|
||||||
bot.add_cog(TestCog(bot))
|
bot.add_cog(TestCog(bot))
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
"""
|
||||||
|
Contained commands and views for record commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import nextcord
|
||||||
|
from discord import TextInputStyle
|
||||||
|
from nextcord.ext import commands
|
||||||
|
|
||||||
|
from config import DakinyConfig
|
||||||
|
from utils import record_event_points
|
||||||
|
|
||||||
|
|
||||||
|
class RecordCog(commands.Cog, name="Record Commands"):
|
||||||
|
"""
|
||||||
|
Class holds record commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot) -> None:
|
||||||
|
"""
|
||||||
|
Default constructor for RecordCog class. Does nothing.
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.bot: commands.Bot = bot
|
||||||
|
|
||||||
|
@nextcord.slash_command()
|
||||||
|
async def record(self, interaction: nextcord.Interaction):
|
||||||
|
"""
|
||||||
|
Parent slash command for record. Never called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@record.subcommand(description="Record an event and its participants.")
|
||||||
|
async def event(
|
||||||
|
self,
|
||||||
|
interaction: nextcord.Interaction,
|
||||||
|
name: str = nextcord.SlashOption(
|
||||||
|
required=True, description="Name of the event that was hosted."
|
||||||
|
),
|
||||||
|
host: nextcord.Member = nextcord.SlashOption(
|
||||||
|
required=False,
|
||||||
|
description="An alternative host if you are recording event for someone else.",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This command creates the base event recording embed and sends a message with it and the EventView
|
||||||
|
for gathering more information on the event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): interaction with this command.
|
||||||
|
name (str): Name of the event.
|
||||||
|
host (nextcord.Member, optional): Alternative host to user using command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# check role
|
||||||
|
if (
|
||||||
|
interaction.guild.get_role(self.bot.config.params.event_host_role_id)
|
||||||
|
not in interaction.user.roles
|
||||||
|
):
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"You must have the <@&{self.bot.config.params.event_host_role_id}> role to record an event.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if host is None:
|
||||||
|
host = interaction.user
|
||||||
|
|
||||||
|
embed: nextcord.Embed = nextcord.Embed(title=name, description="")
|
||||||
|
embed.add_field(name="Participants", value="")
|
||||||
|
embed.add_field(name="Non-Discord Participants", value="")
|
||||||
|
embed.add_field(name="Hosted by", value=f"<@{host.id}>", inline=False)
|
||||||
|
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Fill out event details and select all members who participated!",
|
||||||
|
view=EventView(name, self.bot.config, host.id),
|
||||||
|
ephemeral=True,
|
||||||
|
embed=embed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EventView(nextcord.ui.View):
|
||||||
|
"""
|
||||||
|
Class handles gathering information on an event. Has buttons to edit the information, cancel, and submit.
|
||||||
|
Also contains a UserSelect object to pick discord members.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, config: DakinyConfig, host_id: int):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.name: str = name
|
||||||
|
self.config: DakinyConfig = config
|
||||||
|
self.host_id: int = host_id
|
||||||
|
|
||||||
|
self.event_participants = ParticipantSelect()
|
||||||
|
|
||||||
|
self.add_item(self.event_participants)
|
||||||
|
|
||||||
|
@nextcord.ui.button(label="Cancel", style=nextcord.ButtonStyle.red, row=1)
|
||||||
|
async def cancel(
|
||||||
|
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Cancels event submission. Removes embed and view.
|
||||||
|
"""
|
||||||
|
await interaction.response.edit_message(
|
||||||
|
"Canceled", ephemeral=True, embed=None, view=None
|
||||||
|
)
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
@nextcord.ui.button(label="Edit", style=nextcord.ButtonStyle.blurple, row=1)
|
||||||
|
async def edit(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
|
||||||
|
"""
|
||||||
|
Calls modal to update the text input information of the event.
|
||||||
|
"""
|
||||||
|
await interaction.response.send_modal(
|
||||||
|
ParticipantModal(interaction.message.embeds[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
@nextcord.ui.button(label="Confirm", style=nextcord.ButtonStyle.green, row=1)
|
||||||
|
async def confirm(
|
||||||
|
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Submits the event embed to the event channel. Removes view and embed from recording message.
|
||||||
|
Sends point information to database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
discord_users = self.event_participants.values.members
|
||||||
|
if discord_users:
|
||||||
|
discord_users_list: list[str] = [
|
||||||
|
str(user.id) for user in discord_users if user.id is not self.host_id
|
||||||
|
]
|
||||||
|
record_event_points(
|
||||||
|
self.config,
|
||||||
|
discord_users_list,
|
||||||
|
True,
|
||||||
|
self.config.params.event_participate_points,
|
||||||
|
)
|
||||||
|
|
||||||
|
non_discord_users = interaction.message.embeds[0].fields[1].value
|
||||||
|
if non_discord_users:
|
||||||
|
non_discord_users_list: list[str] = non_discord_users.split("\n")
|
||||||
|
record_event_points(
|
||||||
|
self.config,
|
||||||
|
non_discord_users_list,
|
||||||
|
False,
|
||||||
|
self.config.params.event_participate_points,
|
||||||
|
)
|
||||||
|
|
||||||
|
record_event_points(
|
||||||
|
self.config, [self.host_id], True, self.config.params.event_host_points
|
||||||
|
)
|
||||||
|
|
||||||
|
event_channel: nextcord.TextChannel = interaction.guild.get_channel(
|
||||||
|
self.config.params.event_post_channel_id
|
||||||
|
)
|
||||||
|
await event_channel.send(embed=interaction.message.embeds[0])
|
||||||
|
await interaction.response.edit_message(
|
||||||
|
content="Event has been submitted.", view=None, embed=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantModal(nextcord.ui.Modal):
|
||||||
|
"""
|
||||||
|
This modal contains all the text edits for an event. You can change the name, description, and add the usernames
|
||||||
|
of participants who are not in the discord.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO turn this embed into a dict
|
||||||
|
def __init__(self, embed):
|
||||||
|
super().__init__(
|
||||||
|
"Edit the event.",
|
||||||
|
timeout=5 * 60,
|
||||||
|
)
|
||||||
|
self.embed: nextcord.Embed = embed
|
||||||
|
self.name: nextcord.ui.TextInput = nextcord.ui.TextInput(
|
||||||
|
required=True,
|
||||||
|
label="Event Name",
|
||||||
|
default_value=self.embed.title,
|
||||||
|
)
|
||||||
|
self.description: nextcord.ui.TextInput = nextcord.ui.TextInput(
|
||||||
|
required=False,
|
||||||
|
label="Event Description",
|
||||||
|
default_value=self.embed.description,
|
||||||
|
style=TextInputStyle.paragraph,
|
||||||
|
placeholder="Add a description if desired.",
|
||||||
|
max_length=1000,
|
||||||
|
)
|
||||||
|
self.participants: nextcord.ui.TextInput = nextcord.ui.TextInput(
|
||||||
|
required=False,
|
||||||
|
label="Non-Discord Usernames",
|
||||||
|
style=TextInputStyle.paragraph,
|
||||||
|
placeholder="Please separate usernames with a new line.",
|
||||||
|
default_value=self.embed.fields[1].value,
|
||||||
|
)
|
||||||
|
self.add_item(self.name)
|
||||||
|
self.add_item(self.description)
|
||||||
|
self.add_item(self.participants)
|
||||||
|
|
||||||
|
async def callback(self, interaction: nextcord.Interaction):
|
||||||
|
self.embed.title = self.name.value
|
||||||
|
self.embed.description = self.description.value
|
||||||
|
self.embed.set_field_at(
|
||||||
|
index=1, name="Non-Discord Participants", value=self.participants.value
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.edit_message(embed=self.embed)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantSelect(nextcord.ui.UserSelect):
|
||||||
|
"""
|
||||||
|
A selection drop down for discord members. Updates the embed with all users selected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(
|
||||||
|
min_values=1,
|
||||||
|
max_values=25,
|
||||||
|
placeholder="Select all participants",
|
||||||
|
row=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def callback(self, interaction: nextcord.Interaction):
|
||||||
|
embed: nextcord.Embed = interaction.message.embeds[0]
|
||||||
|
participants: str = "\n".join(
|
||||||
|
[f"<@{participant.id}>" for participant in self.values.members]
|
||||||
|
)
|
||||||
|
embed.set_field_at(0, name="Participants", value=participants)
|
||||||
|
await interaction.response.edit_message(embed=embed)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
Contained commands and views for refresh commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import nextcord
|
||||||
|
from nextcord.ext import commands
|
||||||
|
|
||||||
|
from utils import MESSAGES_DIR, edit_clan_council_info, update_clan_messages
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshCog(commands.Cog, name="Refresh Commands"):
|
||||||
|
"""
|
||||||
|
Class holds refresh commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot) -> None:
|
||||||
|
"""
|
||||||
|
Default constructor for RefreshCog class. Does nothing.
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.bot: commands.Bot = bot
|
||||||
|
|
||||||
|
@nextcord.slash_command(
|
||||||
|
default_member_permissions=nextcord.Permissions(administrator=True)
|
||||||
|
)
|
||||||
|
async def refresh(self, interaction: nextcord.Interaction):
|
||||||
|
"""
|
||||||
|
Parent slash command for refresh. Never called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@refresh.subcommand(description="Force refresh all of the bot managed messages.")
|
||||||
|
async def messages(
|
||||||
|
self,
|
||||||
|
interaction: nextcord.Interaction,
|
||||||
|
channel: nextcord.TextChannel = nextcord.SlashOption(
|
||||||
|
description="Channel to refresh messages in.",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Calls update_clan_messages() to update all messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): Interaction with the command.
|
||||||
|
channel (nextcord.TextChannel): channel to refresh messages in.
|
||||||
|
"""
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
if channel is None:
|
||||||
|
channel_updates = [
|
||||||
|
dir_name
|
||||||
|
for dir_name in os.listdir(MESSAGES_DIR)
|
||||||
|
if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}")
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
channel_updates = [channel.name]
|
||||||
|
|
||||||
|
await update_clan_messages(interaction.guild, channel_updates)
|
||||||
|
await interaction.followup.send("Messages have been refreshed.", ephemeral=True)
|
||||||
|
|
||||||
|
@refresh.subcommand(description="Updates the current council info message.")
|
||||||
|
async def council_info(self, interaction: nextcord.Interaction):
|
||||||
|
"""
|
||||||
|
Calls edit_clan_council_info().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): Interaction with the command.
|
||||||
|
"""
|
||||||
|
await edit_clan_council_info(interaction.guild, self.bot.config)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Clan Council Info message updated.", ephemeral=True
|
||||||
|
)
|
||||||
@@ -24,7 +24,7 @@ class RequestCog(commands.Cog, name="Request Commands"):
|
|||||||
@nextcord.slash_command()
|
@nextcord.slash_command()
|
||||||
async def request(self, interaction: nextcord.Interaction):
|
async def request(self, interaction: nextcord.Interaction):
|
||||||
"""
|
"""
|
||||||
Parent slash command for help. Never called.
|
Parent slash command for request. Never called.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@request.subcommand(
|
@request.subcommand(
|
||||||
|
|||||||
+25
-9
@@ -10,8 +10,8 @@ from sqlalchemy import delete, select
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dakiny_bot import birthday_list
|
|
||||||
from database_setup.sql_data_classes import UserBirthday, engine
|
from database_setup.sql_data_classes import UserBirthday, engine
|
||||||
|
from main import birthday_list
|
||||||
|
|
||||||
|
|
||||||
class setCog(commands.Cog, name="Set Commands"):
|
class setCog(commands.Cog, name="Set Commands"):
|
||||||
@@ -30,12 +30,18 @@ class setCog(commands.Cog, name="Set Commands"):
|
|||||||
Parent slash command for set. Never called.
|
Parent slash command for set. Never called.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@set.subcommand(description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself.")
|
@set.subcommand(
|
||||||
|
description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself."
|
||||||
|
)
|
||||||
async def birthday(
|
async def birthday(
|
||||||
self,
|
self,
|
||||||
interaction: nextcord.Interaction,
|
interaction: nextcord.Interaction,
|
||||||
month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."),
|
month: int = nextcord.SlashOption(
|
||||||
day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."),
|
required=True, min_value=0, max_value=12, description="Your birth month."
|
||||||
|
),
|
||||||
|
day: int = nextcord.SlashOption(
|
||||||
|
required=True, min_value=0, max_value=31, description="Your birth day."
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list.
|
Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list.
|
||||||
@@ -50,7 +56,9 @@ class setCog(commands.Cog, name="Set Commands"):
|
|||||||
await interaction.response.defer(ephemeral=True)
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
if month == 0 and day == 0:
|
if month == 0 and day == 0:
|
||||||
stmt = delete(UserBirthday).where(UserBirthday.UserId == interaction.user.id)
|
stmt = delete(UserBirthday).where(
|
||||||
|
UserBirthday.UserId == interaction.user.id
|
||||||
|
)
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
session.execute(stmt)
|
session.execute(stmt)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -62,14 +70,18 @@ class setCog(commands.Cog, name="Set Commands"):
|
|||||||
try:
|
try:
|
||||||
birthday = datetime(today.year, month, day)
|
birthday = datetime(today.year, month, day)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True)
|
await interaction.followup.send(
|
||||||
|
f"Invalid date: {month}/{day}.", ephemeral=True
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If the birthday is today, add to birthday_list
|
# If the birthday is today, add to birthday_list
|
||||||
if today.date() == birthday.date():
|
if today.date() == birthday.date():
|
||||||
birthday_list.append(interaction.user.id)
|
birthday_list.append(interaction.user.id)
|
||||||
|
|
||||||
new_birthday = UserBirthday(UserId=interaction.user.id, Birthday=f"{month}/{day}")
|
new_birthday = UserBirthday(
|
||||||
|
UserId=interaction.user.id, Birthday=f"{month}/{day}"
|
||||||
|
)
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
try:
|
try:
|
||||||
session.add(new_birthday)
|
session.add(new_birthday)
|
||||||
@@ -77,9 +89,13 @@ class setCog(commands.Cog, name="Set Commands"):
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# User already has birthday, change it instead.
|
# User already has birthday, change it instead.
|
||||||
session.rollback()
|
session.rollback()
|
||||||
stmt = select(UserBirthday).where(UserBirthday.UserId == interaction.user.id)
|
stmt = select(UserBirthday).where(
|
||||||
|
UserBirthday.UserId == interaction.user.id
|
||||||
|
)
|
||||||
new_birthday = session.scalar(stmt)
|
new_birthday = session.scalar(stmt)
|
||||||
new_birthday.Birthday = f"{month}/{day}"
|
new_birthday.Birthday = f"{month}/{day}"
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
await interaction.followup.send(f"Birthday set to {month}/{day}.", ephemeral=True)
|
await interaction.followup.send(
|
||||||
|
f"Birthday set to {month}/{day}.", ephemeral=True
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from nextcord.ext import application_checks, commands
|
|||||||
|
|
||||||
from classes.election_cycle import ElectionCycle
|
from classes.election_cycle import ElectionCycle
|
||||||
from config import DakinyConfig
|
from config import DakinyConfig
|
||||||
from utils import get_current_cycle
|
from utils import get_current_election_cycle
|
||||||
|
|
||||||
|
|
||||||
def is_me():
|
def is_me():
|
||||||
@@ -50,7 +50,7 @@ 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),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
@@ -67,7 +67,7 @@ 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),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
@@ -84,7 +84,7 @@ 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),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
@@ -101,7 +101,7 @@ 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),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
@@ -112,7 +112,7 @@ class TestCog(commands.Cog, name="Test commands."):
|
|||||||
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),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
@@ -123,7 +123,7 @@ class TestCog(commands.Cog, name="Test commands."):
|
|||||||
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),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
@@ -134,7 +134,7 @@ class TestCog(commands.Cog, name="Test commands."):
|
|||||||
async def term_start(self, interaction: nextcord.Interaction):
|
async def term_start(self, interaction: nextcord.Interaction):
|
||||||
config: DakinyConfig = self.bot.config
|
config: DakinyConfig = self.bot.config
|
||||||
cycle = ElectionCycle(
|
cycle = ElectionCycle(
|
||||||
get_current_cycle(config),
|
get_current_election_cycle(config),
|
||||||
config,
|
config,
|
||||||
self.bot.get_guild(config.params.guild_id),
|
self.bot.get_guild(config.params.guild_id),
|
||||||
)
|
)
|
||||||
|
|||||||
+113
-3
@@ -3,16 +3,18 @@ Commands for viewing information in the database.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import nextcord
|
import nextcord
|
||||||
|
import pandas as pd
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from database_setup.sql_data_classes import (
|
from database_setup.sql_data_classes import (
|
||||||
ElectionInformation,
|
ParticipationPoints,
|
||||||
Presences,
|
Presences,
|
||||||
Votes,
|
Votes,
|
||||||
engine,
|
engine,
|
||||||
)
|
)
|
||||||
|
from utils import VoteType
|
||||||
|
|
||||||
|
|
||||||
class ViewCog(commands.Cog, name="View Commands"):
|
class ViewCog(commands.Cog, name="View Commands"):
|
||||||
@@ -31,6 +33,77 @@ class ViewCog(commands.Cog, name="View Commands"):
|
|||||||
Parent slash command for set. Never called.
|
Parent slash command for set. Never called.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@view.subcommand(description="Show event points.")
|
||||||
|
async def event_points(
|
||||||
|
self,
|
||||||
|
interaction: nextcord.Interaction,
|
||||||
|
quarter_name: str = nextcord.SlashOption(
|
||||||
|
description="The quarter to view",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
# just in case
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
if quarter_name:
|
||||||
|
df = pd.read_sql_query(
|
||||||
|
sql=session.query(ParticipationPoints)
|
||||||
|
.where(ParticipationPoints.Cycle.startswith(quarter_name))
|
||||||
|
.statement,
|
||||||
|
con=engine,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
df = pd.read_sql_query(
|
||||||
|
sql=session.query(ParticipationPoints).statement,
|
||||||
|
con=engine,
|
||||||
|
)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
await interaction.followup.send(
|
||||||
|
f'Quarter "{quarter_name}" is invalid.', ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
cycles = df.groupby("Cycle")
|
||||||
|
content = ""
|
||||||
|
for cycle in cycles.groups:
|
||||||
|
content += f"**{cycle}**\n"
|
||||||
|
unsorted_all = cycles.get_group(cycle).groupby("DiscordMember")
|
||||||
|
members = unsorted_all.get_group(True)
|
||||||
|
non_members = unsorted_all.get_group(False)
|
||||||
|
sorted_members = members.sort_values(
|
||||||
|
by="UserId",
|
||||||
|
key=lambda user_ids: (
|
||||||
|
[
|
||||||
|
interaction.guild.get_member(int(id)).display_name
|
||||||
|
for id in user_ids
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
sorted_non_members = non_members.sort_values(by="UserId")
|
||||||
|
sorted_all = pd.concat(
|
||||||
|
[sorted_members, sorted_non_members], ignore_index=True, sort=False
|
||||||
|
)
|
||||||
|
for row in sorted_all.itertuples():
|
||||||
|
userid = row.UserId
|
||||||
|
points = row.Points
|
||||||
|
discord_member = row.DiscordMember
|
||||||
|
content += (
|
||||||
|
f"<@{userid}>: {points}\n"
|
||||||
|
if discord_member
|
||||||
|
else f"{userid}: {points}\n"
|
||||||
|
)
|
||||||
|
content += "\n"
|
||||||
|
|
||||||
|
if len(content) > 2000:
|
||||||
|
await interaction.followup.send(
|
||||||
|
"Message length is too long, please select search to limit results."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await interaction.followup.send(content=content, ephemeral=True)
|
||||||
|
|
||||||
@view.subcommand(description="Show all possible presence choices.")
|
@view.subcommand(description="Show all possible presence choices.")
|
||||||
async def presences(
|
async def presences(
|
||||||
self,
|
self,
|
||||||
@@ -76,6 +149,11 @@ class ViewCog(commands.Cog, name="View Commands"):
|
|||||||
election_name: str = nextcord.SlashOption(
|
election_name: str = nextcord.SlashOption(
|
||||||
required=True, description="The election you want to view votes for."
|
required=True, description="The election you want to view votes for."
|
||||||
),
|
),
|
||||||
|
election_type: str = nextcord.SlashOption(
|
||||||
|
required=True,
|
||||||
|
description="The election type. Council, Discord Manager, or Clan Leader.",
|
||||||
|
choices=VoteType,
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Takes in an election name and sends a response message to the user with a list of people who voted in that
|
Takes in an election name and sends a response message to the user with a list of people who voted in that
|
||||||
@@ -87,7 +165,11 @@ class ViewCog(commands.Cog, name="View Commands"):
|
|||||||
election_name (str): Is SlashOption. The name of the election to view votes for.
|
election_name (str): Is SlashOption. The name of the election to view votes for.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
stmt = select(Votes).where(Votes.ElectionCycle == election_name)
|
stmt = (
|
||||||
|
select(Votes)
|
||||||
|
.where(Votes.ElectionCycle == election_name)
|
||||||
|
.where(Votes.Type == election_type)
|
||||||
|
)
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
# Check if election exists.
|
# Check if election exists.
|
||||||
@@ -121,7 +203,7 @@ class ViewCog(commands.Cog, name="View Commands"):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = select(ElectionInformation.ElectionCycle)
|
stmt = select(Votes.ElectionCycle).distinct()
|
||||||
elections = session.scalars(stmt)
|
elections = session.scalars(stmt)
|
||||||
|
|
||||||
# if empty compare string
|
# if empty compare string
|
||||||
@@ -135,3 +217,31 @@ class ViewCog(commands.Cog, name="View Commands"):
|
|||||||
if election.lower().startswith(election_name.lower())
|
if election.lower().startswith(election_name.lower())
|
||||||
]
|
]
|
||||||
await interaction.response.send_autocomplete(autofilled_elections[:25])
|
await interaction.response.send_autocomplete(autofilled_elections[:25])
|
||||||
|
|
||||||
|
@event_points.on_autocomplete("quarter_name")
|
||||||
|
async def event_points_autocomplete(
|
||||||
|
self, interaction: nextcord.Interaction, quarter_name: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Collects all elections that start with the election string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): Interaction with command.
|
||||||
|
quarter_name (str): String to run autocomplete on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
stmt = select(ParticipationPoints.Cycle).distinct()
|
||||||
|
quarters = session.scalars(stmt)
|
||||||
|
|
||||||
|
# if empty compare string
|
||||||
|
if not quarter_name:
|
||||||
|
await interaction.response.send_autocomplete(list(quarters)[:25])
|
||||||
|
return
|
||||||
|
|
||||||
|
autofilled_quarters: "list[str]" = [
|
||||||
|
quarter
|
||||||
|
for quarter in quarters
|
||||||
|
if quarter.lower().startswith(quarter_name.lower())
|
||||||
|
]
|
||||||
|
await interaction.response.send_autocomplete(autofilled_quarters[:25])
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from classes.election_cycle import ElectionCycle
|
from classes.election_cycle import ElectionCycle
|
||||||
from config import DakinyConfig
|
from config import DakinyConfig
|
||||||
from dakiny_bot import birthday_list
|
|
||||||
from database_setup.sql_data_classes import UserBirthday, engine
|
from database_setup.sql_data_classes import UserBirthday, engine
|
||||||
from utils import get_current_cycle
|
from main import birthday_list
|
||||||
|
from utils import get_current_election_cycle
|
||||||
|
|
||||||
|
|
||||||
class DailyLoop(commands.Cog, name="Daily Loop"):
|
class DailyLoop(commands.Cog, name="Daily Loop"):
|
||||||
@@ -77,7 +77,7 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
config: DakinyConfig = self.bot.config
|
config: DakinyConfig = self.bot.config
|
||||||
cycle = get_current_cycle(config)
|
cycle = get_current_election_cycle(config)
|
||||||
|
|
||||||
# prevent exception from using cycle as an Int in an array
|
# prevent exception from using cycle as an Int in an array
|
||||||
if cycle is None:
|
if cycle is None:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Defines cog for on message listener.
|
|||||||
import nextcord
|
import nextcord
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from dakiny_bot import birthday_list
|
from main import birthday_list
|
||||||
|
|
||||||
|
|
||||||
class SubmissionListener(commands.Cog, name="Message Listener"):
|
class SubmissionListener(commands.Cog, name="Message Listener"):
|
||||||
@@ -39,5 +39,7 @@ async def birthday_action(message: nextcord.Message) -> None:
|
|||||||
message (nextcord.Message): The message to check for a birthday person.
|
message (nextcord.Message): The message to check for a birthday person.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if message.author.id in birthday_list or bool(set(message.raw_mentions) & set(birthday_list)):
|
if message.author.id in birthday_list or bool(
|
||||||
|
set(message.raw_mentions) & set(birthday_list)
|
||||||
|
):
|
||||||
await message.add_reaction("🎂")
|
await message.add_reaction("🎂")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
File contains classes defining the Hydra configuraton structure.
|
File contains classes defining the Hydra configuraton structure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +18,13 @@ class Params:
|
|||||||
clan_info_channel_id: int
|
clan_info_channel_id: int
|
||||||
clan_member_role_id: int
|
clan_member_role_id: int
|
||||||
discord_token: str
|
discord_token: str
|
||||||
|
event_host_points: int
|
||||||
|
event_host_role_id: int
|
||||||
|
event_participate_points: int
|
||||||
|
event_points_weekly_limit: int
|
||||||
|
event_post_channel_id: int
|
||||||
general_channel_id: int
|
general_channel_id: int
|
||||||
|
git_branch: str
|
||||||
guild_id: int
|
guild_id: int
|
||||||
high_council_role_id: int
|
high_council_role_id: int
|
||||||
voting_parent_id: int
|
voting_parent_id: int
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ CREATE TABLE Votes(
|
|||||||
ElectionCycle varchar(30) not null,
|
ElectionCycle varchar(30) not null,
|
||||||
VoterId bigint unsigned not null,
|
VoterId bigint unsigned not null,
|
||||||
VotedForId bigint unsigned,
|
VotedForId bigint unsigned,
|
||||||
|
Type varchar(14) not null DEFAULT 'council' check("Type" in ('council', 'discordmanager', 'clanleader')),
|
||||||
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
|
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
|
||||||
PRIMARY KEY (ElectionCycle, VoterId)
|
PRIMARY KEY (ElectionCycle, VoterId, Type)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE UserBirthday (
|
CREATE TABLE UserBirthday (
|
||||||
@@ -38,4 +39,24 @@ CREATE TABLE UserBirthday (
|
|||||||
CREATE TABLE Presences (
|
CREATE TABLE Presences (
|
||||||
Id integer PRIMARY KEY,
|
Id integer PRIMARY KEY,
|
||||||
Presence varchar(100) not null
|
Presence varchar(100) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ParticipationPoints (
|
||||||
|
Cycle varchar(30) not null,
|
||||||
|
UserId varchar(32) not null,
|
||||||
|
DiscordMember boolean not null,
|
||||||
|
Points int unsigned DEFAULT 0,
|
||||||
|
PRIMARY KEY (Cycle, UserId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Messages (
|
||||||
|
Path varchar(100) PRIMARY KEY,
|
||||||
|
Id bigint unsigned not null
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE EventPointsLimit (
|
||||||
|
UserId varchar(32) not null,
|
||||||
|
Points int unsigned DEFAULT 0,
|
||||||
|
Week varchar(7) not null,
|
||||||
|
PRIMARY KEY (UserId, Week)
|
||||||
);
|
);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Simple file to set up the sqlalchemy connection to database.
|
Simple file to set up the sqlalchemy connection to database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.automap import automap_base
|
from sqlalchemy.ext.automap import automap_base
|
||||||
|
|
||||||
@@ -11,8 +12,11 @@ Base = automap_base()
|
|||||||
Base.prepare(autoload_with=engine)
|
Base.prepare(autoload_with=engine)
|
||||||
|
|
||||||
# define the classes
|
# define the classes
|
||||||
ElectionInformation = Base.classes.ElectionInformation
|
|
||||||
CouncilMembers = Base.classes.CouncilMembers
|
CouncilMembers = Base.classes.CouncilMembers
|
||||||
Votes = Base.classes.Votes
|
ElectionInformation = Base.classes.ElectionInformation
|
||||||
UserBirthday = Base.classes.UserBirthday
|
EventPointsLimit = Base.classes.EventPointsLimit
|
||||||
|
Messages = Base.classes.Messages
|
||||||
|
ParticipationPoints = Base.classes.ParticipationPoints
|
||||||
Presences = Base.classes.Presences
|
Presences = Base.classes.Presences
|
||||||
|
UserBirthday = Base.classes.UserBirthday
|
||||||
|
Votes = Base.classes.Votes
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "dakiny-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Dakiny's Discord Bot."
|
||||||
|
authors = ["imcompiling <[email protected]>"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.11"
|
||||||
|
gitpython = "3.1.43"
|
||||||
|
fastapi = "0.111.0"
|
||||||
|
hydra-core = "1.3.2"
|
||||||
|
nextcord = "2.4.2"
|
||||||
|
pandas = "1.5.2"
|
||||||
|
python-dotenv = "1.0.0"
|
||||||
|
pytz = "2022.7.1"
|
||||||
|
sqlalchemy = "1.4.46"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
nextcord==2.4.2
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
pytz==2022.7.1
|
|
||||||
hydra-core==1.3.2
|
|
||||||
sqlalchemy<2.0
|
|
||||||
@@ -2,16 +2,31 @@
|
|||||||
File contains util functions.
|
File contains util functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
import nextcord
|
import nextcord
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, true, update
|
||||||
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 CouncilMembers, Votes, engine
|
from database_setup.sql_data_classes import (
|
||||||
|
CouncilMembers,
|
||||||
|
EventPointsLimit,
|
||||||
|
Messages,
|
||||||
|
ParticipationPoints,
|
||||||
|
Votes,
|
||||||
|
engine,
|
||||||
|
)
|
||||||
|
|
||||||
NOMINEE_SPLITTER = "========================================\n\n"
|
NOMINEE_SPLITTER = "========================================\n\n"
|
||||||
|
REPO_DIR = "./dakiny-messages"
|
||||||
|
MESSAGES_DIR = f"{REPO_DIR}/messages"
|
||||||
|
TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
|
||||||
|
|
||||||
|
VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
|
||||||
|
|
||||||
|
|
||||||
def get_datetime(day: str) -> datetime:
|
def get_datetime(day: str) -> datetime:
|
||||||
@@ -38,7 +53,7 @@ def get_datetime(day: str) -> datetime:
|
|||||||
return datetime(year, month, day)
|
return datetime(year, month, day)
|
||||||
|
|
||||||
|
|
||||||
def get_election_cycle_name(config: DakinyConfig) -> str:
|
def get_election_name(config: DakinyConfig) -> str:
|
||||||
"""
|
"""
|
||||||
Builds the unique name of the current election cycle
|
Builds the unique name of the current election cycle
|
||||||
|
|
||||||
@@ -48,43 +63,30 @@ def get_election_cycle_name(config: DakinyConfig) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
str: the name of the election cycle
|
str: the name of the election cycle
|
||||||
"""
|
"""
|
||||||
cycle = get_current_cycle(config)
|
|
||||||
|
cycle = get_current_election_cycle(config)
|
||||||
|
current_cycle = config.election_cycles[cycle]
|
||||||
|
return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_quarter_name(config: DakinyConfig) -> str:
|
||||||
|
"""
|
||||||
|
Builds the name of the current cycle/quarter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (DakinyConfig): the config used to know the election cycle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the name of the cycle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cycle = get_quarter_cycle(config)
|
||||||
current_cycle = config.election_cycles[cycle]
|
current_cycle = config.election_cycles[cycle]
|
||||||
|
|
||||||
return (
|
return f"{get_datetime(current_cycle.term_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:
|
def get_current_election_cycle(config: DakinyConfig) -> int:
|
||||||
"""
|
|
||||||
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:
|
|
||||||
"""
|
"""
|
||||||
Returns index for active election. An election is considered active between the election start and the term start.
|
Returns index for active election. An election is considered active between the election start and the term start.
|
||||||
|
|
||||||
@@ -104,6 +106,29 @@ def get_current_cycle(config: DakinyConfig) -> int:
|
|||||||
):
|
):
|
||||||
return cycle
|
return cycle
|
||||||
|
|
||||||
|
# It isn't a current election, get the quarter
|
||||||
|
return get_quarter_cycle(config)
|
||||||
|
|
||||||
|
|
||||||
|
def get_quarter_cycle(config: DakinyConfig) -> int:
|
||||||
|
"""
|
||||||
|
Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (DakinyConfig): Config object to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: cycle index for current cycle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
today = datetime.today()
|
||||||
|
for cycle, election in enumerate(config.election_cycles):
|
||||||
|
if (
|
||||||
|
today.date() >= get_datetime(election.term_start).date()
|
||||||
|
and today.date() <= get_datetime(election.term_end).date()
|
||||||
|
):
|
||||||
|
return cycle
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -121,9 +146,11 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
|
|||||||
|
|
||||||
votes: dict[int, int] = {}
|
votes: dict[int, int] = {}
|
||||||
|
|
||||||
|
election_name = get_election_name(config)
|
||||||
|
|
||||||
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 == election_name
|
||||||
)
|
)
|
||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
@@ -132,8 +159,10 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
|
|||||||
):
|
):
|
||||||
votes[nominee.MemberId] = 0
|
votes[nominee.MemberId] = 0
|
||||||
|
|
||||||
stmt = select(Votes).where(
|
stmt = (
|
||||||
Votes.ElectionCycle == get_election_cycle_name(config)
|
select(Votes)
|
||||||
|
.where(Votes.ElectionCycle == election_name)
|
||||||
|
.where(Votes.Type == VoteType.COUNCIL)
|
||||||
)
|
)
|
||||||
|
|
||||||
for vote in session.scalars(stmt):
|
for vote in session.scalars(stmt):
|
||||||
@@ -161,10 +190,12 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
cl_votes: dict[int, int] = {}
|
cl_votes: dict[int, int] = {}
|
||||||
dm_votes: dict[int, int] = {}
|
dm_votes: dict[int, int] = {}
|
||||||
|
|
||||||
|
election_name = get_election_name(config)
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMembers)
|
||||||
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
|
.where(CouncilMembers.ElectionCycle == election_name)
|
||||||
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -174,7 +205,11 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
):
|
):
|
||||||
cl_votes[nominee.MemberId] = 0
|
cl_votes[nominee.MemberId] = 0
|
||||||
|
|
||||||
stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config))
|
stmt = (
|
||||||
|
select(Votes)
|
||||||
|
.where(Votes.ElectionCycle == election_name)
|
||||||
|
.where(Votes.Type == VoteType.CLANLEADER)
|
||||||
|
)
|
||||||
|
|
||||||
for vote in session.scalars(stmt):
|
for vote in session.scalars(stmt):
|
||||||
if vote.VotedForId is not None:
|
if vote.VotedForId is not None:
|
||||||
@@ -182,7 +217,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(CouncilMembers)
|
select(CouncilMembers)
|
||||||
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
|
.where(CouncilMembers.ElectionCycle == election_name)
|
||||||
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,7 +227,11 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
):
|
):
|
||||||
dm_votes[nominee.MemberId] = 0
|
dm_votes[nominee.MemberId] = 0
|
||||||
|
|
||||||
stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config))
|
stmt = (
|
||||||
|
select(Votes)
|
||||||
|
.where(Votes.ElectionCycle == election_name)
|
||||||
|
.where(Votes.Type == VoteType.DISCORDMANAGER)
|
||||||
|
)
|
||||||
|
|
||||||
for vote in session.scalars(stmt):
|
for vote in session.scalars(stmt):
|
||||||
if vote.VotedForId is not None:
|
if vote.VotedForId is not None:
|
||||||
@@ -277,3 +316,288 @@ def decorate_dm_nomination(user_id: int, speech: str) -> str:
|
|||||||
msg += speech
|
msg += speech
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def record_event_points(
|
||||||
|
config: DakinyConfig, users: Sequence[str], is_member: bool, add_points: int
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Adds event points to all users listed. Checks if they don't exist in order to make a new entry, otherwise
|
||||||
|
adds points to the existing value. Can be used for hosting or participating.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (DakinyConfig): Dakiny config for getting points
|
||||||
|
users (Sequence[int]): users that participated and need points.
|
||||||
|
is_member (bool): whether the users are a discord member or not
|
||||||
|
points (int): number of points to add
|
||||||
|
"""
|
||||||
|
|
||||||
|
quarter_name = get_quarter_name(config)
|
||||||
|
with Session(engine) as session:
|
||||||
|
for user in users:
|
||||||
|
stmt = (
|
||||||
|
select(ParticipationPoints)
|
||||||
|
.where(ParticipationPoints.UserId == user)
|
||||||
|
.where(ParticipationPoints.Cycle == quarter_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
points = session.scalar(stmt)
|
||||||
|
if points is None:
|
||||||
|
add_points = get_allowed_points(
|
||||||
|
session, config.params.event_points_weekly_limit, user, add_points
|
||||||
|
)
|
||||||
|
new_points = ParticipationPoints(
|
||||||
|
Cycle=quarter_name,
|
||||||
|
UserId=user,
|
||||||
|
DiscordMember=is_member,
|
||||||
|
Points=add_points,
|
||||||
|
)
|
||||||
|
session.add(new_points)
|
||||||
|
else:
|
||||||
|
add_points = get_allowed_points(
|
||||||
|
session, config.params.event_points_weekly_limit, user, add_points
|
||||||
|
)
|
||||||
|
points.Points += add_points
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_allowed_points(
|
||||||
|
session: Session, points_limit: int, user: str, points: int
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Gets the maximum number of points a user can be given. Searches how many points they have been given
|
||||||
|
this week and subtracts that from the weekly limit. If under, returns up to "points" number of points (will not
|
||||||
|
go over points_limit) otherwise returns 0. Updates the limit object or creates a new one if one isn't found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Open connection to database.
|
||||||
|
points_limit (int): Maximum number of points a user can be given in a week.
|
||||||
|
user (str): user to be given points. Used to check a user's limit.
|
||||||
|
points (int): maximum number of points to give to user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: how many points are allowed to be given to the user.
|
||||||
|
"""
|
||||||
|
# check for limit
|
||||||
|
this_week = (
|
||||||
|
f"{datetime.today().isocalendar().year}-{datetime.today().isocalendar().week}"
|
||||||
|
)
|
||||||
|
stmt = (
|
||||||
|
select(EventPointsLimit)
|
||||||
|
.where(EventPointsLimit.UserId == user)
|
||||||
|
.where(EventPointsLimit.Week == this_week)
|
||||||
|
)
|
||||||
|
|
||||||
|
# if under limit, can add up to however many points would make the current limit equal to the
|
||||||
|
# allowed limit.
|
||||||
|
limit = session.scalar(stmt)
|
||||||
|
if limit is None:
|
||||||
|
new_limit = EventPointsLimit(UserId=user, Points=points, Week=this_week)
|
||||||
|
add_points = points
|
||||||
|
session.add(new_limit)
|
||||||
|
else:
|
||||||
|
add_points = (
|
||||||
|
points
|
||||||
|
if limit.Points + points <= points_limit
|
||||||
|
else (points_limit - limit.Points if points_limit - limit.Points > 0 else 0)
|
||||||
|
)
|
||||||
|
limit.Points += add_points
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return add_points
|
||||||
|
|
||||||
|
|
||||||
|
async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[str]):
|
||||||
|
"""
|
||||||
|
Updates all clan messages to contents in MESSAGES_DIR.
|
||||||
|
Does not to any git work. Is expected to be called after git is pulled.
|
||||||
|
|
||||||
|
for ever directory, sort the files by number then edits/sends the message.
|
||||||
|
|
||||||
|
Channel fetching/management is handled by another function.
|
||||||
|
Message editing and sending is handled by another function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild (nextcord.Guild): Guild object that all channels and messages belong to.
|
||||||
|
channel_updates: (Sequence[str]): a sequence of the channels to update.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dirs = [
|
||||||
|
dir_name
|
||||||
|
for dir_name in channel_updates
|
||||||
|
if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}")
|
||||||
|
]
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
for dir_name in dirs:
|
||||||
|
dir_path = f"{MESSAGES_DIR}/{dir_name}"
|
||||||
|
channel = await get_clan_message_channel(session, dir_name, guild)
|
||||||
|
sorted_files = sorted(
|
||||||
|
os.listdir(dir_path),
|
||||||
|
key=lambda filename: int(filename.split(".")[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
for file_name in sorted_files:
|
||||||
|
message_name = f"{dir_name}/{file_name}"
|
||||||
|
file_path = f"{dir_path}/{file_name}"
|
||||||
|
with open(file_path, "r", encoding="utf8") as reader:
|
||||||
|
await edit_clan_message(
|
||||||
|
session, channel, message_name, reader.read(2000)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_clan_message_channel(
|
||||||
|
session: Session, channel_name: str, guild: nextcord.Guild
|
||||||
|
) -> nextcord.TextChannel:
|
||||||
|
"""
|
||||||
|
Used to obtain a channel from the guild object. Searches database for an id to get the channel. If that is not
|
||||||
|
found, then searches through all channels in the guild to find one with a matching name. If that also doesn't
|
||||||
|
turn up a channel, then creates it. Adds an entry into database for last two paths.
|
||||||
|
Assumes that there is an open session to the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Open connection to database.
|
||||||
|
channel_name (str): Name of channel to obtain.
|
||||||
|
guild (nextcord.Guild): Guild object to get channel from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
nextcord.TextChannel: Channel object with name channel_name.
|
||||||
|
"""
|
||||||
|
stmt = select(Messages.Id).where(Messages.Path == channel_name)
|
||||||
|
channel_id = session.scalar(stmt)
|
||||||
|
if channel_id is None:
|
||||||
|
ret_channel = None
|
||||||
|
# channel is not tracked. see if it exists
|
||||||
|
channels = await guild.fetch_channels()
|
||||||
|
for channel in channels:
|
||||||
|
if (
|
||||||
|
channel.name == channel_name
|
||||||
|
and channel.__class__ is nextcord.TextChannel
|
||||||
|
):
|
||||||
|
ret_channel = channel
|
||||||
|
|
||||||
|
if ret_channel is None:
|
||||||
|
# channel doesn't exist, we must create it
|
||||||
|
ret_channel = await guild.create_text_channel(
|
||||||
|
channel_name,
|
||||||
|
reason="Creating channel as required for by design of Dakiny-Messages repo.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# record channel information
|
||||||
|
new_item = Messages(Path=channel_name, Id=ret_channel.id)
|
||||||
|
session.add(new_item)
|
||||||
|
session.commit()
|
||||||
|
return ret_channel
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await guild.fetch_channel(channel_id)
|
||||||
|
except nextcord.NotFound:
|
||||||
|
# the channel was deleted for some reason
|
||||||
|
ret_channel = await guild.create_text_channel(
|
||||||
|
channel_name,
|
||||||
|
reason="Creating channel as required for by design of Dakiny-Messages repo.",
|
||||||
|
)
|
||||||
|
stmt = (
|
||||||
|
update(Messages)
|
||||||
|
.where(Messages.Path == channel_name)
|
||||||
|
.values(Id=ret_channel.id)
|
||||||
|
)
|
||||||
|
session.execute(stmt)
|
||||||
|
session.commit()
|
||||||
|
return ret_channel
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_clan_message(
|
||||||
|
session: Session,
|
||||||
|
channel: nextcord.TextChannel,
|
||||||
|
message_name: str,
|
||||||
|
message_content: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Obtains a message id from database. If no id, then need to send a new message and record the id.
|
||||||
|
If there is a message, then edit the message. If that message is not found in discord, then need to
|
||||||
|
send a new message and record the id in the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Open connection to database.
|
||||||
|
channel (nextcord.TextChannel): Channel object that message belongs to.
|
||||||
|
message_name (str): Name of the message. Corresponds to the file that contains message content.
|
||||||
|
message_content (str): Content of the message to edit/send.
|
||||||
|
"""
|
||||||
|
stmt = select(Messages.Id).where(Messages.Path == message_name)
|
||||||
|
msg_id = session.scalar(stmt)
|
||||||
|
if msg_id is None:
|
||||||
|
# don't have a message for this file
|
||||||
|
msg = await channel.send(message_content)
|
||||||
|
new_message = Messages(Path=message_name, Id=msg.id)
|
||||||
|
session.add(new_message)
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
msg: nextcord.Message = await channel.fetch_message(msg_id)
|
||||||
|
await msg.edit(message_content)
|
||||||
|
except nextcord.NotFound:
|
||||||
|
msg = await channel.send(message_content)
|
||||||
|
stmt = (
|
||||||
|
update(Messages).where(Messages.Path == message_name).values(Id=msg.id)
|
||||||
|
)
|
||||||
|
session.execute(stmt)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig):
|
||||||
|
"""
|
||||||
|
Gets the winners of the elections and updates the perminant message that states the current council.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild (nextcord.Guild): Guild object that to use to get channel.
|
||||||
|
config (DakinyConfig): Configs
|
||||||
|
"""
|
||||||
|
clan_info_channel: nextcord.TextChannel = await guild.fetch_channel(
|
||||||
|
config.params.clan_info_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
election = get_election_name(config)
|
||||||
|
with Session(engine) as session:
|
||||||
|
stmt = (
|
||||||
|
select(CouncilMembers.MemberId)
|
||||||
|
.where(CouncilMembers.ElectionCycle == election)
|
||||||
|
.where(CouncilMembers.DiscordManager == true())
|
||||||
|
)
|
||||||
|
discord_manager = session.scalar(stmt)
|
||||||
|
stmt = (
|
||||||
|
select(CouncilMembers.MemberId)
|
||||||
|
.where(CouncilMembers.ElectionCycle == election)
|
||||||
|
.where(CouncilMembers.ClanLeader == true())
|
||||||
|
)
|
||||||
|
clan_leader = session.scalar(stmt)
|
||||||
|
stmt = (
|
||||||
|
select(CouncilMembers.MemberId)
|
||||||
|
.where(CouncilMembers.ElectionCycle == election)
|
||||||
|
.where(CouncilMembers.Elected == true())
|
||||||
|
)
|
||||||
|
|
||||||
|
council_members_strs = [
|
||||||
|
f"<@{id}>"
|
||||||
|
for id in session.scalars(stmt)
|
||||||
|
if id not in (discord_manager, clan_leader)
|
||||||
|
]
|
||||||
|
|
||||||
|
quarter = get_quarter_name(config)
|
||||||
|
|
||||||
|
with open(
|
||||||
|
f"{TEMPLATES_DIR}/council_members.txt", mode="r", encoding="utf8"
|
||||||
|
) as file:
|
||||||
|
message = file.read()
|
||||||
|
message = message.format(
|
||||||
|
quarter=quarter,
|
||||||
|
council="\n".join(council_members_strs),
|
||||||
|
clan_leader=clan_leader,
|
||||||
|
discord_manager=discord_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
await edit_clan_message(
|
||||||
|
session, clan_info_channel, "council_members.txt", message
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user