# 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 charname = DATABASE_CONN.return_char_name(uid=user_id) await interaction.response.send_message(f'Pong! {charname} -- 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 breaks(interaction: discord.Interaction, days: int = 365) -> None: delete_invalidate() cleanup_invalidate() await interaction.response.defer(thinking=True) breaks: list = DATABASE_CONN.query_breaks(days=days) break_output: str = DATABASE_CONN.format_list_of_breaks(breaks=breaks) await interaction.followup.send(f'Breaks for the next {days} days:\n{break_output}') 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)