339 lines
14 KiB
Python
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)
|