Compare commits
157
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45b304308b | ||
|
|
43ef19fd82 | ||
|
|
757d2e5c19 | ||
|
|
8394cdd6fd | ||
|
|
a9c3719c29 | ||
|
|
250de5baf8 | ||
|
|
d449dc6d1a | ||
|
|
b8e35b5e90 | ||
|
|
644eb1dc31 | ||
|
|
09ad45c897 | ||
|
|
55259a2653 | ||
|
|
d75c64ba28 | ||
|
|
08cf614a60 | ||
|
|
18d433ce8c | ||
|
|
a1896416fc | ||
|
|
a7eafdd52a | ||
|
|
8e27e03709 | ||
|
|
be507f3970 | ||
|
|
88d8e6178c | ||
|
|
ee13019a0f | ||
|
|
6e8f999611 | ||
|
|
505f27110d | ||
|
|
1c33139f00 | ||
|
|
b292f956e8 | ||
|
|
654c74e622 | ||
|
|
969880fdb6 | ||
|
|
f9da9fe7ef | ||
|
|
b5ea709c3a | ||
|
|
9980c49764 | ||
|
|
8d02e28271 | ||
|
|
3a5add3c60 | ||
|
|
dd1c304522 | ||
|
|
619ae92d03 | ||
|
|
431e9c0b35 | ||
|
|
27f90bbaef | ||
|
|
c98bb5888c | ||
|
|
c38632ca21 | ||
|
|
75fc59ed5e | ||
|
|
8d2f2f7dba | ||
|
|
ab88652cdb | ||
|
|
b0e3162123 | ||
|
|
8c1eb8d11f | ||
|
|
40d220e2bb | ||
|
|
4e8032823d | ||
|
|
678fcd37e1 | ||
|
|
7530c59a94 | ||
|
|
6abab5c4e5 | ||
|
|
088de263cc | ||
|
|
ffc78a8814 | ||
|
|
3e88d62577 | ||
|
|
392044c658 | ||
|
|
9442a294f3 | ||
|
|
d149caafea | ||
|
|
000cb9b257 | ||
|
|
3fb65a7b0a | ||
|
|
66e7159022 | ||
|
|
5803c336c0 | ||
|
|
80c4f053bc | ||
|
|
289b752678 | ||
|
|
181b1da879 | ||
|
|
dcea911564 | ||
|
|
81e2885e8d | ||
|
|
14f2e13772 | ||
|
|
fa3284b9e3 | ||
|
|
a5ef579767 | ||
|
|
9c588a18af | ||
|
|
ff68e7df1c | ||
|
|
5b1b4cfecb | ||
|
|
626cca8b81 | ||
|
|
e7bd608017 | ||
|
|
a5f0fb71e0 | ||
|
|
a0f160c801 | ||
|
|
19baf84c80 | ||
|
|
133ff79b94 | ||
|
|
647af0353d | ||
|
|
63e3b3edb2 | ||
|
|
ad58f3a9cb | ||
|
|
c013c85f80 | ||
|
|
f86225849c | ||
|
|
a8def8fd69 | ||
|
|
e5adfdb16f | ||
|
|
ee19f32be9 | ||
|
|
8ff53d9a13 | ||
|
|
9609837b1d | ||
|
|
8cf66351dd | ||
|
|
a4c02b2f77 | ||
|
|
8d8af9ab35 | ||
|
|
f70a7989ae | ||
|
|
fb909c009a | ||
|
|
bdefa035ff | ||
|
|
85eb52d2e1 | ||
|
|
7d1b2f7cac | ||
|
|
c5c79015a9 | ||
|
|
7b335efbe6 | ||
|
|
e686cf88e5 | ||
|
|
117dea48c4 | ||
|
|
62e4417ce8 | ||
|
|
672df41f19 | ||
|
|
328f3fea78 | ||
|
|
18751bc85c | ||
|
|
e243b40be7 | ||
|
|
7a91cd37ec | ||
|
|
efadcb30c7 | ||
|
|
5668e4be7e | ||
|
|
1a8c5616ef | ||
|
|
43c1a5c6cd | ||
|
|
c278bae498 | ||
|
|
8b831fb6aa | ||
|
|
e7cfe8bd4c | ||
|
|
2640ebdad1 | ||
|
|
c5c242de21 | ||
|
|
0710db0654 | ||
|
|
369ea0dd55 | ||
|
|
077119acdd | ||
|
|
ac8d1bc681 | ||
|
|
df8cb3dd49 | ||
|
|
25833364d7 | ||
|
|
d73b671257 | ||
|
|
f6e827fba5 | ||
|
|
2991f81e04 | ||
|
|
b1c0b7013d | ||
|
|
22ea29e8ea | ||
|
|
8c20641041 | ||
|
|
fa3fc6bd75 | ||
|
|
271cf14697 | ||
|
|
ee2aaba421 | ||
|
|
6ae722beb9 | ||
|
|
757a753cf5 | ||
|
|
b292fc09fe | ||
|
|
7789e679ce | ||
|
|
9cae7d002a | ||
|
|
f71df0b7f4 | ||
|
|
2d2631bfac | ||
|
|
f9d6227db1 | ||
|
|
c9946c9afe | ||
|
|
c5d148ad49 | ||
|
|
198d5acea0 | ||
|
|
7e3b59ea59 | ||
|
|
babc5d95ab | ||
|
|
0eae8afa2a | ||
|
|
d79c990558 | ||
|
|
3465698b85 | ||
|
|
1731bd46a6 | ||
|
|
1b4c5780df | ||
|
|
d67940aa26 | ||
|
|
9867f91268 | ||
|
|
4763651e93 | ||
|
|
1c76e6806c | ||
|
|
5cb9be7e37 | ||
|
|
d573595114 | ||
|
|
7bded4aa57 | ||
|
|
a6c6a80f8f | ||
|
|
a52979a847 | ||
|
|
8eae82536e | ||
|
|
e5e638edc9 | ||
|
|
fc9eb7b151 | ||
|
|
1cb16d5a39 |
@@ -1,6 +1,10 @@
|
||||
name: build_develop
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '!main'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,7 +16,15 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Install Docker
|
||||
uses: https://github.com/papodaca/install-docker-action@v1
|
||||
- name: Log into container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: gitea.cheezers.org
|
||||
username: imcompiling
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
- name: set env
|
||||
run: echo "GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
- name: Build Docker Image
|
||||
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:develop .
|
||||
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_BRANCH}} .
|
||||
- name: Push Docker Image
|
||||
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:develop
|
||||
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_BRANCH}}
|
||||
|
||||
@@ -14,6 +14,12 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Install Docker
|
||||
uses: https://github.com/papodaca/install-docker-action@v1
|
||||
- name: Log into container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: gitea.cheezers.org
|
||||
username: imcompiling
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
- name: Build Docker Image
|
||||
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:latest .
|
||||
- name: Push Docker Image
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: build_release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v[0-9].[0-9].[0-9]"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Docker
|
||||
uses: https://github.com/papodaca/install-docker-action@v1
|
||||
- name: Log into container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: gitea.cheezers.org
|
||||
username: imcompiling
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
- name: Set env
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- name: Build Docker Image
|
||||
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_TAG}} .
|
||||
- name: Push Tagged Image
|
||||
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_TAG}}
|
||||
@@ -139,3 +139,4 @@ TODO.txt
|
||||
sql_tester.py
|
||||
conf
|
||||
outputs
|
||||
database
|
||||
+8
-3
@@ -1,4 +1,4 @@
|
||||
FROM python:3.8.10-slim
|
||||
FROM python:3.10.6-slim
|
||||
|
||||
# install dependencies for mysqlclient
|
||||
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
|
||||
@@ -7,11 +7,16 @@ RUN pip install --upgrade pip
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
COPY cogs cogs/
|
||||
COPY dakiny_bot.py dakiny_bot.py
|
||||
COPY requirements.txt requirements.txt
|
||||
|
||||
COPY config.py config.py
|
||||
COPY dakiny_bot.py dakiny_bot.py
|
||||
COPY utils.py utils.py
|
||||
|
||||
COPY bot_messages bot_messages/
|
||||
COPY classes classes/
|
||||
COPY cogs cogs/
|
||||
COPY database_setup database_setup/
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
|
||||
@@ -1,50 +1,5 @@
|
||||
# Discord Bot Template
|
||||
# Dakiny Bot
|
||||
|
||||
[](https://discord.gg/fPrdqh3Zfu "Dev Pro Tips Discussion & Support Server")
|
||||
[](https://github.com/nextcord/nextcord "Powered by Nextcord Python API Wrapper")
|
||||
|
||||
This repo is a template for easy creation of maintainable Python Discord bots.
|
||||
|
||||
The library used is [Nextcord](https://github.com/nextcord/nextcord), a maintained fork of Discord.py.
|
||||
|
||||
Nextcord documentation: https://nextcord.readthedocs.io/en/latest/
|
||||
|
||||
📺 Python Discord Tutorial: https://www.youtube.com/playlist?list=PL9YUC9AZJGFG6larkQJYio_f0V-O1NRjy
|
||||
|
||||
## How to use
|
||||
|
||||
Click "use this template" at the top of this repo and follow the instructions, or alternatively, initialize a git repo and copy the template files into the directory.
|
||||
|
||||
```bash
|
||||
# Create a new folder (replace my-discord-bot with your bot's name)
|
||||
mkdir my-discord-bot && cd my-discord-bot
|
||||
# Initialize the folder as a git repository and copy the template files
|
||||
git init
|
||||
cp ../discord-bot-template/* .
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
To run your bot, you'll need a token and other secrets set in a `.env` file.
|
||||
|
||||
Create a file called `.env` and place it in the root of your project.
|
||||
|
||||
(You can do this by creating a copy of `.env.sample` and renaming it to `.env`)
|
||||
|
||||
The contents should look something like this (where the part after `=` is the token you received from the Discord Developer Portal)
|
||||
|
||||
```
|
||||
DISCORD_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
|
||||
If you require additional API keys or variables specific to an enviroment, they should also be added here. You can access them by adding a line such as `GUILD_ID = os.getenv("GUILD_ID", "")` to `config.py`.
|
||||
|
||||
## Heroku
|
||||
|
||||
`runtime.txt` and `Procfile` are used for Heroku configuration and can be deleted in case you do not plan on hosting there.
|
||||
|
||||
Heroku setup tutorial: https://www.youtube.com/watch?v=EreE-0hQibM
|
||||
|
||||
## IDE Configuration
|
||||
|
||||
IDE config such as the `.vscode` folder do not normally belong on GitHub since they are often specific to a particular environment. To make sure GitHub will ignore the `.vscode` folder uncomment the line at the end of the `.gitignore`.
|
||||
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
|
||||
exist in the database.
|
||||
@@ -1,5 +1,5 @@
|
||||
```fix
|
||||
The Dakiny's admin team election
|
||||
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.
|
||||
|
||||
@@ -14,10 +14,14 @@ Each quarter, a channel is created here on Discord for clan members to cast thei
|
||||
**Only clan members can vote and all clan members are granted exactly one equally weighted vote.** If you vote twice for different people, your vote will be annulled.
|
||||
|
||||
All clan members are permitted to run for a seat on the Clan Council as long as they:
|
||||
**i) **have been in the clan for enough time to have participated in at least one election;
|
||||
**ii)** have no warnings under their name
|
||||
and
|
||||
**iii)** have been active (hosted 1 clan event or participate in 1 clan event) in the clan in the past 12 weeks before current election.
|
||||
|
||||
**i)** have been in the clan for at least 13 weeks (enough time to have participated in at least one election);
|
||||
|
||||
**ii)** have no warnings under their name in the last 6 months and
|
||||
|
||||
**iii)** have been active in the clan for the past 13 weeks prior to the first day of elections. To be considered active in that timeframe, one must fulfill the following criteria: hosted/participated in at least three clan events, each one in a different week;
|
||||
|
||||
**iv)** are not running under aliases that hide their true identity.
|
||||
|
||||
Candidates are permitted to vote for themselves. Once a candidate reaches 3 votes from clan members, they have earned their seat, meaning no more votes can be cast for them.
|
||||
|
||||
@@ -34,16 +38,16 @@ The Clan Council will be denoted by the Discord ranks of <@&{clan_council_id}> a
|
||||
<:Rank_Owner:479008325137072155> Clan Leader
|
||||
---message_break---
|
||||
__Here's the timeline:__
|
||||
Election begins: {election_start_date}
|
||||
Term: {term_start_date} - {term_end_date}
|
||||
Election begins: {election_start}
|
||||
Term: {term_start} - {term_end}
|
||||
|
||||
**{election_start_date}**: Post here to announce candidacy
|
||||
**{voting_start_date}**: Voting begins
|
||||
**{voting_end_date}**: Votes are tallied and winners are announced
|
||||
**{highcouncil_election_date}**: The new Clan Council votes who among them shall receive the two High Council positions of: Clan Leader and Discord Server Manager.
|
||||
**{term_start_date}**: The new Clan Council is officially inaugurated and a drop party (or a giveaway event) begins to commemorate the new leadership.
|
||||
**{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_date} with the inauguration scheduled for {term_end_date}.
|
||||
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!
|
||||
**
|
||||
|
||||
@@ -8,6 +8,6 @@ From {highcouncil_election_announce} to {highcouncil_election_start}, you will b
|
||||
Discord Server Manager
|
||||
|
||||
Each member of the CCE can cast one for each category by using the emotes: <:Rank_Owner:479008325137072155> for Clan Leader and <:Rank_Deputy:479008295319896064> for Discord Server Manager.
|
||||
If needed, the icon 🤷 can be used to abstain to vote in any or both categories and it should be apply to all candidates of that category.
|
||||
If needed, the icon 🤷 can be used to abstain to vote in any or both categories and it should be applied to all candidates of that category.
|
||||
|
||||
Votes can be cast until the {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}
|
||||
Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
|
||||
@@ -0,0 +1 @@
|
||||
<@&{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,8 +1,8 @@
|
||||
====================
|
||||
<@&{clan_member_id}>, it's time to vote for clan council.
|
||||
|
||||
React with <{voting_emote_id}> to vote.
|
||||
Voting ends next {voting_end_day}, {voting_end_date}.
|
||||
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.
|
||||
|
||||
+336
-70
@@ -2,13 +2,32 @@
|
||||
Contains the election cycle class
|
||||
"""
|
||||
|
||||
import collections
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select, true
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from classes.nominate_highcouncil_views import NominateHighCouncilView
|
||||
from classes.nominate_views import NominateView
|
||||
from classes.voting_highcouncil_views import VoteHighcouncilView
|
||||
from classes.voting_views import VoteView
|
||||
from config import DakinyConfig
|
||||
from utils import get_datetime
|
||||
from database_setup.sql_data_classes import (
|
||||
CouncilMembers,
|
||||
ElectionInformation,
|
||||
Votes,
|
||||
engine,
|
||||
)
|
||||
from utils import (
|
||||
build_highcouncil_votes_str,
|
||||
build_votes_str,
|
||||
get_cl_election_name,
|
||||
get_datetime,
|
||||
get_dm_election_name,
|
||||
get_election_cycle_name,
|
||||
)
|
||||
|
||||
|
||||
class ElectionCycle:
|
||||
@@ -17,28 +36,35 @@ class ElectionCycle:
|
||||
the cycle.
|
||||
"""
|
||||
|
||||
def __init__(self, cycle: int, bot) -> None:
|
||||
def __init__(self, cycle: int, config: DakinyConfig, guild: nextcord.Guild) -> None:
|
||||
"""
|
||||
Generates all dates relevant to the election cycle from input election start, term start and end dates.
|
||||
Generates all dates relevant to the election cycle determined by the config and cycle.
|
||||
|
||||
Args:
|
||||
election_start (datetime): Date the election cycle starts.
|
||||
term_start (datetime): Date the term being elected starts.
|
||||
term_end (datetime): Date the term being elected ends.
|
||||
bot: the discord bot instance
|
||||
cycle (int): Which election cycle this cycle is.
|
||||
config (DakinyConfig): The entire config object.
|
||||
guild (nextcord.Guild): The guild that this election is running in.
|
||||
"""
|
||||
self.config: DakinyConfig = bot.config
|
||||
|
||||
self.config: DakinyConfig = config
|
||||
self.cycle = cycle
|
||||
self.elecion_cycle = self.config.election_cycles[self.cycle]
|
||||
self.election_start: datetime = get_datetime(self.elecion_cycle.election_start)
|
||||
self.term_start: datetime = get_datetime(self.elecion_cycle.term_start)
|
||||
self.term_end: datetime = get_datetime(self.elecion_cycle.term_end)
|
||||
self.voting_start: datetime = get_datetime(self.elecion_cycle.voting_start)
|
||||
self.voting_end: datetime = get_datetime(self.elecion_cycle.voting_end)
|
||||
self.highcouncil_election_announce: datetime = self.voting_end + timedelta(days=1)
|
||||
self.highcouncil_election_start: datetime = self.highcouncil_election_announce + timedelta(days=3)
|
||||
self.highcouncil_election_end: datetime = self.term_start - timedelta(days=1)
|
||||
self.bot: commands.Bot = bot
|
||||
self.election_cycle = self.config.election_cycles[self.cycle]
|
||||
self.election_start: datetime = get_datetime(self.election_cycle.election_start)
|
||||
self.term_start: datetime = get_datetime(self.election_cycle.term_start)
|
||||
self.term_end: datetime = get_datetime(self.election_cycle.term_end)
|
||||
self.voting_start: datetime = get_datetime(self.election_cycle.voting_start)
|
||||
self.voting_end: datetime = get_datetime(self.election_cycle.voting_end)
|
||||
self.highcouncil_election_announce: datetime = get_datetime(
|
||||
self.election_cycle.highcouncil_election_announce
|
||||
)
|
||||
self.highcouncil_election_start: datetime = get_datetime(
|
||||
self.election_cycle.highcouncil_election_start
|
||||
)
|
||||
self.highcouncil_election_end: datetime = get_datetime(
|
||||
self.election_cycle.highcouncil_election_end
|
||||
)
|
||||
self.guild: nextcord.Guild = guild
|
||||
self.election_over: bool = False
|
||||
|
||||
def build_election_announcement(self) -> list[str]:
|
||||
"""
|
||||
@@ -49,26 +75,32 @@ class ElectionCycle:
|
||||
long.
|
||||
"""
|
||||
|
||||
with open("bot_messages/election_announcement.txt", mode="r", encoding="utf8") as file:
|
||||
with open(
|
||||
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
|
||||
) as file:
|
||||
message = file.read()
|
||||
|
||||
next_election = get_datetime(self.config.election_cycles[(self.cycle + 1) % 4].election_start)
|
||||
next_election = get_datetime(
|
||||
self.config.election_cycles[
|
||||
(self.cycle + 1) % len(self.config.election_cycles)
|
||||
].election_start
|
||||
)
|
||||
|
||||
message = message.format(
|
||||
clan_council_id=self.config.params.clan_council_role_id,
|
||||
high_council_id=self.config.params.high_council_role_id,
|
||||
election_start_date=self.election_start.strftime("%B %d"),
|
||||
term_start_date=self.term_start.strftime("%B %d"),
|
||||
term_end_date=self.term_end.strftime("%B %d"),
|
||||
voting_start_date=self.voting_start.strftime("%B %d"),
|
||||
voting_end_date=self.voting_end.strftime("%B %d"),
|
||||
highcouncil_election_date=self.highcouncil_election_start.strftime("%B %d"),
|
||||
next_election_start_date=next_election.strftime("%B %d"),
|
||||
election_start=self.election_start.strftime("%B %d"),
|
||||
term_start=self.term_start.strftime("%B %d"),
|
||||
term_end=self.term_end.strftime("%B %d"),
|
||||
voting_start=self.voting_start.strftime("%B %d"),
|
||||
voting_end=self.voting_end.strftime("%B %d"),
|
||||
highcouncil_election=self.highcouncil_election_start.strftime("%B %d"),
|
||||
next_election_start=next_election.strftime("%B %d"),
|
||||
)
|
||||
|
||||
return message.split("---message_break---")
|
||||
|
||||
def build_highcouncil_election(self) -> str:
|
||||
def build_highcouncil_election_announce(self) -> str:
|
||||
"""
|
||||
Builds message to announce highcouncil election start.
|
||||
|
||||
@@ -76,10 +108,42 @@ class ElectionCycle:
|
||||
str: Message to send.
|
||||
"""
|
||||
|
||||
with open("bot_messages/highcouncil_election.txt", mode="r", encoding="utf8") as file:
|
||||
with open(
|
||||
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
|
||||
) as file:
|
||||
message = file.read()
|
||||
|
||||
return message.format()
|
||||
return message.format(
|
||||
clan_council_elect_role_id=self.config.params.clan_council_elect_role_id,
|
||||
highcouncil_election_announce=self.highcouncil_election_announce.strftime(
|
||||
"%B %d"
|
||||
),
|
||||
highcouncil_election_start=self.highcouncil_election_start.strftime(
|
||||
"%B %d"
|
||||
),
|
||||
highcouncil_election_end=self.highcouncil_election_end.strftime(
|
||||
"%B %d"
|
||||
),
|
||||
term_start=self.term_start.strftime("%B %d"),
|
||||
)
|
||||
|
||||
def build_highcouncil_vote_start(self) -> str:
|
||||
"""
|
||||
Builds the message to announce that highcouncil voting has started.
|
||||
|
||||
Returns:
|
||||
str: Message to send.
|
||||
"""
|
||||
|
||||
with open(
|
||||
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
|
||||
) as file:
|
||||
message = file.read()
|
||||
|
||||
return message.format(
|
||||
clan_council_elect_role_id=self.config.params.clan_council_elect_role_id,
|
||||
term_start=self.term_start.strftime("%B %d"),
|
||||
)
|
||||
|
||||
def build_vote_start(self) -> str:
|
||||
"""
|
||||
@@ -91,12 +155,13 @@ class ElectionCycle:
|
||||
|
||||
with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file:
|
||||
message = file.read()
|
||||
vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end)
|
||||
vote_end: datetime = get_datetime(
|
||||
self.config.election_cycles[self.cycle].voting_end
|
||||
)
|
||||
|
||||
return message.format(
|
||||
clan_member_id=self.config.params.clan_member_role_id,
|
||||
voting_emote_id=self.config.params.voting_emote_id,
|
||||
voting_end_date=vote_end.strftime("%B %d"),
|
||||
voting_end=vote_end.strftime("%B %d"),
|
||||
voting_end_day=vote_end.strftime("%A"),
|
||||
)
|
||||
|
||||
@@ -105,78 +170,279 @@ class ElectionCycle:
|
||||
Function checks current date to see if something needs to be run.
|
||||
"""
|
||||
today = datetime.today()
|
||||
print("checking the date")
|
||||
|
||||
# check all dates if action is needed
|
||||
if today.date() == self.election_start.date():
|
||||
await self.election_start_action()
|
||||
elif today.date() == self.voting_start.date():
|
||||
await self.voting_end_action()
|
||||
await self.voting_start_action()
|
||||
elif (
|
||||
(self.voting_end - timedelta(3)).date()
|
||||
<= today.date()
|
||||
< self.voting_end.date()
|
||||
):
|
||||
await self.election_reminder()
|
||||
elif today.date() == self.voting_end.date():
|
||||
await self.voting_end_action()
|
||||
elif today.date() == self.highcouncil_election_start.date():
|
||||
await self.highcouncil_election_action()
|
||||
await self.highcouncil_election_start_action()
|
||||
elif today.date() == self.term_start.date():
|
||||
await self.term_start_action()
|
||||
elif today.date() == self.term_end.date():
|
||||
await self.term_end_action()
|
||||
else:
|
||||
print("today was not a special date")
|
||||
print(today)
|
||||
|
||||
async def election_start_action(self) -> None:
|
||||
"""
|
||||
Performs required actions for when the election cycle starts.
|
||||
"""
|
||||
print("election cycle has started")
|
||||
# create new channel for election
|
||||
category: nextcord.CategoryChannel = self.bot.get_channel(self.config.params.active_category_id)
|
||||
quarter_name: str = str(datetime.today().year) + "-" + self.elecion_cycle.name + "-election"
|
||||
# TODO need to store this id in the sqlite database
|
||||
election_channel = await category.create_text_channel(quarter_name)
|
||||
parent: nextcord.TextChannel = self.guild.get_channel(
|
||||
self.config.params.voting_parent_id
|
||||
)
|
||||
quarter_name: str = get_election_cycle_name(self.config)
|
||||
|
||||
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
|
||||
|
||||
thread: nextcord.Thread = await parent.create_thread(
|
||||
name=quarter_name, message=thread_msg
|
||||
)
|
||||
|
||||
# post message announcing election
|
||||
# election start message is returned as an array of string because it is too long. send each string
|
||||
# individually.
|
||||
messages = self.build_election_announcement()
|
||||
for index, message in enumerate(messages):
|
||||
# if last item, add the nominate button
|
||||
if index + 1 == len(messages):
|
||||
nominate_msg: nextcord.Message = await thread.send(
|
||||
message, view=NominateView(self.config, self.guild)
|
||||
)
|
||||
else:
|
||||
await thread.send(message)
|
||||
|
||||
for message in messages:
|
||||
await election_channel.send(message)
|
||||
# create new entry into database.
|
||||
with Session(engine) as session:
|
||||
new_cycle = ElectionInformation(
|
||||
ElectionCycle=quarter_name,
|
||||
ThreadId=thread.id,
|
||||
CouncilNominationMsgId=nominate_msg.id,
|
||||
)
|
||||
session.add(new_cycle)
|
||||
session.commit()
|
||||
|
||||
async def voting_start_action(self) -> None:
|
||||
"""
|
||||
Performs required actions for when voting for candidates starts.
|
||||
Sends message informing members that voting has started.
|
||||
Removes write permission from the general clan member role.
|
||||
"""
|
||||
print("Voting has started")
|
||||
await self.message_me("Voting has started")
|
||||
message = self.build_vote_start()
|
||||
election_thread: nextcord.Thread = self.guild.get_thread(
|
||||
self.get_election_info().ThreadId
|
||||
)
|
||||
await election_thread.send(message)
|
||||
vote_msg = await election_thread.send(
|
||||
build_votes_str(self.config), view=VoteView(self.config, self.guild)
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = select(ElectionInformation).where(
|
||||
ElectionInformation.ElectionCycle
|
||||
== get_election_cycle_name(self.config)
|
||||
)
|
||||
info = session.scalar(stmt)
|
||||
info.CouncilVoteMsgId = vote_msg.id
|
||||
session.commit()
|
||||
|
||||
thread: nextcord.Thread = self.guild.get_thread(info.ThreadId)
|
||||
nominate_msg: nextcord.Message = await thread.fetch_message(
|
||||
info.CouncilNominationMsgId
|
||||
)
|
||||
await nominate_msg.edit(view=None)
|
||||
|
||||
async def voting_end_action(self) -> None:
|
||||
"""
|
||||
Performs required actions for when voting time has ended.
|
||||
"""
|
||||
print("voting has ended")
|
||||
await self.message_me("voting has ended")
|
||||
|
||||
async def highcouncil_election_action(self) -> None:
|
||||
message = self.build_highcouncil_election_announce()
|
||||
council_elect_role: nextcord.Role = self.guild.get_role(
|
||||
self.config.params.clan_council_elect_role_id
|
||||
)
|
||||
election_thread: nextcord.Thread = self.guild.get_thread(
|
||||
self.get_election_info().ThreadId
|
||||
)
|
||||
|
||||
nominate_msg: nextcord.Message = await election_thread.send(
|
||||
message, view=NominateHighCouncilView(self.config, self.guild)
|
||||
)
|
||||
|
||||
election_name = get_election_cycle_name(self.config)
|
||||
|
||||
with Session(engine) as session:
|
||||
# remove vote button from vote message
|
||||
stmt = select(ElectionInformation).where(
|
||||
ElectionInformation.ElectionCycle == election_name
|
||||
)
|
||||
info = session.scalar(stmt)
|
||||
vote_msg: nextcord.Message = await election_thread.fetch_message(
|
||||
info.CouncilVoteMsgId
|
||||
)
|
||||
await vote_msg.edit(view=None)
|
||||
|
||||
# add the highcouncil nomination message to election information
|
||||
info.HighCouncilNominationMsgId = nominate_msg.id
|
||||
session.commit()
|
||||
|
||||
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database
|
||||
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name)
|
||||
vote_count = collections.Counter(session.scalars(stmt))
|
||||
|
||||
for user_id, votes in vote_count.items():
|
||||
if votes >= 3:
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == election_name)
|
||||
.where(CouncilMembers.MemberId == user_id)
|
||||
)
|
||||
council_member = session.scalar(stmt)
|
||||
council_member.Elected = True
|
||||
session.commit()
|
||||
|
||||
member: nextcord.Member = self.guild.get_member(user_id)
|
||||
|
||||
await member.add_roles(
|
||||
council_elect_role,
|
||||
reason=f"{member.display_name} has won a nomination and become a Council Elect.",
|
||||
)
|
||||
|
||||
async def highcouncil_election_start_action(self) -> None:
|
||||
"""
|
||||
Performs required actions for when the highcouncil is elected.
|
||||
"""
|
||||
print("Highcouncil election has started.")
|
||||
await self.message_me("Highcouncil election has started.")
|
||||
|
||||
message = self.build_highcouncil_vote_start()
|
||||
election_thread: nextcord.Thread = self.guild.get_thread(
|
||||
self.get_election_info().ThreadId
|
||||
)
|
||||
await election_thread.send(message)
|
||||
vote_msg = await election_thread.send(
|
||||
build_highcouncil_votes_str(self.config),
|
||||
view=VoteHighcouncilView(self.config, self.guild),
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = select(ElectionInformation).where(
|
||||
ElectionInformation.ElectionCycle
|
||||
== get_election_cycle_name(self.config)
|
||||
)
|
||||
info = session.scalar(stmt)
|
||||
info.HighCouncilVoteMsgId = vote_msg.id
|
||||
session.commit()
|
||||
|
||||
# remove highcouncil nomination view
|
||||
nomination_msg: nextcord.Message = await election_thread.fetch_message(
|
||||
info.HighCouncilNominationMsgId
|
||||
)
|
||||
await nomination_msg.edit(view=None)
|
||||
|
||||
async def term_start_action(self) -> None:
|
||||
msg: str = "the term has started"
|
||||
print(msg)
|
||||
self.message_me(msg)
|
||||
|
||||
async def term_end_action(self):
|
||||
msg: str = "the term has ended"
|
||||
print(msg)
|
||||
self.message_me(msg)
|
||||
|
||||
async def message_me(self, msg: str):
|
||||
"""
|
||||
message a string to me
|
||||
Marks the election as over.
|
||||
"""
|
||||
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
|
||||
await imcompiling.send(msg)
|
||||
|
||||
self.election_over = True
|
||||
|
||||
elect_role: nextcord.Role = self.guild.get_role(
|
||||
self.config.params.clan_council_elect_role_id
|
||||
)
|
||||
council_role: nextcord.Role = self.guild.get_role(
|
||||
self.config.params.clan_council_role_id
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
# role management
|
||||
stmt = (
|
||||
select(CouncilMembers.MemberId)
|
||||
.where(
|
||||
CouncilMembers.ElectionCycle == get_election_cycle_name(self.config)
|
||||
)
|
||||
.where(CouncilMembers.Elected == true())
|
||||
)
|
||||
|
||||
for member_id in session.scalars(stmt):
|
||||
member = self.guild.get_member(member_id)
|
||||
await member.remove_roles(
|
||||
elect_role,
|
||||
reason=f"{member.display_name} no longer needs the elect role.",
|
||||
)
|
||||
await member.add_roles(
|
||||
council_role,
|
||||
reason=f"{member.display_name} has been elected to council.",
|
||||
)
|
||||
|
||||
# announce high council members
|
||||
# ties are undefined behavior.
|
||||
dm_election = get_dm_election_name(self.config)
|
||||
cl_election = get_cl_election_name(self.config)
|
||||
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == dm_election)
|
||||
dm_votes = collections.Counter(session.scalars(stmt))
|
||||
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election)
|
||||
cl_votes = collections.Counter(session.scalars(stmt))
|
||||
|
||||
try:
|
||||
dm_winner = max(dm_votes, key=dm_votes.get)
|
||||
except ValueError:
|
||||
dm_winner = "No winner"
|
||||
try:
|
||||
cl_winner = max(cl_votes, key=cl_votes.get)
|
||||
except ValueError:
|
||||
cl_winner = "No winner"
|
||||
|
||||
election_thread: nextcord.Thread = self.guild.get_thread(
|
||||
self.get_election_info().ThreadId
|
||||
)
|
||||
|
||||
# clear view from vote message
|
||||
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
||||
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
|
||||
)
|
||||
vote_msg_id = session.scalar(stmt)
|
||||
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
|
||||
await vote_msg.edit(view=None)
|
||||
|
||||
highcouncil_msg: str = f"Congratulations for our high council members!\n Clan Leader: <@{cl_winner}>\n"
|
||||
highcouncil_msg += f"Discord Manager: <@{dm_winner}>"
|
||||
await election_thread.send(highcouncil_msg)
|
||||
|
||||
await election_thread.send(
|
||||
"This election cycle is now over. Thank you everyone for participating!"
|
||||
)
|
||||
|
||||
await election_thread.edit(archived=True, locked=True)
|
||||
|
||||
def get_election_info(self) -> ElectionInformation:
|
||||
"""
|
||||
Gets information for current election.
|
||||
|
||||
Returns:
|
||||
ElectionInformation: The information from the database about the current election.
|
||||
"""
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = select(ElectionInformation).where(
|
||||
ElectionInformation.ElectionCycle
|
||||
== get_election_cycle_name(self.config)
|
||||
)
|
||||
return session.scalar(stmt)
|
||||
|
||||
async def election_reminder(self) -> None:
|
||||
"""
|
||||
Sends a message to general chat reminder clan about election.
|
||||
"""
|
||||
|
||||
general: nextcord.TextChannel = self.guild.get_channel(
|
||||
self.config.params.general_channel_id
|
||||
)
|
||||
|
||||
message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over "
|
||||
message += f"to <#{self.get_election_info().ThreadId}> to cast your vote!"
|
||||
|
||||
await general.send(content=message)
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Contains various views to be used in the high council nomination process.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.interactions import Interaction
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import DakinyConfig
|
||||
from database_setup.sql_data_classes import CouncilMembers, engine
|
||||
from utils import (
|
||||
NOMINEE_SPLITTER,
|
||||
decorate_cl_nomination,
|
||||
decorate_dm_nomination,
|
||||
get_election_cycle_name,
|
||||
)
|
||||
|
||||
|
||||
class NominateDMModal(nextcord.ui.Modal):
|
||||
"""
|
||||
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: DakinyConfig,
|
||||
usr_id: int,
|
||||
usr_nominated: bool,
|
||||
speech_content: str,
|
||||
):
|
||||
super().__init__(
|
||||
"Discord Manager Nomination",
|
||||
timeout=5 * 60, # 5 minutes
|
||||
)
|
||||
self.usr_id: int = usr_id
|
||||
self.config: DakinyConfig = config
|
||||
self.usr_nominated = usr_nominated
|
||||
|
||||
self.speech = nextcord.ui.TextInput(
|
||||
label="Nomination Speech",
|
||||
style=nextcord.TextInputStyle.paragraph,
|
||||
placeholder="Please write your speech here.",
|
||||
required=True,
|
||||
max_length=1500,
|
||||
default_value=speech_content,
|
||||
)
|
||||
self.add_item(self.speech)
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
"""
|
||||
Sends the nomination speech into the voting channel and enters the user into the voting database.
|
||||
|
||||
Args:
|
||||
interaction (Interaction): Interaction with user.
|
||||
|
||||
"""
|
||||
|
||||
speech = decorate_dm_nomination(interaction.user.id, self.speech.value)
|
||||
|
||||
with Session(engine) as session:
|
||||
# user already in table
|
||||
cycle = get_election_cycle_name(self.config)
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.where(CouncilMembers.MemberId == self.usr_id)
|
||||
)
|
||||
council_elect = session.scalar(stmt)
|
||||
|
||||
if not self.usr_nominated:
|
||||
msg = await interaction.channel.send(speech)
|
||||
|
||||
council_elect.DiscordManagerSpeechId = msg.id
|
||||
session.commit()
|
||||
else:
|
||||
msg: nextcord.Message = await interaction.channel.fetch_message(
|
||||
council_elect.DiscordManagerSpeechId
|
||||
)
|
||||
await msg.edit(
|
||||
content=decorate_dm_nomination(
|
||||
interaction.user.id, self.speech.value
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NominateCLModal(nextcord.ui.Modal):
|
||||
"""
|
||||
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: DakinyConfig,
|
||||
usr_id: int,
|
||||
usr_nominated: bool,
|
||||
speech_content: str,
|
||||
):
|
||||
super().__init__(
|
||||
"Clan Leader Nomination",
|
||||
timeout=5 * 60, # 5 minutes
|
||||
)
|
||||
self.usr_id: int = usr_id
|
||||
self.config: DakinyConfig = config
|
||||
self.usr_nominated = usr_nominated
|
||||
|
||||
self.speech = nextcord.ui.TextInput(
|
||||
label="Nomination Speech",
|
||||
style=nextcord.TextInputStyle.paragraph,
|
||||
placeholder="Please write your speech here.",
|
||||
required=True,
|
||||
max_length=1500,
|
||||
default_value=speech_content,
|
||||
)
|
||||
self.add_item(self.speech)
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
"""
|
||||
Sends the nomination speech into the voting channel and enters the user into the voting database.
|
||||
|
||||
Args:
|
||||
interaction (Interaction): Interaction with user.
|
||||
|
||||
"""
|
||||
|
||||
speech = decorate_cl_nomination(interaction.user.id, self.speech.value)
|
||||
|
||||
with Session(engine) as session:
|
||||
# user already in table
|
||||
cycle = get_election_cycle_name(self.config)
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.where(CouncilMembers.MemberId == self.usr_id)
|
||||
)
|
||||
council_elect = session.scalar(stmt)
|
||||
|
||||
if not self.usr_nominated:
|
||||
msg = await interaction.channel.send(speech)
|
||||
|
||||
council_elect.ClanLeaderSpeechId = msg.id
|
||||
session.commit()
|
||||
else:
|
||||
msg: nextcord.Message = await interaction.channel.fetch_message(
|
||||
council_elect.ClanLeaderSpeechId
|
||||
)
|
||||
await msg.edit(
|
||||
content=decorate_cl_nomination(
|
||||
interaction.user.id, self.speech.value
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NominateHighCouncilView(nextcord.ui.View):
|
||||
"""
|
||||
The buttons to press to start your High Council nomination.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
|
||||
super().__init__(timeout=None)
|
||||
self.config: DakinyConfig = config
|
||||
self.guild: nextcord.Guild = guild
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Nominate for Discord Manager",
|
||||
style=nextcord.ButtonStyle.primary,
|
||||
custom_id="nominate-dm",
|
||||
)
|
||||
async def nominate_dm(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
) -> None:
|
||||
"""
|
||||
Gathers previous information about the user if they have already nominated themselves, then launches
|
||||
the nomination modal to get their speech.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||
"""
|
||||
|
||||
# enforce council elect only
|
||||
if (
|
||||
self.guild.get_role(self.config.params.clan_council_elect_role_id)
|
||||
not in interaction.user.roles
|
||||
):
|
||||
await interaction.response.send_message(
|
||||
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
cycle = get_election_cycle_name(self.config)
|
||||
usr_nominated = False
|
||||
speech_content = ""
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.where(CouncilMembers.MemberId == interaction.user.id)
|
||||
)
|
||||
obj = session.scalar(stmt)
|
||||
|
||||
# user needs to exist to get here anyways...
|
||||
if obj.DiscordManagerSpeechId is not None:
|
||||
usr_nominated = True
|
||||
msg = await interaction.channel.fetch_message(
|
||||
obj.DiscordManagerSpeechId
|
||||
)
|
||||
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
||||
|
||||
await interaction.response.send_modal(
|
||||
NominateDMModal(
|
||||
self.config, interaction.user.id, usr_nominated, speech_content
|
||||
)
|
||||
)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Nominate for Clan Leader",
|
||||
style=nextcord.ButtonStyle.primary,
|
||||
custom_id="nominate-cl",
|
||||
)
|
||||
async def nominate_cl(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
) -> None:
|
||||
"""
|
||||
Gathers previous information about the user if they have already nominated themselves, then launches
|
||||
the nomination modal to get their speech.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||
"""
|
||||
|
||||
# enforce council elect only
|
||||
if (
|
||||
self.guild.get_role(self.config.params.clan_council_elect_role_id)
|
||||
not in interaction.user.roles
|
||||
):
|
||||
await interaction.response.send_message(
|
||||
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
cycle = get_election_cycle_name(self.config)
|
||||
usr_nominated = False
|
||||
speech_content = ""
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.where(CouncilMembers.MemberId == interaction.user.id)
|
||||
)
|
||||
obj = session.scalar(stmt)
|
||||
|
||||
# user needs to exist to get here anyways...
|
||||
if obj.ClanLeaderSpeechId is not None:
|
||||
usr_nominated = True
|
||||
msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId)
|
||||
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
||||
|
||||
await interaction.response.send_modal(
|
||||
NominateCLModal(
|
||||
self.config, interaction.user.id, usr_nominated, speech_content
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Contains various views to be used in the nomination process.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.interactions import Interaction
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import DakinyConfig
|
||||
from database_setup.sql_data_classes import CouncilMembers, engine
|
||||
from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name
|
||||
|
||||
|
||||
class NominateModal(nextcord.ui.Modal):
|
||||
"""
|
||||
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: DakinyConfig,
|
||||
usr_id: int,
|
||||
usr_nominated: bool,
|
||||
speech_content: str,
|
||||
):
|
||||
super().__init__(
|
||||
"Nomination",
|
||||
timeout=5 * 60, # 5 minutes
|
||||
)
|
||||
self.usr_id: int = usr_id
|
||||
self.config: DakinyConfig = config
|
||||
self.usr_nominated = usr_nominated
|
||||
|
||||
self.speech = nextcord.ui.TextInput(
|
||||
label="Nomination Speech",
|
||||
style=nextcord.TextInputStyle.paragraph,
|
||||
placeholder="Please write your speech here.",
|
||||
required=True,
|
||||
max_length=1500,
|
||||
default_value=speech_content,
|
||||
)
|
||||
self.add_item(self.speech)
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
"""
|
||||
Sends the nomination speech into the voting channel and enters the user into the voting database.
|
||||
|
||||
Args:
|
||||
interaction (Interaction): Interaction with user.
|
||||
|
||||
"""
|
||||
|
||||
speech = decorate_nomination(interaction.user.id, self.speech.value)
|
||||
|
||||
with Session(engine) as session:
|
||||
if not self.usr_nominated:
|
||||
msg = await interaction.channel.send(speech)
|
||||
|
||||
council_member = CouncilMembers(
|
||||
ElectionCycle=get_election_cycle_name(self.config),
|
||||
MemberId=interaction.user.id,
|
||||
SpeechMsgId=msg.id,
|
||||
)
|
||||
session.add(council_member)
|
||||
session.commit()
|
||||
else:
|
||||
# user already nominated, just need to update the election speech
|
||||
cycle = get_election_cycle_name(self.config)
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.where(CouncilMembers.MemberId == self.usr_id)
|
||||
)
|
||||
obj = session.scalar(stmt)
|
||||
|
||||
msg: nextcord.Message = await interaction.channel.fetch_message(
|
||||
obj.SpeechMsgId
|
||||
)
|
||||
await msg.edit(
|
||||
content=decorate_nomination(interaction.user.id, self.speech.value)
|
||||
)
|
||||
|
||||
|
||||
class NominateView(nextcord.ui.View):
|
||||
"""
|
||||
The button to press to start your nomination.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
|
||||
super().__init__(timeout=None)
|
||||
self.config: DakinyConfig = config
|
||||
self.guild: nextcord.Guild = guild
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Nominate Yourself",
|
||||
style=nextcord.ButtonStyle.primary,
|
||||
custom_id="nominate",
|
||||
)
|
||||
async def nominate(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
) -> None:
|
||||
"""
|
||||
Gathers previous information about the user if they have already nominated themselves, then launches
|
||||
the nomination modal to get their speech.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||
"""
|
||||
|
||||
# enforce members only
|
||||
if (
|
||||
self.guild.get_role(self.config.params.clan_member_role_id)
|
||||
not in interaction.user.roles
|
||||
):
|
||||
await interaction.response.send_message(
|
||||
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
cycle = get_election_cycle_name(self.config)
|
||||
usr_nominated = False
|
||||
speech_content = ""
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.where(CouncilMembers.MemberId == interaction.user.id)
|
||||
)
|
||||
obj = session.scalar(stmt)
|
||||
if obj is not None:
|
||||
usr_nominated = True
|
||||
msg = await interaction.channel.fetch_message(obj.SpeechMsgId)
|
||||
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
|
||||
|
||||
await interaction.response.send_modal(
|
||||
NominateModal(
|
||||
self.config, interaction.user.id, usr_nominated, speech_content
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Contains various views to be used while voting for high council. Controls the logic when performing the vote.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
import nextcord
|
||||
from nextcord import SelectOption
|
||||
from nextcord.interactions import Interaction
|
||||
from sqlalchemy import exc, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import DakinyConfig
|
||||
from database_setup.sql_data_classes import (
|
||||
CouncilMembers,
|
||||
ElectionInformation,
|
||||
Votes,
|
||||
engine,
|
||||
)
|
||||
from utils import (
|
||||
build_highcouncil_votes_str,
|
||||
get_cl_election_name,
|
||||
get_dm_election_name,
|
||||
get_election_cycle_name,
|
||||
)
|
||||
|
||||
|
||||
class VoteSelect(nextcord.ui.StringSelect):
|
||||
def __init__(
|
||||
self, options: List["SelectOption"], config: DakinyConfig, election_type: str
|
||||
) -> None:
|
||||
super().__init__(
|
||||
placeholder="Choose 1",
|
||||
min_values=1,
|
||||
max_values=1,
|
||||
options=options,
|
||||
)
|
||||
self.config = config
|
||||
self.election_type = election_type
|
||||
|
||||
async def callback(self, interaction: Interaction):
|
||||
# decide on election type
|
||||
match self.election_type:
|
||||
case "cl":
|
||||
election_name: str = get_cl_election_name(self.config)
|
||||
case "dm":
|
||||
election_name: str = get_dm_election_name(self.config)
|
||||
case _:
|
||||
# should never happen
|
||||
raise ValueError('election type was not "cl" or "dm"')
|
||||
|
||||
cycle: str = get_election_cycle_name(self.config)
|
||||
with Session(engine) as session:
|
||||
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
|
||||
ElectionInformation.ElectionCycle == cycle
|
||||
)
|
||||
vote_msg_id = session.scalar(stmt)
|
||||
|
||||
if self.values[0] == "Abstain":
|
||||
voted_for = None
|
||||
else:
|
||||
voted_for = int(self.values[0])
|
||||
|
||||
new_vote = Votes(
|
||||
ElectionCycle=election_name,
|
||||
VoterId=interaction.user.id,
|
||||
VotedForId=voted_for,
|
||||
)
|
||||
|
||||
try:
|
||||
session.add(new_vote)
|
||||
session.commit()
|
||||
|
||||
except exc.IntegrityError:
|
||||
session.rollback()
|
||||
stmt = (
|
||||
update(Votes)
|
||||
.where(Votes.ElectionCycle == election_name)
|
||||
.where(Votes.VoterId == interaction.user.id)
|
||||
.values(VotedForId=voted_for)
|
||||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
|
||||
vote_message_str = build_highcouncil_votes_str(self.config)
|
||||
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
|
||||
vote_msg_id
|
||||
)
|
||||
await vote_msg.edit(content=vote_message_str)
|
||||
|
||||
if voted_for is not None:
|
||||
msg = f"You have voted for <@{voted_for}>"
|
||||
else:
|
||||
msg = "You have abstained from voting."
|
||||
|
||||
await interaction.edit(content=msg, view=None)
|
||||
|
||||
|
||||
class VoteHighcouncilView(nextcord.ui.View):
|
||||
"""
|
||||
The button to press to start voting for HighCouncil.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
|
||||
super().__init__(timeout=None)
|
||||
self.config = config
|
||||
self.guild = guild
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Vote for Clan Leader",
|
||||
style=nextcord.ButtonStyle.primary,
|
||||
custom_id="vote-cl",
|
||||
)
|
||||
async def vote_cl(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
) -> None:
|
||||
"""
|
||||
Enforces only council elect vote. Gets election cycle, creates voting
|
||||
options for clan leader (checking if user has voted already),
|
||||
then sends the voting view.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||
"""
|
||||
|
||||
# enforce council elect only
|
||||
if (
|
||||
self.guild.get_role(self.config.params.clan_council_elect_role_id)
|
||||
not in interaction.user.roles
|
||||
):
|
||||
await interaction.response.send_message(
|
||||
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
cycle: str = get_election_cycle_name(self.config)
|
||||
election: str = get_cl_election_name(self.config)
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(Votes.VotedForId)
|
||||
.where(Votes.ElectionCycle == election)
|
||||
.where(Votes.VoterId == interaction.user.id)
|
||||
)
|
||||
voted_for = session.scalar(stmt)
|
||||
|
||||
stmt = (
|
||||
select(CouncilMembers.MemberId)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
||||
)
|
||||
|
||||
options = [
|
||||
SelectOption(
|
||||
label=f"{self.guild.get_member(nominee).display_name} ☑️"
|
||||
if voted_for == nominee
|
||||
else f"{self.guild.get_member(nominee).display_name}",
|
||||
value=str(nominee),
|
||||
)
|
||||
for nominee in session.scalars(stmt)
|
||||
]
|
||||
options.append(
|
||||
SelectOption(
|
||||
label="Abstain ☑️" if voted_for is None else "Abstain",
|
||||
value="Abstain",
|
||||
)
|
||||
)
|
||||
|
||||
view = nextcord.ui.View()
|
||||
view.add_item(
|
||||
VoteSelect(options=options, config=self.config, election_type="cl")
|
||||
)
|
||||
|
||||
await interaction.response.send_message(
|
||||
content="Please vote for a Clan Leader!",
|
||||
view=view,
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Vote for Discord Manager",
|
||||
style=nextcord.ButtonStyle.primary,
|
||||
custom_id="vote-dm",
|
||||
)
|
||||
async def vote_dm(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
) -> None:
|
||||
"""
|
||||
Enforces only council elect vote. Gets election cycle, creates voting
|
||||
options for discord manager (checking if user has voted already),
|
||||
then sends the voting view.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||
"""
|
||||
|
||||
# enforce council elect only
|
||||
if (
|
||||
self.guild.get_role(self.config.params.clan_council_elect_role_id)
|
||||
not in interaction.user.roles
|
||||
):
|
||||
await interaction.response.send_message(
|
||||
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
cycle: str = get_election_cycle_name(self.config)
|
||||
election: str = get_dm_election_name(self.config)
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(Votes.VotedForId)
|
||||
.where(Votes.ElectionCycle == election)
|
||||
.where(Votes.VoterId == interaction.user.id)
|
||||
)
|
||||
voted_for = session.scalar(stmt)
|
||||
|
||||
stmt = (
|
||||
select(CouncilMembers.MemberId)
|
||||
.where(CouncilMembers.ElectionCycle == cycle)
|
||||
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
||||
)
|
||||
|
||||
options = [
|
||||
SelectOption(
|
||||
label=f"{self.guild.get_member(nominee).display_name} ☑️"
|
||||
if voted_for == nominee
|
||||
else f"{self.guild.get_member(nominee).display_name}",
|
||||
value=str(nominee),
|
||||
)
|
||||
for nominee in session.scalars(stmt)
|
||||
]
|
||||
options.append(
|
||||
SelectOption(
|
||||
label="Abstain ☑️" if voted_for is None else "Abstain",
|
||||
value="Abstain",
|
||||
)
|
||||
)
|
||||
|
||||
view = nextcord.ui.View()
|
||||
view.add_item(
|
||||
VoteSelect(options=options, config=self.config, election_type="dm")
|
||||
)
|
||||
|
||||
await interaction.response.send_message(
|
||||
content="Please vote for a Discord Manager!",
|
||||
view=view,
|
||||
ephemeral=True,
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Contains various views to be used while voting. Controls the logic when performing the vote.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
import nextcord
|
||||
from nextcord import SelectOption
|
||||
from nextcord.interactions import Interaction
|
||||
from sqlalchemy import exc, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import DakinyConfig
|
||||
from database_setup.sql_data_classes import (
|
||||
CouncilMembers,
|
||||
ElectionInformation,
|
||||
Votes,
|
||||
engine,
|
||||
)
|
||||
from utils import build_votes_str, get_election_cycle_name
|
||||
|
||||
|
||||
class VoteSelect(nextcord.ui.StringSelect):
|
||||
def __init__(self, options: List["SelectOption"], config: DakinyConfig) -> None:
|
||||
super().__init__(
|
||||
placeholder="Choose 1",
|
||||
min_values=1,
|
||||
max_values=1,
|
||||
options=options,
|
||||
)
|
||||
self.config = config
|
||||
|
||||
async def callback(self, interaction: Interaction):
|
||||
election_name = get_election_cycle_name(self.config)
|
||||
with Session(engine) as session:
|
||||
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
|
||||
ElectionInformation.ElectionCycle == election_name
|
||||
)
|
||||
vote_msg_id = session.scalar(stmt)
|
||||
|
||||
if self.values[0] == "Abstain":
|
||||
voted_for = None
|
||||
else:
|
||||
voted_for = int(self.values[0])
|
||||
|
||||
new_vote = Votes(
|
||||
ElectionCycle=election_name,
|
||||
VoterId=interaction.user.id,
|
||||
VotedForId=voted_for,
|
||||
)
|
||||
|
||||
try:
|
||||
session.add(new_vote)
|
||||
session.commit()
|
||||
|
||||
except exc.IntegrityError:
|
||||
session.rollback()
|
||||
stmt = (
|
||||
update(Votes)
|
||||
.where(Votes.ElectionCycle == election_name)
|
||||
.where(Votes.VoterId == interaction.user.id)
|
||||
.values(VotedForId=voted_for)
|
||||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
|
||||
vote_message_str = build_votes_str(self.config)
|
||||
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
|
||||
vote_msg_id
|
||||
)
|
||||
await vote_msg.edit(content=vote_message_str)
|
||||
|
||||
if voted_for is not None:
|
||||
msg = f"You have voted for <@{voted_for}>"
|
||||
else:
|
||||
msg = "You have abstained from voting."
|
||||
|
||||
await interaction.edit(content=msg, view=None)
|
||||
|
||||
|
||||
class VoteView(nextcord.ui.View):
|
||||
"""
|
||||
The button to press to start your nomination.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
|
||||
super().__init__(timeout=None)
|
||||
self.config = config
|
||||
self.guild = guild
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote"
|
||||
)
|
||||
async def vote(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
) -> None:
|
||||
"""
|
||||
Enforces only members vote. Gets election cycle, creates voting options (checking if user has voted already),
|
||||
then sends the voting view.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
|
||||
interaction (nextcord.Interaction): Interaction of user who pressed the button
|
||||
"""
|
||||
|
||||
# enforce members only
|
||||
if (
|
||||
self.guild.get_role(self.config.params.clan_member_role_id)
|
||||
not in interaction.user.roles
|
||||
):
|
||||
await interaction.response.send_message(
|
||||
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
election: str = get_election_cycle_name(self.config)
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(Votes.VotedForId)
|
||||
.where(Votes.ElectionCycle == election)
|
||||
.where(Votes.VoterId == interaction.user.id)
|
||||
)
|
||||
voted_for = session.scalar(stmt)
|
||||
|
||||
stmt = select(CouncilMembers.MemberId).where(
|
||||
CouncilMembers.ElectionCycle == election
|
||||
)
|
||||
|
||||
options = [
|
||||
SelectOption(
|
||||
label=f"{self.guild.get_member(nominee).display_name} ☑️"
|
||||
if voted_for == nominee
|
||||
else f"{self.guild.get_member(nominee).display_name}",
|
||||
value=str(nominee),
|
||||
)
|
||||
for nominee in session.scalars(stmt)
|
||||
]
|
||||
options.append(
|
||||
SelectOption(
|
||||
label="Abstain ☑️" if voted_for is None else "Abstain",
|
||||
value="Abstain",
|
||||
)
|
||||
)
|
||||
|
||||
view = nextcord.ui.View()
|
||||
view.add_item(VoteSelect(options=options, config=self.config))
|
||||
|
||||
await interaction.response.send_message(
|
||||
content="Please vote for a clan member!",
|
||||
view=view,
|
||||
ephemeral=True,
|
||||
)
|
||||
@@ -4,7 +4,10 @@ Cog loader. Adds all command cogs to the bot.
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from cogs.commands.request import RequestCog
|
||||
from cogs.commands.set import setCog
|
||||
from cogs.commands.test import TestCog
|
||||
from cogs.commands.view import ViewCog
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
@@ -13,4 +16,8 @@ def setup(bot: commands.Bot):
|
||||
"""
|
||||
Adds all command classes to bot.
|
||||
"""
|
||||
|
||||
bot.add_cog(RequestCog(bot))
|
||||
bot.add_cog(TestCog(bot))
|
||||
bot.add_cog(setCog())
|
||||
bot.add_cog(ViewCog())
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Contains commands and views for request commands.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
from config import DakinyConfig
|
||||
|
||||
|
||||
class RequestCog(commands.Cog, name="Request Commands"):
|
||||
"""
|
||||
Class holds request commands.
|
||||
"""
|
||||
|
||||
def __init__(self, bot) -> None:
|
||||
"""
|
||||
Default constructor for RequestCog class. Does nothing.
|
||||
"""
|
||||
self.bot: commands.Bot = bot
|
||||
|
||||
@nextcord.slash_command()
|
||||
async def request(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
Parent slash command for help. Never called.
|
||||
"""
|
||||
|
||||
@request.subcommand(description="Request to change your Dakiny nickname")
|
||||
async def change_nickname(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
new_nickname: str = nextcord.SlashOption(
|
||||
required=True, min_length=2, max_length=32, description="Your requested new nickname."
|
||||
),
|
||||
):
|
||||
"""
|
||||
Sends a message to admins to request a nickname change.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
new_username(str): The username to change to.
|
||||
"""
|
||||
|
||||
config: DakinyConfig = self.bot.config
|
||||
content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."'
|
||||
view = UsernameRequest(interaction.user.id, new_nickname)
|
||||
await self.bot.get_channel(config.params.admin_channel_id).send(content=content, view=view)
|
||||
await interaction.response.send_message("Request sent to admins.", ephemeral=True)
|
||||
|
||||
|
||||
class UsernameRequest(nextcord.ui.View):
|
||||
"""
|
||||
View provides approve and deny buttons and action for a username change request.
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: int, new_nickname: str, *, timeout: float | None = 180, auto_defer: bool = True):
|
||||
super().__init__(timeout=timeout, auto_defer=auto_defer)
|
||||
self.user_id = user_id
|
||||
self.new_nickname = new_nickname
|
||||
|
||||
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
|
||||
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
|
||||
"""
|
||||
Approve button for a nickname change request. Changes the target's nickname to self.new_nickname.
|
||||
Catches an error if the target as a higher role than the bot.
|
||||
Removes the view from the interaction message.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused.
|
||||
interaction (nextcord.Interaction): The interaction of the user clicking the button.
|
||||
"""
|
||||
user = interaction.guild.get_member(self.user_id)
|
||||
if user is not None:
|
||||
try:
|
||||
await user.edit(nick=self.new_nickname)
|
||||
content = interaction.message.content + f"\nApproved by <@{interaction.user.id}>."
|
||||
await interaction.response.edit_message(content=content, view=None)
|
||||
except nextcord.errors.Forbidden:
|
||||
await interaction.response.send_message(
|
||||
content="Please change manually.",
|
||||
ephemeral=True,
|
||||
)
|
||||
content = (
|
||||
interaction.message.content
|
||||
+ "\n\nFailed. User has too high permissions for bot to change nickname."
|
||||
)
|
||||
await interaction.followup.edit_message(interaction.message.id, content=content, view=None)
|
||||
else:
|
||||
await interaction.response.edit_message(
|
||||
content="An error occured while changing the user's nickname.", view=None
|
||||
)
|
||||
|
||||
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
|
||||
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
|
||||
"""
|
||||
Deny button for a nickname request. Just removes the view and logs who denied the request.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): Unused.
|
||||
interaction (nextcord.Interaction): The interaction of the user clicking the button.
|
||||
"""
|
||||
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
|
||||
await interaction.response.edit_message(content=content, view=None)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Contains commands and views for set commands.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dakiny_bot import birthday_list
|
||||
from database_setup.sql_data_classes import UserBirthday, engine
|
||||
|
||||
|
||||
class setCog(commands.Cog, name="Set Commands"):
|
||||
"""
|
||||
Class holds set commands.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Default constructor for SetCog class. Does nothing.
|
||||
"""
|
||||
|
||||
@nextcord.slash_command()
|
||||
async def set(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
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.")
|
||||
async def birthday(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
month: int = nextcord.SlashOption(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.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
month (int, optional): The month a user was born. Is SlashOption.
|
||||
day (int, optional): The day a user was born. Is SlashOption.
|
||||
"""
|
||||
|
||||
# Must be deferred, apparently datetime takes a little bit on invalid dates.
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
if month == 0 and day == 0:
|
||||
stmt = delete(UserBirthday).where(UserBirthday.UserId == interaction.user.id)
|
||||
with Session(engine) as session:
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
await interaction.followup.send("Removed birthday.", ephemeral=True)
|
||||
return
|
||||
|
||||
# check if valid day
|
||||
today = datetime.today()
|
||||
try:
|
||||
birthday = datetime(today.year, month, day)
|
||||
except ValueError:
|
||||
await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True)
|
||||
return
|
||||
|
||||
# If the birthday is today, add to birthday_list
|
||||
if today.date() == birthday.date():
|
||||
birthday_list.append(interaction.user.id)
|
||||
|
||||
new_birthday = UserBirthday(UserId=interaction.user.id, Birthday=f"{month}/{day}")
|
||||
with Session(engine) as session:
|
||||
try:
|
||||
session.add(new_birthday)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
# User already has birthday, change it instead.
|
||||
session.rollback()
|
||||
stmt = select(UserBirthday).where(UserBirthday.UserId == interaction.user.id)
|
||||
new_birthday = session.scalar(stmt)
|
||||
new_birthday.Birthday = f"{month}/{day}"
|
||||
session.commit()
|
||||
|
||||
await interaction.followup.send(f"Birthday set to {month}/{day}.", ephemeral=True)
|
||||
+106
-20
@@ -3,9 +3,22 @@ Commands for testing.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from nextcord.ext import application_checks, commands
|
||||
|
||||
from classes.election_cycle import ElectionCycle
|
||||
from config import DakinyConfig
|
||||
from utils import get_current_cycle
|
||||
|
||||
|
||||
def is_me():
|
||||
"""
|
||||
predicate that makes sure only I can run the test commands.
|
||||
"""
|
||||
|
||||
def predicate(interaction: nextcord.Interaction):
|
||||
return interaction.user.id == 206602713511886849
|
||||
|
||||
return application_checks.check(predicate)
|
||||
|
||||
|
||||
class TestCog(commands.Cog, name="Test commands."):
|
||||
@@ -19,30 +32,14 @@ class TestCog(commands.Cog, name="Test commands."):
|
||||
"""
|
||||
self.bot = bot
|
||||
|
||||
@nextcord.slash_command()
|
||||
@nextcord.slash_command(guild_ids=[1000247361639030804])
|
||||
async def test(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
Parent slash command for help. Never called.
|
||||
"""
|
||||
|
||||
@test.subcommand()
|
||||
async def msg_builder(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
Tests the message builder.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
"""
|
||||
|
||||
cycle = ElectionCycle(0, self.bot)
|
||||
|
||||
message = cycle.build_election_announcement()
|
||||
|
||||
# doesn't tell discord that the message was responded to. Is a test command so I don't really care atm.
|
||||
for m in message:
|
||||
await interaction.channel.send(content=m)
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def election_start(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
idc
|
||||
@@ -51,5 +48,94 @@ class TestCog(commands.Cog, name="Test commands."):
|
||||
interaction (nextcord.Interaction): _description_
|
||||
"""
|
||||
|
||||
cycle = ElectionCycle(0, self.bot)
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.election_start_action()
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def voting_start(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
tests the voting start action
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): _description_
|
||||
"""
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.voting_start_action()
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def voting_end(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
tests the voting start action
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): _description_
|
||||
"""
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.voting_end_action()
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def highcouncil_vote(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
tests the voting start action
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): _description_
|
||||
"""
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.highcouncil_election_start_action()
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def election_reminder(self, interaction: nextcord.Interaction):
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.election_reminder()
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def daily(self, interaction: nextcord.Interaction):
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.check_date()
|
||||
|
||||
@test.subcommand()
|
||||
@is_me()
|
||||
async def term_start(self, interaction: nextcord.Interaction):
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = ElectionCycle(
|
||||
get_current_cycle(config),
|
||||
config,
|
||||
self.bot.get_guild(config.params.guild_id),
|
||||
)
|
||||
await cycle.term_start_action()
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Commands for viewing information in the database.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database_setup.sql_data_classes import ElectionInformation, Votes, engine
|
||||
|
||||
|
||||
class ViewCog(commands.Cog, name="View Commands"):
|
||||
"""
|
||||
Holds the view commands.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Default constructor for ViewCog class. Does nothing.
|
||||
"""
|
||||
|
||||
@nextcord.slash_command()
|
||||
async def view(self, interaction: nextcord.Interaction):
|
||||
"""
|
||||
Parent slash command for set. Never called.
|
||||
"""
|
||||
|
||||
@view.subcommand(
|
||||
description="Shows all voters and who they voted for in a given election."
|
||||
)
|
||||
async def votes(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
election_name: str = nextcord.SlashOption(
|
||||
required=True, description="The election you want to view votes for."
|
||||
),
|
||||
):
|
||||
"""
|
||||
Takes in an election name and sends a response message to the user with a list of people who voted in that
|
||||
election and who they voted for. Notes those tho abstained. Tells a user if the election is invalid. Uses
|
||||
autocompletion to assist with getting elections.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): User's interaction with the command.
|
||||
election_name (str): Is SlashOption. The name of the election to view votes for.
|
||||
"""
|
||||
|
||||
stmt = select(Votes).where(Votes.ElectionCycle == election_name)
|
||||
|
||||
with Session(engine) as session:
|
||||
# Check if election exists.
|
||||
if session.scalar(stmt) is None:
|
||||
await interaction.response.send_message(
|
||||
f"Invalid election: {election_name}.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
votes = session.scalars(stmt)
|
||||
content = ""
|
||||
for vote in votes:
|
||||
content += (
|
||||
f"<@{vote.VoterId}> voted for <@{vote.VotedForId}>.\n"
|
||||
if vote.VotedForId is not None
|
||||
else f"<@{vote.VoterId}> abstained from voted.\n"
|
||||
)
|
||||
|
||||
await interaction.response.send_message(content=content, ephemeral=True)
|
||||
|
||||
@votes.on_autocomplete("election_name")
|
||||
async def election_autocomplete(
|
||||
self, interaction: nextcord.Interaction, election_name: str
|
||||
):
|
||||
"""
|
||||
Collects all elections that start with the election string.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction with command.
|
||||
election_name (str): String to run autocomplete on.
|
||||
"""
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = select(ElectionInformation.ElectionCycle)
|
||||
elections = session.scalars(stmt)
|
||||
|
||||
# if empty compare string
|
||||
if not election_name:
|
||||
await interaction.response.send_autocomplete(elections)
|
||||
return
|
||||
|
||||
autofilled_elections: "list[str]" = [
|
||||
election
|
||||
for election in elections
|
||||
if election.lower().startswith(election_name.lower())
|
||||
]
|
||||
await interaction.response.send_autocomplete(autofilled_elections[:25])
|
||||
+100
-17
@@ -1,13 +1,21 @@
|
||||
"""
|
||||
_summary_
|
||||
Defines the daily loop that runs the Dakiny bot functions.
|
||||
"""
|
||||
|
||||
from datetime import time
|
||||
import random
|
||||
from datetime import datetime, time
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands, tasks
|
||||
from pytz import timezone
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from classes.election_cycle import ElectionCycle
|
||||
from config import DakinyConfig
|
||||
from dakiny_bot import birthday_list
|
||||
from database_setup.sql_data_classes import UserBirthday, engine
|
||||
from utils import get_current_cycle
|
||||
|
||||
|
||||
class DailyLoop(commands.Cog, name="Daily Loop"):
|
||||
@@ -17,21 +25,96 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
|
||||
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
# TODO need to figure out how to determine the current cycle
|
||||
self.current_election = ElectionCycle(0, self.bot)
|
||||
self.current_election = None
|
||||
self.update_birthdays()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
"""
|
||||
Starts the loop when bot is ready.
|
||||
"""
|
||||
await self.bot.wait_until_ready()
|
||||
# pylint doesn't recognize that periodic_update_guild_data is subclassed from tasks.loop.
|
||||
await self.do_thing.start() # pylint: disable=no-member
|
||||
# pylint doesn't recognize that daily_loop is subclassed from tasks.loop.
|
||||
self.daily_loop.start() # pylint: disable=no-member
|
||||
|
||||
@tasks.loop(time=time(hour=0, tzinfo=timezone("GMT")))
|
||||
async def do_thing(self):
|
||||
""" """
|
||||
print("loop")
|
||||
await self.current_election.check_date()
|
||||
# TODO need to figure out how to change the election cycle after current one is over
|
||||
async def daily_loop(self):
|
||||
"""
|
||||
Runs daily. Checks if the current election is None. If so, checks if there is an active election. If there is
|
||||
an active election, runs the check_date() function for the election to perform actions. Finally checks if the
|
||||
election is over to delete the current election object.
|
||||
"""
|
||||
|
||||
if self.current_election is None:
|
||||
self.current_election = self.get_election_cycle()
|
||||
|
||||
if self.current_election is not None:
|
||||
await self.current_election.check_date()
|
||||
if self.current_election.election_over:
|
||||
self.current_election = None
|
||||
|
||||
self.update_birthdays()
|
||||
|
||||
if 239547993106284544 in birthday_list:
|
||||
# inform kandy that it is her "birthday"
|
||||
kandy: nextcord.User = await self.bot.fetch_user(239547993106284544)
|
||||
await kandy.send('Happy "Birthday"!')
|
||||
|
||||
await self.cakedays()
|
||||
|
||||
@daily_loop.before_loop
|
||||
async def before_loop(self):
|
||||
"""
|
||||
Actions run before the loop starts. Want to wait for bot to be ready and get the current cycle.
|
||||
"""
|
||||
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
# Bot needs to know guilds to get the current cycle.
|
||||
self.current_election = self.get_election_cycle()
|
||||
|
||||
def get_election_cycle(self) -> ElectionCycle:
|
||||
"""
|
||||
Creates an object for the current election cycle if one is active. An election is considered active
|
||||
between the election start and the term start.
|
||||
|
||||
Returns:
|
||||
ElectionCycle: Current election cycle, None if there is no election happening.
|
||||
"""
|
||||
|
||||
config: DakinyConfig = self.bot.config
|
||||
cycle = get_current_cycle(config)
|
||||
|
||||
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
|
||||
|
||||
def update_birthdays(self) -> None:
|
||||
"""
|
||||
Goes through database to collect all birthdays that are today. Adds user's id to birthday list.
|
||||
"""
|
||||
birthday_list.clear()
|
||||
|
||||
# kandy's 1% chance
|
||||
if random.randint(0, 99) == 0:
|
||||
birthday_list.append(239547993106284544)
|
||||
|
||||
today = datetime.today()
|
||||
birthday = f"{today.month}/{today.day}"
|
||||
|
||||
stmt = select(UserBirthday.UserId).where(UserBirthday.Birthday == birthday)
|
||||
with Session(engine) as session:
|
||||
to_add = session.scalars(stmt)
|
||||
birthday_list.extend(to_add.all())
|
||||
|
||||
async def cakedays(self) -> None:
|
||||
"""
|
||||
Loops through the guild's humans checking for joined_at dates that match today's date. Sends a message
|
||||
saying happy cakeday.
|
||||
"""
|
||||
|
||||
today = datetime.today()
|
||||
guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id)
|
||||
announce_channel: nextcord.TextChannel = guild.get_channel(self.bot.config.params.general_channel_id)
|
||||
|
||||
cakedays: list[nextcord.Member] = [
|
||||
human for human in guild.humans if human.joined_at.month == today.month and human.joined_at.day == today.day
|
||||
]
|
||||
|
||||
for member in cakedays:
|
||||
years = today.year - member.joined_at.year
|
||||
year_str = "years" if years > 1 else "year"
|
||||
msg = f"Happy Cake day! <@{member.id}> joined the discord {years} {year_str} ago!"
|
||||
await announce_channel.send(msg)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Defines cog for on message listener.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
from dakiny_bot import birthday_list
|
||||
|
||||
|
||||
class SubmissionListener(commands.Cog, name="Message Listener"):
|
||||
"""Sets up message listener."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: nextcord.Message):
|
||||
"""Listener for message creation. Adds score and attachments to submission."""
|
||||
# don't want to process any messages from bot.
|
||||
if message.author.id == self.bot.user.id:
|
||||
return
|
||||
|
||||
await birthday_action(message)
|
||||
|
||||
|
||||
# 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(SubmissionListener(bot))
|
||||
|
||||
|
||||
async def birthday_action(message: nextcord.Message) -> None:
|
||||
"""
|
||||
Checks a message's auther and mentions to see if they are in the birthday list.
|
||||
|
||||
Args:
|
||||
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)):
|
||||
await message.add_reaction("🎂")
|
||||
@@ -10,16 +10,17 @@ class Params:
|
||||
Class defines the various configuration parameters.
|
||||
"""
|
||||
|
||||
active_category_id: int
|
||||
annoucement_category_id: int
|
||||
archive_channel_group_id: int
|
||||
admin_channel_id: int # might want to rename this...
|
||||
announcement_category_id: int
|
||||
clan_council_elect_role_id: int
|
||||
clan_council_role_id: int
|
||||
clan_info_channel_id: int
|
||||
clan_member_role_id: int
|
||||
discord_token: str
|
||||
general_channel_id: int
|
||||
guild_id: int
|
||||
high_council_role_id: int
|
||||
voting_emote_id: str
|
||||
voting_parent_id: int
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -29,6 +30,9 @@ class ElectionCycle:
|
||||
"""
|
||||
|
||||
election_start: str
|
||||
highcouncil_election_announce: str
|
||||
highcouncil_election_end: str
|
||||
highcouncil_election_start: str
|
||||
name: str
|
||||
term_end: str
|
||||
term_start: str
|
||||
|
||||
+25
-2
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -6,11 +7,26 @@ import nextcord
|
||||
from hydra.core.config_store import ConfigStore
|
||||
from nextcord.ext import commands
|
||||
|
||||
import database_setup.create_database as create_database
|
||||
from classes.nominate_highcouncil_views import NominateHighCouncilView
|
||||
from classes.nominate_views import NominateView
|
||||
from classes.voting_highcouncil_views import VoteHighcouncilView
|
||||
from classes.voting_views import VoteView
|
||||
from config import DakinyConfig
|
||||
|
||||
cs = ConfigStore.instance()
|
||||
cs.store(name="dakiny_config", node=DakinyConfig)
|
||||
|
||||
birthday_list: list[int] = []
|
||||
|
||||
logger = logging.getLogger("nextcord")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
|
||||
handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")
|
||||
)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
@hydra.main(config_path="conf", config_name="config")
|
||||
def main(cfg: DakinyConfig):
|
||||
@@ -25,7 +41,7 @@ def main(cfg: DakinyConfig):
|
||||
# intents.guilds = True
|
||||
|
||||
# To enable the members priveleged intent:
|
||||
# intents.members = True
|
||||
intents.members = True
|
||||
|
||||
bot = commands.Bot(intents=intents)
|
||||
bot.config = cfg
|
||||
@@ -38,6 +54,11 @@ def main(cfg: DakinyConfig):
|
||||
async def on_ready():
|
||||
"""When Discord is connected"""
|
||||
assert bot.user is not None
|
||||
create_database # pylint: disable=pointless-statement
|
||||
bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id)))
|
||||
bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
|
||||
bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
|
||||
bot.add_view(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
|
||||
print(f"{bot.user.name} has connected to Discord!")
|
||||
|
||||
# Run Discord bot
|
||||
@@ -46,4 +67,6 @@ def main(cfg: DakinyConfig):
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv.append("hydra.job.chdir=False")
|
||||
main()
|
||||
sys.argv.append("hydra/job_logging=none")
|
||||
sys.argv.append("hydra/hydra_logging=none")
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Small script to create the database if it doesn't already exist.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from os.path import exists
|
||||
|
||||
if not exists("database/dakiny.db"):
|
||||
with sqlite3.connect("database/dakiny.db") as con:
|
||||
cur = con.cursor()
|
||||
|
||||
with open("database_setup/database.sql", mode="r", encoding="utf-8") as file:
|
||||
database_script = file.read()
|
||||
cur.executescript(database_script)
|
||||
con.commit()
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE ElectionInformation(
|
||||
ElectionCycle varchar(30) not null unique,
|
||||
ThreadId bigint unsigned not null,
|
||||
CouncilVoteMsgId bigint unsigned,
|
||||
HighCouncilVoteMsgId bigint unsigned,
|
||||
CouncilNominationMsgId bigint unsigned,
|
||||
HighCouncilNominationMsgId bigint unsigned,
|
||||
PRIMARY KEY (ElectionCycle)
|
||||
);
|
||||
|
||||
CREATE TABLE CouncilMembers(
|
||||
ElectionCycle varchar(30) not null,
|
||||
MemberId bigint unsigned not null,
|
||||
SpeechMsgId bigint unsigned not null,
|
||||
Elected boolean DEFAULT false,
|
||||
DiscordManager boolean DEFAULT false,
|
||||
ClanLeader boolean DEFAULT false,
|
||||
DiscordManagerSpeechId bigint unsigned,
|
||||
ClanLeaderSpeechId bigint unsigned,
|
||||
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
|
||||
PRIMARY KEY (ElectionCycle, MemberId)
|
||||
);
|
||||
|
||||
CREATE TABLE Votes(
|
||||
ElectionCycle varchar(30) not null,
|
||||
VoterId bigint unsigned not null,
|
||||
VotedForId bigint unsigned,
|
||||
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
|
||||
PRIMARY KEY (ElectionCycle, VoterId)
|
||||
);
|
||||
|
||||
CREATE TABLE UserBirthday (
|
||||
UserId bigint unsigned not null,
|
||||
Birthday varchar(5) not null,
|
||||
PRIMARY KEY (UserId)
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Simple file to set up the sqlalchemy connection to database.
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.automap import automap_base
|
||||
|
||||
# connect with sql alchemy and autoload schema
|
||||
URL = "sqlite+pysqlite:///database/dakiny.db"
|
||||
engine = create_engine(URL)
|
||||
Base = automap_base()
|
||||
Base.prepare(autoload_with=engine)
|
||||
|
||||
# define the classes
|
||||
ElectionInformation = Base.classes.ElectionInformation
|
||||
CouncilMembers = Base.classes.CouncilMembers
|
||||
Votes = Base.classes.Votes
|
||||
UserBirthday = Base.classes.UserBirthday
|
||||
@@ -1,3 +1,6 @@
|
||||
Discord permissions required for bot to operate:
|
||||
|
||||
manage channels
|
||||
manage channels
|
||||
manage roles
|
||||
manage nicknames
|
||||
need member intent
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
nextcord==2.4.2
|
||||
python-dotenv==1.0.0
|
||||
pytz==2022.7.1
|
||||
hydra-core==1.3.2
|
||||
hydra-core==1.3.2
|
||||
sqlalchemy<2.0
|
||||
@@ -4,6 +4,14 @@ File contains util functions.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import DakinyConfig
|
||||
from database_setup.sql_data_classes import CouncilMembers, Votes, engine
|
||||
|
||||
NOMINEE_SPLITTER = "========================================\n\n"
|
||||
|
||||
|
||||
def get_datetime(day: str) -> datetime:
|
||||
"""
|
||||
@@ -24,3 +32,233 @@ def get_datetime(day: str) -> datetime:
|
||||
year += 1
|
||||
|
||||
return datetime(year, month, day)
|
||||
|
||||
|
||||
def get_election_cycle_name(config: DakinyConfig) -> str:
|
||||
"""
|
||||
Builds the unique name of the current election cycle
|
||||
|
||||
Args:
|
||||
config (DakinyConfig): the config used to know the election cycle.
|
||||
|
||||
Returns:
|
||||
str: the name of the election cycle
|
||||
"""
|
||||
cycle = get_current_cycle(config)
|
||||
current_cycle = config.election_cycles[cycle]
|
||||
|
||||
return (
|
||||
f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}"
|
||||
)
|
||||
|
||||
|
||||
def get_dm_election_name(config: DakinyConfig) -> str:
|
||||
"""
|
||||
creates the name of the discord manager election
|
||||
|
||||
Args:
|
||||
config (DakinyConfig): Config sent to get_election_cycle_name
|
||||
|
||||
Returns:
|
||||
str: the name of the election
|
||||
"""
|
||||
|
||||
return f"{get_election_cycle_name(config)}-dm"
|
||||
|
||||
|
||||
def get_cl_election_name(config: DakinyConfig) -> str:
|
||||
"""
|
||||
creates the name of the clan leader election
|
||||
|
||||
Args:
|
||||
config (DakinyConfig): Config sent to get_election_cycle_name
|
||||
|
||||
Returns:
|
||||
str: the name of the election
|
||||
"""
|
||||
|
||||
return f"{get_election_cycle_name(config)}-cl"
|
||||
|
||||
|
||||
def get_current_cycle(config: DakinyConfig) -> int:
|
||||
"""
|
||||
Returns index for active election. An election is considered active between the election start and the term start.
|
||||
|
||||
Args:
|
||||
config (DakinyConfig): Config object to use
|
||||
|
||||
Returns:
|
||||
int: cycle index for active election. -1 for no active cycle
|
||||
"""
|
||||
|
||||
today = datetime.today()
|
||||
|
||||
for cycle, election in enumerate(config.election_cycles):
|
||||
if (
|
||||
today.date() >= get_datetime(election.election_start).date()
|
||||
and today.date() <= get_datetime(election.term_start).date()
|
||||
):
|
||||
return cycle
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def build_votes_str(config: DakinyConfig) -> str:
|
||||
"""
|
||||
Creates a string that contains all nominees and their votes to be sent as a discord message.
|
||||
|
||||
Args:
|
||||
config (DakinyConfig): Daking config object
|
||||
|
||||
Returns:
|
||||
str: String that contains all nominees and their votes.
|
||||
"""
|
||||
|
||||
votes: dict[int, int] = {}
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = select(CouncilMembers).where(
|
||||
CouncilMembers.ElectionCycle == get_election_cycle_name(config)
|
||||
)
|
||||
|
||||
for nominee in session.scalars(stmt):
|
||||
votes[nominee.MemberId] = 0
|
||||
|
||||
stmt = select(Votes).where(
|
||||
Votes.ElectionCycle == get_election_cycle_name(config)
|
||||
)
|
||||
|
||||
for vote in session.scalars(stmt):
|
||||
if vote.VotedForId is not None:
|
||||
votes[vote.VotedForId] += 1
|
||||
|
||||
votes_str: str = "===================="
|
||||
votes_str += user_vote_str(votes)
|
||||
|
||||
return votes_str
|
||||
|
||||
|
||||
def build_highcouncil_votes_str(config: DakinyConfig) -> str:
|
||||
"""
|
||||
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
|
||||
|
||||
Args:
|
||||
config (DakinyConfig): Daking config object
|
||||
|
||||
Returns:
|
||||
str: String that contains all nominees and their votes.
|
||||
"""
|
||||
|
||||
cl_votes: dict[int, int] = {}
|
||||
dm_votes: dict[int, int] = {}
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
|
||||
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
|
||||
)
|
||||
|
||||
for nominee in session.scalars(stmt):
|
||||
cl_votes[nominee.MemberId] = 0
|
||||
|
||||
stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config))
|
||||
|
||||
for vote in session.scalars(stmt):
|
||||
if vote.VotedForId is not None:
|
||||
cl_votes[vote.VotedForId] += 1
|
||||
|
||||
stmt = (
|
||||
select(CouncilMembers)
|
||||
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
|
||||
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
|
||||
)
|
||||
|
||||
for nominee in session.scalars(stmt):
|
||||
dm_votes[nominee.MemberId] = 0
|
||||
|
||||
stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config))
|
||||
|
||||
for vote in session.scalars(stmt):
|
||||
if vote.VotedForId is not None:
|
||||
dm_votes[vote.VotedForId] += 1
|
||||
|
||||
# build the votes string
|
||||
votes_str: str = "Clan Leader Votes\n===================="
|
||||
votes_str += user_vote_str(cl_votes)
|
||||
|
||||
votes_str += "\n\nDiscord Manager Votes\n===================="
|
||||
votes_str += user_vote_str(dm_votes)
|
||||
|
||||
return votes_str
|
||||
|
||||
|
||||
def user_vote_str(votes: dict[int, int]) -> str:
|
||||
"""
|
||||
Creates the string representing how many votes each user has.
|
||||
|
||||
Args:
|
||||
votes (dict[int, int]): Dict of all users and how many votes they have.
|
||||
|
||||
Returns:
|
||||
str: The string showing names and number of votes.
|
||||
"""
|
||||
ret_str: str = ""
|
||||
|
||||
for user_id, num_votes in votes.items():
|
||||
ret_str += f"\n<@{user_id}>: {'☑️' * num_votes}"
|
||||
return ret_str
|
||||
|
||||
|
||||
def decorate_nomination(user_id: int, speech: str) -> str:
|
||||
"""
|
||||
Decorates a nomination speech so that it sits within multiple speeches better.
|
||||
|
||||
Args:
|
||||
user_id (int): ID of user sending the speech
|
||||
speech (str): Speech from the user
|
||||
|
||||
Returns:
|
||||
str: Decorated speech
|
||||
"""
|
||||
msg = f"**Clan Council Nominee: <@{user_id}>**\n"
|
||||
msg += NOMINEE_SPLITTER
|
||||
msg += speech
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def decorate_cl_nomination(user_id: int, speech: str) -> str:
|
||||
"""
|
||||
Decorates a nomination speech so that it sits within multiple speeches better.
|
||||
|
||||
Args:
|
||||
user_id (int): ID of user sending the speech
|
||||
speech (str): Speech from the user
|
||||
|
||||
Returns:
|
||||
str: Decorated speech
|
||||
"""
|
||||
msg = f"**Clan Leader Nominee: <@{user_id}>**\n"
|
||||
msg += NOMINEE_SPLITTER
|
||||
msg += speech
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def decorate_dm_nomination(user_id: int, speech: str) -> str:
|
||||
"""
|
||||
Decorates a nomination speech so that it sits within multiple speeches better.
|
||||
|
||||
Args:
|
||||
user_id (int): ID of user sending the speech
|
||||
speech (str): Speech from the user
|
||||
|
||||
Returns:
|
||||
str: Decorated speech
|
||||
"""
|
||||
msg = f"**Discord Manager Nominee: <@{user_id}>**\n"
|
||||
msg += NOMINEE_SPLITTER
|
||||
msg += speech
|
||||
|
||||
return msg
|
||||
|
||||
Reference in New Issue
Block a user