Compare commits

...
Author SHA1 Message Date
imcompiling e686cf88e5 Be inclusive when checkign for an election cycle.
build_develop / build (push) Has been cancelled
2023-09-13 10:51:16 -04:00
imcompiling 117dea48c4 Standardize variable name of dates. 2023-09-13 10:42:01 -04:00
imcompiling 62e4417ce8 Use correct action on voting start. 2023-09-13 10:32:59 -04:00
imcompiling 672df41f19 Make the configs and timeline more clear. 2023-09-13 10:16:50 -04:00
imcompiling 328f3fea78 Merge branch 'main' into 22-daily-loop-not-starting-election 2023-09-13 09:31:53 -04:00
imcompiling 18751bc85c Inform Kandy when it is her "birthday".
build_release / build (push) Successful in 48s
build_latest / build (push) Successful in 43s
build_develop / build (push) Successful in 43s
2023-09-12 13:17:25 -04:00
imcompiling e243b40be7 Merge branch '19-birthday-tracking'
build_latest / build (push) Successful in 44s
build_develop / build (push) Successful in 42s
build_release / build (push) Successful in 42s
2023-09-09 13:48:36 -04:00
imcompiling 7a91cd37ec Add birthdays from database.
build_develop / build (push) Has been cancelled
2023-09-09 13:48:22 -04:00
imcompiling efadcb30c7 Finish up set birthday command. 2023-09-09 13:27:22 -04:00
imcompiling 5668e4be7e Add release workflow.
build_release / build (push) Successful in 44s
build_develop / build (push) Successful in 43s
build_latest / build (push) Successful in 44s
2023-09-09 11:49:56 -04:00
imcompiling 1a8c5616ef Merge branch 'main' into 19-birthday-tracking
build_develop / build (push) Successful in 1m24s
2023-09-09 11:48:47 -04:00
imcompiling 43c1a5c6cd Merge branch '22-daily-loop-not-starting-election'
build_develop / build (push) Successful in 43s
build_latest / build (push) Successful in 42s
2023-09-09 11:39:31 -04:00
imcompiling c278bae498 I forgot to give the docker image the message files.
build_develop / build (push) Successful in 1m24s
2023-09-09 11:38:22 -04:00
imcompiling 8b831fb6aa start loop in init, use before loop, waiting on bot ready, to set up needed data. 2023-09-09 11:38:03 -04:00
imcompiling e7cfe8bd4c Fix logging issue.
build_develop / build (push) Successful in 1m24s
2023-09-08 20:07:08 -04:00
imcompiling 2640ebdad1 Move current_election setting. The bot needs to be ready first.
build_develop / build (push) Successful in 1m22s
2023-09-08 11:42:34 -04:00
imcompiling c5c242de21 Correct comparison in get_current_cycle().
build_develop / build (push) Successful in 1m30s
build_latest / build (push) Successful in 47s
2023-09-06 14:14:33 -04:00
imcompiling 0710db0654 Merge branch 'main' into 19-birthday-tracking
build_develop / build (push) Successful in 1m27s
2023-09-03 21:13:14 -04:00
imcompiling 369ea0dd55 Add logging 2023-09-03 21:12:42 -04:00
imcompiling 077119acdd Add initial set birthday command. 2023-08-30 22:49:28 -04:00
imcompiling ac8d1bc681 Start update_birthdays function.
build_develop / build (push) Successful in 1m24s
2023-08-26 23:04:31 -04:00
imcompiling df8cb3dd49 Add message listener portion of birthday fun. 2023-08-26 22:52:02 -04:00
imcompiling 25833364d7 Whoops, period on wrong side of bracket.
build_latest / build (push) Successful in 42s
build_develop / build (push) Successful in 44s
2023-08-26 22:15:12 -04:00
imcompiling d73b671257 Copy utils.py into docker image. sort copying.
build_develop / build (push) Successful in 1m25s
build_latest / build (push) Successful in 44s
2023-08-26 21:39:07 -04:00
imcompiling f6e827fba5 Rename a directory, need to store the db file in a directory. 2023-08-26 21:36:18 -04:00
imcompiling 2991f81e04 Update python version.
build_develop / build (push) Successful in 2m4s
build_latest / build (push) Successful in 42s
2023-08-26 21:02:22 -04:00
imcompiling b1c0b7013d Update dockerfile to include database.
build_develop / build (push) Successful in 1m45s
build_latest / build (push) Successful in 42s
2023-08-26 20:52:31 -04:00
imcompiling 22ea29e8ea Merge branch '10-name-change-request'
build_develop / build (push) Successful in 42s
build_latest / build (push) Successful in 42s
2023-07-29 21:13:59 -04:00
imcompiling 8c20641041 Added doc strings for the approve and deny buttons.
build_develop / build (push) Successful in 1m24s
2023-07-29 21:13:14 -04:00
imcompiling fa3fc6bd75 Added logic to handle bot being unable to change a nickname due to role hierarchy. 2023-07-29 21:08:16 -04:00
imcompiling 271cf14697 Need to be able to manage nicknames. 2023-07-29 19:21:11 -04:00
imcompiling ee2aaba421 Must await the edit. 2023-07-29 19:16:59 -04:00
imcompiling 6ae722beb9 Need member intent. 2023-07-29 19:16:47 -04:00
imcompiling 757a753cf5 I want to edit the message, not send a new one. 2023-07-29 19:05:52 -04:00
imcompiling b292fc09fe Log into gitea registry.
build_develop / build (push) Successful in 1m41s
2023-07-29 18:30:05 -04:00
imcompiling 7789e679ce Flush out view with approve and deny actions. 2023-07-29 18:18:31 -04:00
imcompiling 9cae7d002a Create initial command action. 2023-07-29 16:35:03 -04:00
imcompiling f71df0b7f4 Add the admin channel id to config. 2023-07-29 15:41:12 -04:00
imcompiling 2d2631bfac Create request class. 2023-07-29 15:39:53 -04:00
imcompiling f9d6227db1 Add the birthday table. 2023-07-29 15:16:23 -04:00
imcompiling c9946c9afe Merge branch '20-finish-daily-loop' 2023-07-26 16:27:01 -04:00
imcompiling c5d148ad49 Daily loop is complete. Properly maintains the current election. 2023-07-26 16:24:57 -04:00
imcompiling 198d5acea0 Add an assumption to readme. 2023-07-25 16:06:27 -04:00
imcompiling 7e3b59ea59 clean up election_cycle constructor. 2023-07-25 16:00:49 -04:00
imcompiling babc5d95ab Merge branch '15-term-start-action' 2023-07-25 15:30:45 -04:00
imcompiling 0eae8afa2a Fill out term_start_action 2023-07-25 15:25:35 -04:00
imcompiling d79c990558 Remove term end action. 2023-07-25 15:11:33 -04:00
imcompiling 3465698b85 Merge branch '14-high-council-election-action' 2023-07-25 14:52:57 -04:00
imcompiling 1731bd46a6 Fill out highcouncil_election_start_action
build_develop / build (push) Has been cancelled
2023-07-25 14:52:48 -04:00
imcompiling 1b4c5780df Refactor other function to use get_election_info. 2023-07-25 14:51:17 -04:00
imcompiling d67940aa26 Create new function for getting election information. 2023-07-25 14:50:44 -04:00
imcompiling 9867f91268 Add the highcouncil vote start message. 2023-07-25 14:41:16 -04:00
imcompiling 4763651e93 Merge branch '13-voting-end-action' 2023-07-25 14:38:50 -04:00
22 changed files with 489 additions and 137 deletions
+6
View File
@@ -12,6 +12,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:develop .
- name: Push Docker Image
+6
View File
@@ -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
+28
View File
@@ -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}}
+1 -1
View File
@@ -139,4 +139,4 @@ TODO.txt
sql_tester.py
conf
outputs
dakiny.db
database
+8 -3
View File
@@ -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
+4 -49
View File
@@ -1,50 +1,5 @@
# Discord Bot Template
# Dakiny Bot
[![Discord](https://img.shields.io/discord/819650821314052106?color=7289DA&logo=discord&logoColor=white)](https://discord.gg/fPrdqh3Zfu "Dev Pro Tips Discussion & Support Server")
[![Powered by Nextcord](https://custom-icon-badges.herokuapp.com/badge/-Powered%20by%20Nextcord-0d1620?logo=nextcord)](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.
+8 -8
View File
@@ -34,16 +34,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!
**
+1 -1
View File
@@ -10,4 +10,4 @@ From {highcouncil_election_announce} to {highcouncil_election_start}, you will b
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.
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 the {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
+1
View File
@@ -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 -1
View File
@@ -2,7 +2,7 @@
<@&{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}.
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.
+71 -46
View File
@@ -2,15 +2,14 @@
Contains the election cycle class
"""
from datetime import datetime, timedelta
from datetime import datetime
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database.sql_data_classes import ElectionInformation, engine
from database_setup.sql_data_classes import ElectionInformation, engine
from utils import get_datetime
@@ -20,7 +19,8 @@ class ElectionCycle:
the cycle.
"""
def __init__(self, cycle: int, bot: commands.Bot) -> None:
def __init__(self, cycle: int, config: DakinyConfig, guild: nextcord.Guild) -> None:
# TODO update
"""
Generates all dates relevant to the election cycle from input election start, term start and end dates.
@@ -30,7 +30,7 @@ class ElectionCycle:
term_end (datetime): Date the term being elected ends.
bot: the discord bot instance
"""
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)
@@ -38,10 +38,11 @@ class ElectionCycle:
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.guild: nextcord.Guild = bot.get_guild(self.config.params.guild_id)
self.highcouncil_election_announce: datetime = get_datetime(self.elecion_cycle.highcouncil_election_announce)
self.highcouncil_election_start: datetime = get_datetime(self.elecion_cycle.highcouncil_election_start)
self.highcouncil_election_end: datetime = get_datetime(self.elecion_cycle.highcouncil_election_end)
self.guild: nextcord.Guild = guild
self.election_over: bool = False
def build_election_announcement(self) -> list[str]:
"""
@@ -60,13 +61,13 @@ class ElectionCycle:
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---")
@@ -87,7 +88,23 @@ class ElectionCycle:
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_date=self.term_start.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:
@@ -105,7 +122,7 @@ class ElectionCycle:
return message.format(
clan_member_id=self.config.params.clan_member_role_id,
voting_emote_id=self.config.params.voting_emote_id,
voting_end_date=vote_end.strftime("%B %d"),
voting_end=vote_end.strftime("%B %d"),
voting_end_day=vote_end.strftime("%A"),
)
@@ -119,15 +136,13 @@ class ElectionCycle:
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 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()
async def election_start_action(self) -> None:
"""
@@ -138,7 +153,7 @@ class ElectionCycle:
quarter_name: str = self.get_election_cycle_name()
# create new entry into database.
election_channel = await category.create_text_channel(quarter_name)
election_channel: nextcord.TextChannel = await category.create_text_channel(quarter_name)
with Session(engine) as session:
new_cycle = ElectionInformation(ElectionCycle=quarter_name, ElectionChannel=election_channel.id)
session.add(new_cycle)
@@ -159,14 +174,9 @@ class ElectionCycle:
"""
message = self.build_vote_start()
clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == self.get_election_cycle_name()
)
election_info = session.scalar(stmt)
election_channel = self.guild.get_channel(election_info.ElectionChannel)
await election_channel.send(message)
await election_channel.set_permissions(clan_member_role, send_messages=False)
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
await election_channel.send(message)
await election_channel.set_permissions(clan_member_role, send_messages=False)
async def voting_end_action(self) -> None:
"""
@@ -174,28 +184,29 @@ class ElectionCycle:
"""
message = self.build_highcouncil_election_announce()
council_elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == self.get_election_cycle_name()
)
election_info = session.scalar(stmt)
election_channel = self.guild.get_channel(election_info.ElectionChannel)
await election_channel.send(message)
await election_channel.set_permissions(council_elect_role, send_messages=True)
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
await election_channel.send(message)
await election_channel.set_permissions(council_elect_role, send_messages=True)
async def highcouncil_election_action(self) -> None:
async def highcouncil_election_start_action(self) -> None:
"""
Performs required actions for when the highcouncil is elected.
"""
print("Highcouncil election has started.")
message = self.build_highcouncil_vote_start()
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
await election_channel.send(message)
async def term_start_action(self) -> None:
msg: str = "the term has started"
print(msg)
"""
Moves the election channel into the archive category.
"""
async def term_end_action(self):
msg: str = "the term has ended"
print(msg)
self.election_over = True
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
archive: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.archive_channel_group_id)
await election_channel.move(beginning=True, category=archive)
def get_election_cycle_name(self) -> str:
"""
@@ -206,3 +217,17 @@ class ElectionCycle:
"""
return f"{self.election_start.year} {self.elecion_cycle.name} election"
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 == self.get_election_cycle_name()
)
return session.scalar(stmt)
+5
View File
@@ -4,6 +4,8 @@ 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
@@ -13,4 +15,7 @@ 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())
+103
View File
@@ -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)
+85
View File
@@ -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)
+86 -17
View File
@@ -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_datetime
class DailyLoop(commands.Cog, name="Daily Loop"):
@@ -17,21 +25,82 @@ 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_current_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"!')
@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_current_cycle()
def get_current_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.
"""
today = datetime.today()
config: DakinyConfig = self.bot.config
current_election = None
for cycle, election in enumerate(config.election_cycles):
if (
today.date() >= get_datetime(election.election_start).date()
and today.date() <= get_datetime(election.term_start).date()
):
current_election = ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
return current_election
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())
+43
View File
@@ -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("🎂")
+4
View File
@@ -11,6 +11,7 @@ class Params:
"""
active_category_id: int
admin_channel_id: int # might want to rename this...
annoucement_category_id: int
archive_channel_group_id: int
clan_council_elect_role_id: int
@@ -30,6 +31,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
+14 -3
View File
@@ -1,3 +1,4 @@
import logging
import os
import sys
@@ -6,12 +7,20 @@ import nextcord
from hydra.core.config_store import ConfigStore
from nextcord.ext import commands
import database.create_database as create_database
import database_setup.create_database as create_database
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):
@@ -26,7 +35,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
@@ -48,4 +57,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
@@ -5,11 +5,11 @@ Small script to create the database if it doesn't already exist.
import sqlite3
from os.path import exists
if not exists("dakiny.db"):
with sqlite3.connect("dakiny.db") as con:
if not exists("database/dakiny.db"):
with sqlite3.connect("database/dakiny.db") as con:
cur = con.cursor()
with open("database/database.sql", mode="r", encoding="utf-8") as file:
with open("database_setup/database.sql", mode="r", encoding="utf-8") as file:
database_script = file.read()
cur.executescript(database_script)
con.commit()
@@ -1,6 +1,3 @@
-- CREATE DATABASE dakiny;
-- use dakiny;
CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique,
ElectionChannel bigint unsigned not null,
@@ -24,4 +21,10 @@ CREATE TABLE Votes(
VotedForId bigint unsigned not null,
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)
);
@@ -5,7 +5,7 @@ from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
# connect with sql alchemy and autoload schema
URL = "sqlite+pysqlite:///dakiny.db"
URL = "sqlite+pysqlite:///database/dakiny.db"
engine = create_engine(URL)
Base = automap_base()
Base.prepare(autoload_with=engine)
@@ -14,3 +14,4 @@ Base.prepare(autoload_with=engine)
ElectionInformation = Base.classes.ElectionInformation
CouncilMembers = Base.classes.CouncilMembers
Votes = Base.classes.Votes
UserBirthday = Base.classes.UserBirthday
+3 -1
View File
@@ -1,4 +1,6 @@
Discord permissions required for bot to operate:
manage channels
manage roles
manage roles
manage nicknames
need member intent