Files
raid-callouts/src/py/bot_core.py
2025-10-17 11:08:19 -06:00

339 lines
14 KiB
Python

# pylint: disable=consider-using-with, no-member
"""
This module is the listener to discord.
This module will listen to the discord server for two things:
1. the /schedule command -- which will report the current callouts for the next X days, where X is either supplied or is the default of 7
2. the /callout command -- which will allow users to add a new scheduled callout
3. a /ping command, to test the bot's current status!
4. a /registercharacter command, to allow users to register their character's name independently of their server nickname
5. a /checkcharname command, to allow users to verify their character's name
6. a /remove_callout command, to allow users to remove callouts that are no longer necessary
7. a /help command, to direct users to the github for this bot!
@author: Gabriella 'contrastellar' Agathon
"""
import datetime
import argparse
import os
import discord
import psycopg2
from discord.ext import commands
import helper.db_helper
# module constants
DAYS_FOR_CALLOUTS = 7
CONTRASTELLAR = 181187505448681472
DATABASE_CONN: helper.db_helper.DBHelper = None
# psycopg2 'imports'
UNIQUEVIOLATION: psycopg2.Error = psycopg2.errors.UniqueViolation
INVALIDDATETIMEFORMAT: psycopg2.Error = psycopg2.errors.InvalidDatetimeFormat
FOREIGNKEYVIOLATION: psycopg2.Error = psycopg2.errors.ForeignKeyViolation
# discord variables
intents = discord.Intents.default()
intents.message_content = True
intents.guild_messages = True
intents.presences = False
# client declaration
client = commands.Bot(command_prefix='!', intents=intents)
# parser declaration
parser: argparse.ArgumentParser = argparse.ArgumentParser(prog='callouts core',
description='The listener for the callouts bot functionality')
parser.add_argument('database')
parser.add_argument('token')
parser.add_argument('guild_id', type=int)
parser.add_argument('channel_id', type=int)
# utility methods
def cleanup_invalidate() -> None:
DATABASE_CONN.is_procedure_queued = False
def delete_invalidate() -> None:
DATABASE_CONN.is_unregister_queued = False
# discord commands
@client.event
async def on_ready() -> None:
await client.tree.sync()
print(f'{client.user} has connected to Discord!')
print(args.guild_id)
if 'RAID_CALLOUTS_DEV' in os.environ:
return
guild: discord.Guild = client.get_guild(args.guild_id)
channel: discord.TextChannel = guild.get_channel(args.channel_id)
output = f'The bot is now running!\nPlease message <@{CONTRASTELLAR}> with any errors!'
await channel.send(output)
return
@client.event
async def on_error(interaction: discord.Interaction) -> None:
delete_invalidate()
cleanup_invalidate()
output = "Something awful has happened. In all honesty you should never see this message. Reporting to <@{CONTRASTELLAR}>."
await interaction.response.send_message(output)
return
# === slash commands are below here
@client.tree.command()
async def registercharacter(interaction: discord.Interaction, character_name: str) -> None:
delete_invalidate()
cleanup_invalidate()
user_id = interaction.user.id
user_nick = interaction.user.display_name
try:
DATABASE_CONN.register_char_name(user_id, character_name)
except psycopg2.Error as e:
char_name = DATABASE_CONN.return_char_name(user_id)
await interaction.response.send_message(f'User {char_name} -- you have already registered a character!\n{e}')
else:
await interaction.response.send_message(f'{user_nick} -- you have registered your discord account with {character_name}!')
return
@client.tree.command()
async def check_char_name(interaction: discord.Interaction) -> None:
delete_invalidate()
cleanup_invalidate()
charname: str = DATABASE_CONN.return_char_name(interaction.user.id)
if charname == "":
await interaction.response.send_message("You have not registered! Please do with `/registercharacter`")
return
if interaction.user.id == 151162055142014976:
await interaction.response.send_message("You are: " + charname + "... in case you forgot.")
return
await interaction.response.send_message("You are: " + charname)
return
@client.tree.command()
async def remove_registration(interaction: discord.Interaction) -> None:
delete_invalidate()
cleanup_invalidate()
await interaction.response.send_message("To remove your registration with the boss, please run the `/confirm_unregister` command\nPlease be aware that this will also remove all of your callouts from the bot! ***This is in an irreversable action!***")
DATABASE_CONN.is_unregister_queued = True
return
@client.tree.command()
async def validate_unregister(interaction: discord.Interaction) -> None:
cleanup_invalidate()
user_id = interaction.user.id
user_nick = interaction.user.nick
await interaction.response.defer(thinking=True)
print(f"Removing {user_id} from the database!")
DATABASE_CONN.remove_registration(user_id, DATABASE_CONN.is_unregister_queued)
await interaction.followup.send(f"{user_nick}, you have been unregistered!")
delete_invalidate()
@client.tree.command()
async def invalidate_unregister(interaction: discord.Interaction) -> None:
cleanup_invalidate()
delete_invalidate()
print("User deletion has been invalidated! Aborting process!")
await interaction.response.send_message("Unregister has been invalidated!")
return
@client.tree.command()
async def ping(interaction: discord.Interaction) -> None:
delete_invalidate()
cleanup_invalidate()
user_id = interaction.user.id
await interaction.response.send_message(f'Pong! {user_id} -- the bot is active, please message contrastellar with issues!')
return
@client.tree.command()
async def cleanup(interaction: discord.Interaction) -> None:
delete_invalidate()
cleanup_invalidate()
number_to_be_affected: int = DATABASE_CONN.number_affected_in_cleanup()
await interaction.response.send_message(f"Is the bot being weird or slow? You can try the `/validate_cleanup` command to clear out old database entries!\nBe warned that this is an admin-level command, and may have unintended side effects!\n{number_to_be_affected} rows will be affected by the `/validate_cleanup` command!\nThese entries are all in the past.")
DATABASE_CONN.is_procedure_queued = True
print("Bot has been primed for cleanup!")
return
@client.tree.command()
async def validate_cleanup(interaction: discord.Interaction) -> None:
delete_invalidate()
user_nickname = interaction.user.nick
await interaction.response.defer(thinking=True)
print(f"{user_nickname} has called validate_cleanup!\n\nCalling now.")
number_rows_affected: int
try:
number_rows_affected = DATABASE_CONN.call_cleanup(DATABASE_CONN.is_procedure_queued)
except psycopg2.Error as e:
print(e)
await interaction.followup.send(f"Something happened! This message is to inform <@{CONTRASTELLAR}> of this error!\n`{e}`")
return
print("cleanup should be complete. Setting queue variable to False")
DATABASE_CONN.is_procedure_queued = False
await interaction.followup.send(f"Database has been cleaned!\n\n{number_rows_affected} rows have been purged!")
return
@client.tree.command()
async def invalidate_cleanup(interaction: discord.Interaction) -> None:
delete_invalidate()
cleanup_invalidate()
await interaction.response.defer(thinking=True)
print(f"{interaction.user.id} has called the invalidate command!")
print("Cleanup has been invalidated!")
await interaction.followup.send("The queued action has been cancelled!")
return
@client.tree.command()
async def callout(interaction: discord.Interaction, day: int, month: int, year: int, reason: str = '', fill: str = '') -> None:
delete_invalidate()
cleanup_invalidate()
user_id = interaction.user.id
user_nick = interaction.user.display_name
user_char_name = DATABASE_CONN.return_char_name(user_id)
today: datetime.date = datetime.date.today()
callout_date: datetime.date = datetime.date(year=year, month=month, day=day)
if today > callout_date:
await interaction.response.send_message(f'{user_char_name}, date in the past given. Please give a date for today or in the future!')
return
if len(reason) > 512:
await interaction.response.send_message(f'{user_char_name}, your reason was too long. Keep it to 512 characters or less.')
return
try:
DATABASE_CONN.add_callout(user_id=user_id, callout=callout_date, reason=reason, nickname=user_nick, char_name=user_char_name, potential_fill=fill)
except UNIQUEVIOLATION:
await interaction.response.send_message(f'{user_char_name} -- you have already added a callout for {callout_date} with reason: {reason}')
except INVALIDDATETIMEFORMAT:
await interaction.response.send_message(f'{user_char_name} -- please format the date as the following format: MM/DD/YYYY')
except FOREIGNKEYVIOLATION:
await interaction.response.send_message(f'{user_nick} -- please register with the bot using the following command!\n`/registercharacter`\n Please use your in-game name!')
except helper.db_helper.DateTimeError:
await interaction.response.send_message(f'{user_nick}, you\'re trying to submit a callout for a time in the past! Please verify that this is what you want to do!')
except psycopg2.Error as e:
await interaction.response.send_message(f'{user_nick} -- an error has occured!\nNotifying <@{CONTRASTELLAR}> of this error. Error is as follows --\n{e}')
else:
await interaction.response.send_message(f'{user_char_name} -- you added a callout for {callout_date} with reason: {reason}')
await interaction.followup.send(f'{DATABASE_CONN.format_list_of_callouts(DATABASE_CONN.query_callouts(7))}')
@client.tree.command()
async def add_break(interaction: discord.Interaction, start_day: int, start_month: int,
start_year: int, end_day: int, end_month: int, end_year: int) -> None:
delete_invalidate()
cleanup_invalidate()
uid = interaction.user.id
user_nick = interaction.user.display_name
user_char_name = DATABASE_CONN.return_char_name(uid)
start_date: datetime.date = datetime.date(year=start_year, month=start_month, day=start_day)
end_date: datetime.date = datetime.date(year=end_year, month=end_month, day=end_day)
try:
DATABASE_CONN.add_break(user_id=uid, break_start=start_date, break_end=end_date)
except UNIQUEVIOLATION:
await interaction.response.send_message(f'{user_char_name} -- you have already added a break for {start_date} through {end_date}!')
except helper.db_helper.DateTimeError:
await interaction.response.send_message(f'{user_nick}, you\'re trying to submit a break for a time in the past! Please verify that this is what you want to do!')
except psycopg2.Error as e:
await interaction.response.send_message(f'{user_nick} -- an error has occured!\nNotifying <@{CONTRASTELLAR}> of this error. Error is as follows --\n{e}')
else:
await interaction.response.send_message(f'{user_char_name} -- you added a break for for {start_date} through {end_date}!')
@client.tree.command()
async def remove_callout(interaction: discord.Interaction, day: int, month: int, year: int) -> None:
delete_invalidate()
cleanup_invalidate()
user_id = interaction.user.id
user_char_name = DATABASE_CONN.return_char_name(user_id)
callout_date: datetime.date = datetime.date(year=year, month=month, day=day)
try:
DATABASE_CONN.remove_callout(user_id=user_id, callout=callout_date)
except psycopg2.Error:
await interaction.response.send_message(f'{user_char_name} -- you have not added a callout for {callout_date}')
else:
await interaction.response.send_message(f'{user_char_name} removed a callout for {callout_date}')
@client.tree.command()
async def remove_break(interaction: discord.Interaction, day: int, month: int, year: int) -> None:
delete_invalidate()
cleanup_invalidate()
user_id = interaction.user.id
user_char_name = DATABASE_CONN.return_char_name(user_id)
break_date: datetime.date = datetime.date(year=year, month=month, day=day)
try:
DATABASE_CONN.remove_break(user_id=user_id, break_date=break_date)
except psycopg2.Error:
await interaction.response.send_message(f'{user_char_name} -- no break was added for {break_date}')
else:
await interaction.response.send_message(f'{user_char_name} -- you removed the break starting on {break_date}')
return
@client.tree.command()
async def schedule(interaction: discord.Interaction, days: int = DAYS_FOR_CALLOUTS) -> None:
delete_invalidate()
cleanup_invalidate()
await interaction.response.defer(thinking=True)
callouts: list = DATABASE_CONN.query_callouts(days=days)
callouts: str = DATABASE_CONN.formatted_list_of_callouts(callouts)
await interaction.followup.send(f'Callouts for the next {days} days:\n{callouts}')
return
@client.tree.command()
async def self_callouts(interaction: discord.Interaction, days: int = 365) -> None:
delete_invalidate()
cleanup_invalidate()
uid = interaction.user.id
await interaction.response.defer(thinking=True)
callouts: list = DATABASE_CONN.query_self_callouts(user_id=uid, days=days)
callouts: str = DATABASE_CONN.formatted_list_of_callouts(callouts)
character_name: str = DATABASE_CONN.return_char_name(uid)
await interaction.followup.send(f'Callouts for the next **{days}** for user **{character_name}**:\n{callouts}')
args: argparse.Namespace = parser.parse_args()
# To be used for reading/writing to the database
# #will not handle the parsing of the returns from the db
DATABASE_CONN = helper.db_helper.DBHelper(args.database)
TOKEN: str = open(args.token, encoding='utf-8').read()
client.run(TOKEN)