Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c20641041 | ||
|
|
fa3fc6bd75 | ||
|
|
271cf14697 | ||
|
|
ee2aaba421 | ||
|
|
6ae722beb9 | ||
|
|
757a753cf5 | ||
|
|
b292fc09fe | ||
|
|
7789e679ce | ||
|
|
9cae7d002a | ||
|
|
f71df0b7f4 | ||
|
|
2d2631bfac | ||
|
|
c9946c9afe | ||
|
|
c5d148ad49 | ||
|
|
198d5acea0 | ||
|
|
7e3b59ea59 | ||
|
|
babc5d95ab | ||
|
|
0eae8afa2a | ||
|
|
d79c990558 | ||
|
|
3465698b85 | ||
|
|
1731bd46a6 | ||
|
|
1b4c5780df | ||
|
|
d67940aa26 | ||
|
|
9867f91268 | ||
|
|
4763651e93 | ||
|
|
1c76e6806c | ||
|
|
5cb9be7e37 | ||
|
|
d573595114 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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}.
|
||||
@@ -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.
|
||||
+65
-26
@@ -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]:
|
||||
"""
|
||||
@@ -71,7 +71,7 @@ class ElectionCycle:
|
||||
|
||||
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.
|
||||
|
||||
@@ -82,7 +82,29 @@ class ElectionCycle:
|
||||
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_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:
|
||||
"""
|
||||
@@ -117,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:
|
||||
"""
|
||||
@@ -132,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)
|
||||
@@ -153,34 +173,39 @@ 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:
|
||||
"""
|
||||
Performs required actions for when voting time has ended.
|
||||
"""
|
||||
print("voting has ended")
|
||||
message = self.build_highcouncil_election_announce()
|
||||
council_elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id)
|
||||
election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel)
|
||||
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:
|
||||
"""
|
||||
@@ -191,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)
|
||||
|
||||
@@ -4,6 +4,7 @@ Cog loader. Adds all command cogs to the bot.
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from cogs.commands.request import RequestCog
|
||||
from cogs.commands.test import TestCog
|
||||
|
||||
|
||||
@@ -13,4 +14,5 @@ def setup(bot: commands.Bot):
|
||||
"""
|
||||
Adds all command classes to bot.
|
||||
"""
|
||||
bot.add_cog(RequestCog(bot))
|
||||
bot.add_cog(TestCog(bot))
|
||||
|
||||
@@ -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)
|
||||
+42
-10
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ class Params:
|
||||
"""
|
||||
|
||||
active_category_id: int
|
||||
admin_channel_id: int
|
||||
annoucement_category_id: int
|
||||
archive_channel_group_id: int
|
||||
clan_council_elect_role_id: int
|
||||
|
||||
+1
-1
@@ -26,7 +26,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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Discord permissions required for bot to operate:
|
||||
|
||||
manage channels
|
||||
manage roles
|
||||
manage roles
|
||||
manage nicknames
|
||||
need member intent
|
||||
Reference in New Issue
Block a user