Compare commits

..
5 changed files with 103 additions and 91 deletions
+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.
+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.
+55 -31
View File
@@ -5,7 +5,6 @@ Contains the election cycle class
from datetime import datetime, timedelta
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
@@ -20,7 +19,7 @@ class ElectionCycle:
the cycle.
"""
def __init__(self, cycle: int, bot: commands.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.
@@ -30,7 +29,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)
@@ -41,7 +40,8 @@ class ElectionCycle:
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.guild: nextcord.Guild = guild
self.election_over: bool = False
def build_election_announcement(self) -> list[str]:
"""
@@ -90,6 +90,22 @@ class ElectionCycle:
term_start_date=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_date=self.term_start.strftime("%B %d"),
)
def build_vote_start(self) -> str:
"""
Builds the voting start message out of template and configurations.
@@ -123,11 +139,9 @@ class ElectionCycle:
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 +152,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 +173,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 +183,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 +216,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)
+42 -10
View File
@@ -1,13 +1,15 @@
"""
_summary_
Defines the daily loop that runs the Dakiny bot functions.
"""
from datetime import time
from datetime import datetime, time
from nextcord.ext import commands, tasks
from pytz import timezone
from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from utils import get_datetime
class DailyLoop(commands.Cog, name="Daily Loop"):
@@ -17,8 +19,7 @@ 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 = self.get_current_cycle()
@commands.Cog.listener()
async def on_ready(self):
@@ -27,11 +28,42 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
"""
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
await 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
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