diff --git a/.gitignore b/.gitignore index 6233bb3..ae99898 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /.idea/ .env /src/db/postgres-data/ +.gitignore +.vscode/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -157,3 +159,5 @@ dmypy.json cython_debug/ /.obsidian/ +src/bot/cogs/moderation/admin_detect_spam_message.py +.gitignore diff --git a/src/api/api.py b/src/api/api.py index b7590bc..4ee2ce1 100644 --- a/src/api/api.py +++ b/src/api/api.py @@ -10,6 +10,7 @@ from routes.settings import settings from routes.points import points from routes.roles import role +from routes.tickets import tickets from core.db_helper import DB @@ -28,6 +29,7 @@ app.register_blueprint(settings) app.register_blueprint(points) app.register_blueprint(role) +app.register_blueprint(tickets) # Error handlers @app.errorhandler(Exception) diff --git a/src/api/core/db_helper.py b/src/api/core/db_helper.py index d1a47d1..cf04ba0 100644 --- a/src/api/core/db_helper.py +++ b/src/api/core/db_helper.py @@ -51,7 +51,9 @@ def get_log_settings(self): try: self.cursor.execute("SELECT * FROM logging") # case sensitive result = self.cursor.fetchall() - return {"status": "ok", "logging": result} + response = {"status": "ok", "logging": result} + logger.debug(f"DB Response:\n{response}") + return response except OperationalError as err: logger.error(f"Error fetching logging: {err}") return {"status": "error", "message": str(err)} @@ -61,7 +63,9 @@ def get_logging(self): try: self.cursor.execute("SELECT * FROM logging") result = self.cursor.fetchall() - return {"status": "ok", "logging": result} + response = {"status": "ok", "logging": result} + logger.debug(f"DB Response:\n{response}") + return response except OperationalError as err: logger.error(f"Error fetching logging: {err}") return {"status": "error", "message": str(err)} @@ -96,6 +100,7 @@ def delete_log_setting(self, log_id): ################## ## Settings ## ################## + def get_setting(self, setting_id): logger.debug("API attempting to contact DB for get_setting...") try: @@ -103,7 +108,7 @@ def get_setting(self, setting_id): result = self.cursor.fetchone() return {"status": "ok", "setting": result} except OperationalError as err: - logger.error(f"Error fetching logging: {err}") + logger.error(f"Error fetching setting: {err}") return {"status": "error", "message": str(err)} def get_settings(self): @@ -113,7 +118,7 @@ def get_settings(self): result = self.cursor.fetchall() return {"status": "ok", "setting": result} except OperationalError as err: - logger.error(f"Error fetching setting: {err}") + logger.error(f"Error fetching settings: {err}") return {"status": "error", "message": str(err)} @@ -148,6 +153,7 @@ def delete_setting(self, log_id): ################## ## roles ## ################## + def get_role(self, role_id): logger.debug("API attempting to contact DB for get_role...") try: @@ -189,10 +195,10 @@ def add_role(self, name, value): def delete_role(self, role_id): logger.debug(f"API attempting to contact DB for delete_role with role_ID:{role_id}") try: - self.cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,)) + self.cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,)) return {"status": "ok", "message": f"role with ID {role_id} deleted successfully"} except OperationalError as err: - logger.error(f"Error deleting role: {err}") + logger.error(f"Error deleting role: {err}") return {"status": "error", "message": str(err)} ################## @@ -212,12 +218,10 @@ def get_points_for_user(self, user_id): def update_points(self, user_id, value): try: - self.cursor.execute("UPDATE users SET points = points + %s WHERE discord_id = %s", (value, user_id)) - self.conn.commit() + self.cursor.execute("UPDATE users SET points = points + %s WHERE discord_id = %s", (value, user_id)) return {"status": "ok", "message": "points updated successfully"} except OperationalError as err: logger.error(f"Error updating points: {err}") - self.conn.rollback() return {"status": "error", "message": str(err)} def add_user_to_points(self, user_id): @@ -226,11 +230,11 @@ def add_user_to_points(self, user_id): "INSERT INTO users (discord_id, points) VALUES (%s, 0) ON CONFLICT (discord_id) DO NOTHING;" , (user_id,) ) - # self.conn.commit() + # return {"status": "ok", "message": "New user added to 'points' successfully"} except OperationalError as err: logger.error(f"Error adding new user: {err}") - # self.conn.rollback() + # return {"status": "error", "message": str(err)} def remove_user_from_points(self, user_id): @@ -238,13 +242,13 @@ def remove_user_from_points(self, user_id): self.cursor.execute("DELETE FROM users WHERE discord_id = %s", (user_id,)) affected_rows = self.cursor.rowcount if affected_rows > 0: - self.conn.commit() + return {"status": "ok", "message": f"User with ID: {user_id} deleted successfully"} else: return {"status": "not_found", "message": f"No user found with ID: {user_id}"} except OperationalError as err: logger.error(f"Error deleting user: {err}") - self.conn.rollback() + return {"status": "error", "message": str(err)} def get_top_10(self): @@ -254,5 +258,121 @@ def get_top_10(self): return {"status": "ok", "message": result} except OperationalError as err: logger.error(f"Error deleting user: {err}") - self.conn.rollback() - return {"status": "error", "message": str(err)} \ No newline at end of file + + return {"status": "error", "message": str(err)} + + + ################## + ## Ticket ## + ################## + + def get_ticket(self, thread_id: int): + """Fetches a specific ticket from the database by its thread ID.""" + + logger.debug("API attempting to contact DB for get_ticket...") + try: + self.cursor.execute("SELECT * FROM tickets where thread_id = %s", (thread_id,)) + result = self.cursor.fetchone() + return {"status": "ok", "ticket": result} + except OperationalError as err: + logger.error(f"Error fetching ticket: {err}") + return {"status": "error", "message": str(err)} + + def get_tickets(self): + """Fetches all tickets from the database.""" + + logger.debug("API attempting to contact DB for get_tickets...") + try: + self.cursor.execute("SELECT * FROM tickets") + result = self.cursor.fetchall() + return {"status": "ok", "tickets": result} + except OperationalError as err: + logger.error(f"Error fetching tickets: {err}") + return {"status": "error", "message": str(err)} + + def add_ticket(self, thread_id: int, creator_id: int, channel_id: int, status: str = 'open'): + """ + Add a new ticket to the database. + Args: + - thread_id: The unique ID of the ticket (e.g., thread ID). + - creator_id: The Discord ID of the user who created the ticket. + - channel_id: The Discord ID of the channel associated with the ticket. + - status: The current status of the ticket (e.g., 'open', 'closed') + + """ + + logger.debug(f"API attempting to contact DB for add_ticket with thread_id:{thread_id} - creator_id:{creator_id} - channel_id:{channel_id} - status:{status}") + try: + self.cursor.execute( + """ + INSERT INTO tickets ( + thread_id, + creator_id, + channel_id, + status + ) + VALUES (%s, %s, %s, %s)""", + ( + thread_id, + creator_id, + channel_id, + status + ) + ) + + + return {"status": "ok", "message": "New ticket added successfully"} + except OperationalError as err: + logger.error(f"Error adding new ticket: {err}") + + return {"status": "error", "message": str(err)} + + def close_ticket(self, thread_id: int): + """ + Close a specific ticket in the database by updating its status to 'closed'. + Args: + - thread_id: The unique ID of the ticket (e.g., thread ID) to be closed. + """ + + logger.debug(f"API attempting to contact DB for close_ticket with thread_id:{thread_id}") + try: + self.cursor.execute("UPDATE tickets SET status = 'closed' WHERE thread_id = %s", (thread_id,)) + + return {"status": "ok", "message": f"Ticket with thread ID {thread_id} closed successfully"} + except OperationalError as err: + logger.error(f"Error closing ticket: {err}") + + return {"status": "error", "message": str(err)} + + def update_ticket_status(self, thread_id: int, status: str): + logger.debug(f"API attempting to contact DB for update_ticket_status with thread_id:{thread_id} - status:{status}") + try: + self.cursor.execute("UPDATE tickets SET status = %s WHERE thread_id = %s", (status, thread_id)) + + return {"status": "ok", "message": "Ticket status updated successfully"} + except OperationalError as err: + logger.error(f"Error updating ticket status: {err}") + + return {"status": "error", "message": str(err)} + + def delete_ticket(self, thread_id: int): + logger.debug(f"API attempting to contact DB for delete_ticket with thread_id:{thread_id}") + try: + self.cursor.execute("DELETE FROM tickets WHERE thread_id = %s", (thread_id,)) + + return {"status": "ok", "message": f"Ticket with thread ID {thread_id} deleted successfully"} + except OperationalError as err: + logger.error(f"Error deleting ticket: {err}") + + return {"status": "error", "message": str(err)} + + def get_open_tickets(self): + logger.debug("API attempting to contact DB for get_open_tickets...") + try: + self.cursor.execute("SELECT * FROM tickets WHERE status = 'open'") + result = self.cursor.fetchall() + return {"status": "ok", "tickets": result} + except OperationalError as err: + logger.error(f"Error fetching open tickets: {err}") + return {"status": "error", "message": str(err)} + \ No newline at end of file diff --git a/src/api/routes/healthchecks.py b/src/api/routes/healthchecks.py index 34448d8..be59676 100644 --- a/src/api/routes/healthchecks.py +++ b/src/api/routes/healthchecks.py @@ -19,7 +19,7 @@ def api_health_check(): if request.method == 'GET': return jsonify({'status': 'ok'}, 200) - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @health_checks.route('/hc_db', methods=['GET']) @@ -30,9 +30,9 @@ def database_health_check(): if request.method == 'GET': try: hc = eos.db.database_health_check() - return jsonify(hc, 200) + return jsonify(hc), 200 except TypeError as ded: - return jsonify({"status": "unhealthy", "error": "DB unreachable"}, 404) + return jsonify({"status": "unhealthy", "error": "DB unreachable"}), 404 - return jsonify({'message': 'improper request method'}, 404) + return jsonify({'message': 'improper request method'}), 404 diff --git a/src/api/routes/logging.py b/src/api/routes/logging.py index 6d26100..4d26915 100644 --- a/src/api/routes/logging.py +++ b/src/api/routes/logging.py @@ -25,7 +25,7 @@ def get_log_setting(log_id=None): # Retrieve a single setting result = eos.db.get_log_setting(log_id) - return jsonify(result, 200) + return jsonify(result), 200 # @settings.route('/log_settings', methods=['GET']) # def get_log_settings(): @@ -36,7 +36,7 @@ def get_log_setting(log_id=None): # """ # result = eos.db.get_log_settings() # -# return jsonify(result, 200) +# return jsonify(result), 200 @logs.route('/logging/', methods=['PUT']) def update_log_setting(log_id): @@ -46,9 +46,9 @@ def update_log_setting(log_id): if request.method == 'PUT': data = request.json result = eos.db.update_logging(int(log_id), data['value']) - return jsonify(result, 200) + return jsonify(result), 200 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @logs.route('/logging', methods=['POST']) def add_log_setting(): @@ -58,9 +58,9 @@ def add_log_setting(): if request.method == 'POST': data = request.json result = eos.db.add_log_setting(data['name'], data['value']) - return jsonify(result, 201) + return jsonify(result), 201 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @logs.route('/logging/', methods=['DELETE']) def delete_log_setting(log_id): @@ -69,6 +69,6 @@ def delete_log_setting(log_id): """ if request.method == 'DELETE': result = eos.db.delete_log_setting(log_id) - return jsonify(result, 200) + return jsonify(result), 200 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 diff --git a/src/api/routes/roles.py b/src/api/routes/roles.py index 08b6752..7934556 100644 --- a/src/api/routes/roles.py +++ b/src/api/routes/roles.py @@ -25,7 +25,7 @@ def get_role(role_id=None): # Retrieve a single role result = eos.db.get_role(role_id) - return jsonify(result, 200) + return jsonify(result), 200 @role.route('/role/', methods=['PUT']) def update_role(role_id): @@ -35,9 +35,9 @@ def update_role(role_id): if request.method == 'PUT': data = request.json result = eos.db.update_role(int(role_id), data['value']) - return jsonify(result, 200) + return jsonify(result), 200 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @role.route('/role', methods=['POST']) def add_role(): @@ -47,9 +47,9 @@ def add_role(): if request.method == 'POST': data = request.json result = eos.db.add_role(data['name'], data['value']) - return jsonify(result, 201) + return jsonify(result), 201 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @role.route('/role/', methods=['DELETE']) def delete_role(role_id): @@ -58,6 +58,7 @@ def delete_role(role_id): """ if request.method == 'DELETE': result = eos.db.delete_role(role_id) - return jsonify(result, 200) + return jsonify(result), 200 + + return jsonify({'message': 'improper request method'}), 405 - return jsonify({'message': 'improper request method'}, 405) diff --git a/src/api/routes/settings.py b/src/api/routes/settings.py index db8c2c8..e188017 100644 --- a/src/api/routes/settings.py +++ b/src/api/routes/settings.py @@ -11,21 +11,20 @@ @settings.route('/settings', methods=['GET']) @settings.route('/settings/', methods=['GET']) -def get_setting(setting_id): +def get_setting(setting_id=None): """ Retrieve settings from the database. :param setting_id: Optional integer ID of a specific setting :return: JSON response with setting """ - if setting_id == 0: - # Retrieve all settings + if setting_id is None or setting_id == 0: # Retrieve all settings result = eos.db.get_settings() else: # Retrieve a single setting result = eos.db.get_setting(setting_id) - return jsonify(result, 200) + return jsonify(result), 200 @settings.route('/settings/', methods=['PUT']) @@ -36,9 +35,9 @@ def update_setting(setting_id): if request.method == 'PUT': data = request.json result = eos.db.update_setting(int(setting_id), data['value']) - return jsonify(result, 200) + return jsonify(result), 200 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @settings.route('/settings', methods=['POST']) def add_setting(): @@ -48,9 +47,9 @@ def add_setting(): if request.method == 'POST': data = request.json result = eos.db.add_setting(data['name'], data['value']) - return jsonify(result, 201) + return jsonify(result), 201 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 @settings.route('/settings/', methods=['DELETE']) def delete_setting(setting_id): @@ -59,6 +58,6 @@ def delete_setting(setting_id): """ if request.method == 'DELETE': result = eos.db.delete_setting(setting_id) - return jsonify(result, 200) + return jsonify(result), 200 - return jsonify({'message': 'improper request method'}, 405) + return jsonify({'message': 'improper request method'}), 405 diff --git a/src/api/routes/tickets.py b/src/api/routes/tickets.py new file mode 100644 index 0000000..a4820d5 --- /dev/null +++ b/src/api/routes/tickets.py @@ -0,0 +1,92 @@ +from flask import Blueprint, jsonify, request +from flask import current_app as eos +import logging + +logger = logging.getLogger(__name__) + +tickets = Blueprint('tickets', __name__) + +@tickets.route('/tickets', methods=['GET']) +@tickets.route('/tickets/', methods=['GET']) +def get_ticket(thread_id=None): + """ + Retrieve tickets from the database. + + :param thread_id: Optional integer ID of a specific ticket + :return: JSON response with ticket(s) + """ + try: + if thread_id is None: + # Retrieve all tickets + result = eos.db.get_tickets() + else: + # Retrieve a single ticket + result = eos.db.get_ticket(thread_id) + except Exception as e: + logger.error(f"Error retrieving ticket(s) from database: {e}") + return jsonify({'status': 'error', 'message': 'An error occurred while retrieving tickets.'}), 500 + + return jsonify(result), 200 + +@tickets.route('/tickets/', methods=['PUT']) +def update_ticket(thread_id): + """ + Update an existing ticket in the database. + """ + if request.method == 'PUT': + data = request.json + try: + result = eos.db.update_ticket_status(int(thread_id), data['status']) + return jsonify(result), 200 + except Exception as e: + logger.error(f"Error updating ticket in database: {e}") + return jsonify({'status': 'error', 'message': 'An error occurred while updating the ticket.'}), 500 + + return jsonify({'message': 'improper request method'}), 405 + +@tickets.route('/tickets/', methods=['PATCH']) +def close_ticket(thread_id): + """ + Close a specific ticket in the database. + """ + if request.method == 'PATCH': + try: + result = eos.db.update_ticket_status(thread_id, "closed") + return jsonify(result), 200 + except Exception as e: + logger.error(f"Error closing ticket in database: {e}") + return jsonify({'status': 'error', 'message': 'An error occurred while closing the ticket.'}), 500 + + return jsonify({'message': 'improper request method'}), 405 + +@tickets.route('/tickets', methods=['POST']) +def add_ticket(): + """ + Add a new ticket to the database. + """ + if request.method == 'POST': + data = request.json + + try: + result = eos.db.add_ticket(data["thread_id"], data["creator_id"], data["channel_id"], data.get("status", "open")) + return jsonify(result), 201 + except Exception as e: + logger.error(f"Error adding ticket to database: {e}") + return jsonify({'status': 'error', 'message': 'An error occurred while adding the ticket.'}), 500 + + return jsonify({'message': 'improper request method'}), 405 + +@tickets.route('/tickets/', methods=['DELETE']) +def delete_ticket(thread_id): + """ + Delete a specific ticket from the database. + """ + if request.method == 'DELETE': + try: + result = eos.db.delete_ticket(thread_id) + return jsonify(result), 200 + except Exception as e: + logger.error(f"Error deleting ticket from database: {e}") + return jsonify({'status': 'error', 'message': 'An error occurred while deleting the ticket.'}), 500 + + return jsonify({'message': 'improper request method'}), 405 diff --git a/src/bot/cogs/admin/points.py b/src/bot/cogs/admin/points.py index 99e312e..5913897 100644 --- a/src/bot/cogs/admin/points.py +++ b/src/bot/cogs/admin/points.py @@ -48,8 +48,8 @@ async def sync_users(self, ctx: commands.Context) -> None: Users that exist in the DB will not be synced. """ added = 0 - for user in ctx.guild.members: - self.bot.api.add_user_to_points(user.id) + for user in ctx.guild.members: # type: ignore + self.bot.api.add_user_to_points(user.id) # type: ignore added += 1 await ctx.reply( embed=embed_info( @@ -101,9 +101,11 @@ async def update_points(self, ctx: commands.Context, user: discord.Member, amoun if update_points['status'] == 'ok': await ctx.reply( embed=embed_info( - "" - , f"{amount.lstrip('-+')} points {'removed from' if amount.startswith('-') else 'added to'} {user.display_name}" - , discord.Color.green() if not amount.startswith('-') else discord.Color.red() + "", + f"{abs(amount)} points " + f"{'removed from' if amount < 0 else 'added to'} " + f"{user.display_name}", + discord.Color.red() if amount < 0 else discord.Color.green() ) ) else: @@ -181,24 +183,22 @@ async def on_command_error(self, ctx: commands.Context, error): Error handling for the >update_points command. """ if isinstance(error, commands.MissingRequiredArgument): - await ctx.reply( - embed=embed_info( - "Error!", "You must provide a required argument." - , discord.Color.dark_gray() - ) - ) + await ctx.reply(embed=embed_info("Error!", "You must provide a required argument.", discord.Color.dark_gray())) + elif isinstance(error, commands.MissingPermissions): + logger.warning(f"{ctx.author.name} has attempted to use the {ctx.invoked_with} command, and was not allowed to do so.") + await ctx.reply('For one reason, or another, YOU cannot use this command.') @sync_users.error async def sync_users_command_error(self, ctx, error): if isinstance(error, commands.CheckFailure): logger.warning(f"{ctx.author.name} has attempted to use the {ctx.invoked_with} command, and was not allowed to do so.") - await ctx.send('For one reason, or another, YOU cannot use this command.') + await ctx.reply('For one reason, or another, YOU cannot use this command.') - @update_points.error - async def update_points_command_error(self, ctx, error): - if isinstance(error, commands.CheckFailure): - logger.warning(f"{ctx.author.name} has attempted to use the {ctx.invoked_with} command, and was not allowed to do so.") - await ctx.send('For one reason, or another, YOU cannot use this command.') + #@update_points.error + #async def update_points_command_error(self, ctx, error): + # if isinstance(error, commands.CheckFailure): + # logger.warning(f"{ctx.author.name} has attempted to use the {ctx.invoked_with} command, and was not allowed to do so.") + # await ctx.send('For one reason, or another, YOU cannot use this command.') async def setup(bot: commands.Bot) -> None: diff --git a/src/bot/cogs/admin/settings.py b/src/bot/cogs/admin/settings.py index b0c5a16..8e2e633 100644 --- a/src/bot/cogs/admin/settings.py +++ b/src/bot/cogs/admin/settings.py @@ -28,42 +28,46 @@ def __init__(self, bot: commands.Bot) -> None: @is_admin() @commands.hybrid_command() async def settings(self, ctx: commands.Context): - """ - List all available settings. - """ - - server_settings = self.bot.api.get_all_settings() - log_settings = self.bot.api.get_all_log_settings() - - if server_settings[0]["status"] != "ok": - await ctx.send(f"Failed to retrieve settings: {server_settings['message']}") - return - if log_settings[0]["status"] != "ok": - await ctx.send(f"Failed to retrieve settings: {log_settings['message']}") - return - - embed = discord.Embed(title="-- Settings --", - description="Here, you can see the current settings for the server.", - colour=0x000000, - timestamp=datetime.datetime.now()) - - for setting in server_settings[0]["setting"]: - value = f"<#{setting[2]}>" if setting[2] != '0' else 'Off' - embed.add_field(name="" - , value=f"**{setting[1]}**:{value}" - , inline=False) - - for setting in log_settings[0]["logging"]: - value = f"<#{setting[2]}>" if setting[2] != '0' else 'Off' - embed.add_field(name=f"" - , value=f"**{setting[1]}**:{value}" - , inline=False) - - - embed.set_footer(text=ctx.guild.name, + """ + List all available settings. + """ + + server_settings = self.bot.api.get_all_settings() + log_settings = self.bot.api.get_all_log_settings() + try: + if server_settings["status"] != "ok": + await ctx.send(f"Failed to retrieve settings: {server_settings['message']}") + return + if log_settings["status"] != "ok": + await ctx.send(f"Failed to retrieve settings: {log_settings['message']}") + return + except Exception as e: + logger.error(f"Error retrieving settings: {e}") + await ctx.send("An error occurred while retrieving settings.") + return + + embed = discord.Embed(title="-- Settings --", + description="Here, you can see the current settings for the server.", + colour=0x000000, + timestamp=datetime.datetime.now()) + + for setting in server_settings["setting"]: + value = f"<#{setting[2]}>" if setting[2] != '0' else 'Off' + embed.add_field(name="" + , value=f"**{setting[1]}**:{value}" + , inline=False) + + for setting in log_settings["logging"]: + value = f"<#{setting[2]}>" if setting[2] != '0' else 'Off' + embed.add_field(name=f"" + , value=f"**{setting[1]}**:{value}" + , inline=False) + + + embed.set_footer(text=ctx.guild.name, icon_url=ctx.guild.icon) - await ctx.send(embed=embed) + await ctx.send(embed=embed) @is_master_guild() @is_admin() @@ -78,9 +82,9 @@ async def update_settings(self, ctx: commands.Context): server_settings = self.bot.api.get_all_settings() # Pull the names out of the returned JSON - logging_types = [item for item in channel_settings[0]['logging']] - role_types = [role for role in role_settings[0]['roles']] - setting_types = [setting for setting in server_settings[0]['setting']] + logging_types = [item for item in channel_settings['logging']] + role_types = [role for role in role_settings['roles']] + setting_types = [setting for setting in server_settings['setting']] # Get the names of the available channels and roles to map channels = [channel for channel in ctx.guild.text_channels if "log" in channel.name] diff --git a/src/bot/cogs/features/ticket.py b/src/bot/cogs/features/ticket.py index b9296b8..ba86660 100644 --- a/src/bot/cogs/features/ticket.py +++ b/src/bot/cogs/features/ticket.py @@ -9,71 +9,166 @@ logger = logging.getLogger(__name__) - -class AddTicketButton(commands.Cog): +class TicketReasonModal(discord.ui.Modal, title="Create Ticket"): """ - This is the slash command that sends our UI element. + This modal appears when the user selects a ticket type. + It allows them to provide a reason or description for the ticket. """ + + description = discord.ui.TextInput( + label="Please describe The Issue...", + style=discord.TextStyle.short, + required=True, + max_length=1000 + ) - def __init__(self, bot): + + + def __init__(self, bot, selected_option: str): + super().__init__() self.bot = bot + self.selected_option = selected_option + + async def on_submit(self, interaction: discord.Interaction): + guild = interaction.guild + channel = self.bot.api.get_one_setting('5') + staff_role = self.bot.api.get_one_role('3') + staff_role = staff_role['roles'][2] if staff_role['status'] == 'ok' else None + + if channel is None or channel['setting'][2] == "0": + logger.warning("Ticket Channel not set in db. Cannot create ticket.") + await interaction.response.send_message("Sorry, the ticket system is not set up yet. Please contact the staff directly.", ephemeral=True) + return + else: + channel = guild.get_channel(int(channel['setting'][2])) # type: ignore + + option = self.selected_option if self.selected_option else "None Selected" + + thread_name = f"{option}-{interaction.user.name}".lower().replace(" ", "-") - @app_commands.command(description="Make a ticket and contact the Staff.") - async def ticket(self, interaction: discord.Interaction): - """ - A simple command with a view. - """ - logger.info("%s used the %s command.", interaction.user.name, interaction.command.name) - await interaction.response.defer() + thread = await channel.create_thread( # type: ignore + name=thread_name, + type=discord.ChannelType.private_thread, + invitable=False + ) + await thread.add_user(interaction.user) # type: ignore + + await thread.send( + f'<@&{staff_role}>\n## {interaction.user.mention} has created a ticket\n' + f'** Type: ** {option}\n' + f'"{self.description.value}" - {interaction.user.name if interaction.user.nick is None else interaction.user.nick}\n\n' + f'\n\n _ please provide any additional information here and our staff will assist you as soon as possible. _' + ) + try: + self.bot.api.add_ticket(thread.id, interaction.user.id, channel.id) + except Exception as e: + logger.error(f"Error adding ticket to database: {e}") + await thread.send("There was an error saving the ticket to the database") + try: + await interaction.response.send_message(content=f"Your ticket has been created! {thread.jump_url}", ephemeral=True) + except Exception as e: + await interaction.channel.send(content=f"Your ticket has been created! {thread.jump_url}") + logger.error(f"Error sending followup message: {e}") + + - await interaction.followup.send( - "Do you need help, or do you have a question for the Staff?", - view=MakeATicket(self.bot), - ephemeral=True, +class TicketDropdown(discord.ui.Select): + def __init__(self, bot): + self.bot = bot + + options = [ + discord.SelectOption(label="Moderation"), + discord.SelectOption(label="Support"), + discord.SelectOption(label="Proposition"), + discord.SelectOption(label="Request"), + ] + + super().__init__( + placeholder="Select ticket type", + options=options + ) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.send_modal( + TicketReasonModal(self.bot, self.values[0]) ) + + +class TicketView(discord.ui.View): + def __init__(self, bot): + super().__init__(timeout=100) # View will timeout after 1.5 minutes + self.bot = bot + self.add_item(TicketDropdown(bot)) + -class MakeATicket(discord.ui.View): +class TicketManager(commands.Cog): """ - A UI component that sends a button, which does other things. + This is the slash command that sends our UI element. """ - def __init__(self, bot, *, timeout=None): - super().__init__(timeout=timeout) + def __init__(self, bot): self.bot = bot + ticket_channel = self.bot.api.get_one_setting('5') + if ticket_channel is None or ticket_channel['setting'][2] == "0": + logger.warning("Ticket Channel not set in db. Ticket commands will not work until this is set.") + raise RuntimeError("Ticket Channel not set in db. Ticket commands will not work until this is set.") + else: + self.ticket_channel = ticket_channel['setting'][2] + + @app_commands.command(description="Make a ticket and contact the Staff.") + async def ticket(self, interaction: discord.Interaction): + + """ + A simple command with a view. + """ + logger.info("%s used the %s command.", interaction.user.name, interaction.command.name) # type: ignore + + await interaction.response.send_message("creating ticket...", view=TicketView(self.bot), ephemeral=True) - @discord.ui.button(label="Open a support Ticket", style=discord.ButtonStyle.primary) - async def button_callback(self, interaction, button): + @app_commands.command(name="close_ticket", description="Close a ticket in the current channel.") + async def close_ticket(self, interaction: discord.Interaction): """ - The callback on the button, or... what happens on click. + Closes the ticket in the current channel. """ - await interaction.response.defer() - button.label = "Ticket Created!" - button.disabled = True - await interaction.edit_original_response(view=self) - - support = interaction.channel #TODO: guild specific settings for a support channel - staff = interaction.guild.get_role(self.bot.api.get_one_role("3")[0]["roles"][2]) # Staff - - ticket = await support.create_thread( - name=f"[Ticket] - {interaction.user}", - message=None, - auto_archive_duration=4320, - type=discord.ChannelType.private_thread, - reason=None, - ) - - for person in interaction.guild.members: - if staff in person.roles: - await ticket.add_user(person) - - await ticket.add_user(interaction.user) - await interaction.delete_original_response() - await ticket.send(f"**{interaction.user.mention}, we have received your ticket.**") - await ticket.send("To better help you, please describe your issue.") - + logger.info("%s used the %s command.", interaction.user.name, interaction.command.name) # type: ignore + thread = interaction.channel + + + if str(thread.parent_id) != self.ticket_channel: #type: ignore + await interaction.response.send_message("This command can only be used in a ticket thread.", ephemeral=True) + return + + + try: + data = self.bot.api.get_ticket(thread.id) # type: ignore + if data is None or data['status'] != 'ok': + logger.warning(f"Ticket data not found for channel {thread.id}. Cannot close ticket.") + + try: + close = self.bot.api.update_ticket_status(thread.id, "closed") # type: ignore + if close is None or close['status'] != 'ok': + logger.warning(f"Failed to update ticket status to closed for channel {thread.id}.") + await interaction.response.send_message("This thread is unknown to the DB", ephemeral=True) + else: + try: + await thread.send(f"This ticket has been closed by {interaction.user.mention}.") + await thread.edit(archived=True, locked=True) # type: ignore + except Exception as e: + logger.error(f"Error archiving and locking thread {thread.id}: {e}") + await interaction.response.send_message("Ticket status updated to closed, but there was an error archiving the thread.", ephemeral=True) + + + except Exception as e: + logger.error(f"Error updating ticket status to closed for channel {thread.id}: {e}") + await interaction.response.send_message("This thread is unknown to the DB", ephemeral=True) + + except Exception as e: + logger.error(f"Error closing ticket: {e}") + await interaction.response.send_message("There was an error closing the ticket. Please try again later.", ephemeral=True) + async def setup(bot: commands.Bot) -> None: """boink""" - await bot.add_cog(AddTicketButton(bot)) + await bot.add_cog(TicketManager(bot)) diff --git a/src/bot/cogs/logging/logging_avatars.py b/src/bot/cogs/logging/logging_avatars.py index d1d2f26..450910f 100644 --- a/src/bot/cogs/logging/logging_avatars.py +++ b/src/bot/cogs/logging/logging_avatars.py @@ -36,7 +36,12 @@ class LoggingAvatars(commands.Cog): def __init__(self, bot): self.bot = bot self.user_log = self.bot.api.get_one_log_setting("4") # User_log + if self.user_log['status'] != 'ok': + raise RuntimeError("Failed to fetch user log settings from API.") + logger.info("LoggingAvatars cog initialized") + + @commands.Cog.listener() async def on_user_update(self, before, after): """ @@ -49,11 +54,11 @@ async def on_user_update(self, before, after): # return if before.avatar != after.avatar: - if self.user_log[0]["status"] == "ok": - if self.user_log[0]["logging"][2] == "0": + if self.user_log["status"] == "ok": + if self.user_log["logging"][2] == "0": logger.debug(f"log was triggered, but logging is disabled. API: {self.user_log}") return - logs_channel = await self.bot.fetch_channel(self.user_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.user_log["logging"][2]) embed = embed_avatar(before, after) diff --git a/src/bot/cogs/logging/logging_member_ban.py b/src/bot/cogs/logging/logging_member_ban.py index 89a366c..3174232 100644 --- a/src/bot/cogs/logging/logging_member_ban.py +++ b/src/bot/cogs/logging/logging_member_ban.py @@ -31,9 +31,15 @@ class LoggingBans(commands.Cog): def __init__(self, bot): self.bot = bot - self.verification_role = self.bot.api.get_one_role('6')[0]['roles'][2] # Verification role ID + setting = self.bot.api.get_one_role('6') + if setting["status"] == "ok": + self.verification_role = int(setting["roles"][2]) # Verification role ID + else: + self.verification_role = 0 + raise RuntimeError("Failed to fetch verification role from API.") + self.mod_log = self.bot.api.get_one_log_setting("5") # mod_log - + @commands.Cog.listener() async def on_member_remove(self, member): """ @@ -50,11 +56,11 @@ async def on_member_remove(self, member): audit_log = [entry async for entry in member.guild.audit_logs(limit=1)][0] - if self.mod_log[0]["status"] == "ok": - if self.mod_log[0]["logging"][2] == "0": - logger.debug(f"log was triggered, but logging is disabled. API: {self.join_log}") + if self.mod_log["status"] == "ok": + if self.mod_log["logging"][2] == "0": + logger.warning(f"log was triggered, but logging is disabled. API: {self.mod_log}") return - logs_channel = await self.bot.fetch_channel(self.mod_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.mod_log["logging"][2]) if str(audit_log.action) == "AuditLogAction.ban": if audit_log.target == member: diff --git a/src/bot/cogs/logging/logging_member_kick.py b/src/bot/cogs/logging/logging_member_kick.py index 05309ef..8270501 100644 --- a/src/bot/cogs/logging/logging_member_kick.py +++ b/src/bot/cogs/logging/logging_member_kick.py @@ -33,9 +33,18 @@ class LoggingKicks(commands.Cog): def __init__(self, bot): self.bot = bot - self.verification_role = self.bot.api.get_one_role('6')[0]['roles'][2] # Verification role ID - self.mod_log = self.bot.api.get_one_log_setting("5") # mod_log + setting = self.bot.api.get_one_role('6') + if setting["status"] == "ok": + self.verification_role = int(setting["roles"][2]) # Verification role ID + else: + self.verification_role = 0 + raise RuntimeError("Failed to fetch verification role from API.") + + + self.mod_log = self.bot.api.get_one_log_setting("5") # mod_log + logger.info("LoggingKicks cog initialized") + @commands.Cog.listener() async def on_member_remove(self, member): """ @@ -52,11 +61,11 @@ async def on_member_remove(self, member): audit_log = [entry async for entry in member.guild.audit_logs(limit=1)][0] - if self.mod_log[0]["status"] == "ok": - if self.mod_log[0]["logging"][2] == "0": - logger.debug(f"log was triggered, but logging is disabled. API: {self.mod_log}") + if self.mod_log["status"] == "ok": + if self.mod_log["logging"][2] == "0": + logger.warning(f"log was triggered, but logging is disabled. API: {self.mod_log}") return - logs_channel = await self.bot.fetch_channel(self.mod_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.mod_log["logging"][2]) if str(audit_log.action) == "AuditLogAction.kick": if audit_log.target == member: diff --git a/src/bot/cogs/logging/logging_member_leaves.py b/src/bot/cogs/logging/logging_member_leaves.py index 1fa68e9..1661a0e 100644 --- a/src/bot/cogs/logging/logging_member_leaves.py +++ b/src/bot/cogs/logging/logging_member_leaves.py @@ -30,8 +30,17 @@ class LoggingLeaves(commands.Cog): def __init__(self, bot): self.bot = bot - self.verification_role = self.bot.api.get_one_role('6')[0]['roles'][2] # Verification role ID + setting = self.bot.api.get_one_role('6') + if setting["status"] == "ok": + self.verification_role = int(setting["roles"][2]) # Verification role ID + else: + self.verification_role = 0 + # exits the init of the cog and also removes it from the bot so there are no conflicts with failure + raise RuntimeError("Failed to fetch verification role from API.") + self.join_log = self.bot.api.get_one_log_setting("2") # Join_log + + @commands.Cog.listener() async def on_member_remove(self, member): @@ -47,11 +56,11 @@ async def on_member_remove(self, member): if self.verification_role in [role.id for role in member.roles]: return - if self.join_log[0]["status"] == "ok": - if self.join_log[0]["logging"][2] == "0": + if self.join_log["status"] == "ok": + if self.join_log["logging"][2] == "0": logger.debug(f"log was triggered, but logging is disabled. API: {self.join_log}") return - logs_channel = await self.bot.fetch_channel(self.join_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.join_log["logging"][2]) audit_log = [entry async for entry in member.guild.audit_logs(limit=1)][0] diff --git a/src/bot/cogs/logging/logging_message_delete.py b/src/bot/cogs/logging/logging_message_delete.py index cd6520e..c473eee 100644 --- a/src/bot/cogs/logging/logging_message_delete.py +++ b/src/bot/cogs/logging/logging_message_delete.py @@ -1,6 +1,8 @@ """ Logging for message deletes """ +from importlib.resources import files +from io import BytesIO import os import logging import datetime @@ -18,8 +20,8 @@ def embed_message_delete(some_member, some_message, some_moderator=None): title=f'<:red_circle:1043616578744357085> Deleted Message' , description=f'{some_moderator.mention if some_moderator is not None else some_member.mention} deleted a message' - f'\nIn {some_message.channel}\nMessage ' - f'author: {some_member.mention}' + f'\nIn {some_message.channel}\n' + f'Message author: {some_member.mention}' , color=discord.Color.red() , timestamp=datetime.datetime.now(datetime.timezone.utc) ) @@ -32,11 +34,21 @@ def embed_message_delete(some_member, some_message, some_moderator=None): the_message = some_message.content[0:1020] + '...' else: the_message = some_message.content - embed.add_field( - name='Message: ' - , value=the_message - , inline=True - ) + if len(the_message) == 0: + the_message = "*No text content*" + else: + embed.add_field( + name='Message: ' + , value=the_message + , inline=True + ) + + if some_message.attachments: + embed.add_field( + name='Attachments: ' + , value='\n'.join([attachment.url for attachment in some_message.attachments]) + , inline=False + ) return embed @@ -49,14 +61,31 @@ class LoggingMessageDelete(commands.Cog): def __init__(self, bot): self.bot = bot - self.staff_channel = self.bot.api.get_one_setting('3')[0]['setting'][2] # Staff Channel ID + setting = self.bot.api.get_one_setting('3') + + if setting['status'] != 'ok': + raise RuntimeError("Failed to fetch staff channel setting from API.") + else: + self.staff_channel = setting['setting'][2] + self.chat_log = self.bot.api.get_one_log_setting("3") # chat_log + if self.chat_log['status'] != 'ok': + raise RuntimeError("Failed to fetch chat log settings from API.") + + async def build_image_embed(self, attachment: discord.Attachment) -> discord.File: + """ + Return File for message to be included with the logging embed. + """ + data = await attachment.read() + file = discord.File(BytesIO(data), filename=attachment.filename) + return file @commands.Cog.listener() - async def on_message_delete(self, message): + async def on_message_delete(self, message) -> None: """ If a mod deletes, take the audit log event. If a user deletes, handle it normally. """ + if message.author.guild.id != int(os.getenv("MASTER_GUILD")) or \ message.author.guild.id is None: logger.warning(">> on_message_delete fired, but not in master guild. Ignoring event.") @@ -65,23 +94,33 @@ async def on_message_delete(self, message): if message.channel.id == self.staff_channel: logger.debug("Message delete in staff channel was ignored.") return - + audit_log = [entry async for entry in message.guild.audit_logs(limit=1)][0] - if self.chat_log[0]["status"] == "ok": - if self.chat_log[0]["logging"][2] == "0": + + if self.chat_log["status"] == "ok": + if self.chat_log["logging"][2] == "0": logger.debug(f"log was triggered, but logging is disabled. API: {self.chat_log}") return - logs_channel = await self.bot.fetch_channel(self.chat_log[0]["logging"][2]) - + logs_channel = await self.bot.fetch_channel(self.chat_log["logging"][2]) + + + file_embeds = [] + if len(message.attachments) > 0: + for attachment in message.attachments: + if attachment.content_type and attachment.content_type.startswith('image/'): + logger.debug(f"Image attachment detected in deleted message, {attachment.filename}:{attachment.url}") + file_embeds.append(await self.build_image_embed(attachment)) + if str(audit_log.action) == 'AuditLogAction.message_delete': # Then a moderator deleted a message. embed = embed_message_delete(audit_log.target, message, audit_log.user) - await logs_channel.send(embed=embed) - + await logs_channel.send(embed=embed,files=file_embeds) + else: # Otherwise, the author deleted it. username = message.author - await logs_channel.send(embed=embed_message_delete(username, message)) + await logs_channel.send(embed=embed_message_delete(username, message),files=file_embeds) + else: logger.critical(f"API error. API response not ok. -> {self.chat_log}") diff --git a/src/bot/cogs/logging/logging_message_edit.py b/src/bot/cogs/logging/logging_message_edit.py index 5b7cdbe..9c773d1 100644 --- a/src/bot/cogs/logging/logging_message_edit.py +++ b/src/bot/cogs/logging/logging_message_edit.py @@ -49,8 +49,16 @@ class LoggingMessageEdit(commands.Cog): def __init__(self, bot): self.bot = bot - self.staff_channel = self.bot.api.get_one_setting('3')[0]['setting'][2] # Staff Channel ID + setting = self.bot.api.get_one_setting('3') + if setting['status'] != 'ok': + raise RuntimeError("Failed to fetch staff channel setting from API.") + else: + self.staff_channel = setting['setting'][2] + self.chat_log = self.bot.api.get_one_log_setting("3") # chat_log + if self.chat_log['status'] != 'ok': + raise RuntimeError("Failed to fetch chat log settings from API.") + @commands.Cog.listener() async def on_message_edit(self, message_before, message_after): @@ -71,12 +79,12 @@ async def on_message_edit(self, message_before, message_after): return elif message_before.content != message_after.content: - if self.chat_log[0]["status"] == "ok": - if self.chat_log[0]["logging"][2] == "0": + if self.chat_log["status"] == "ok": + if self.chat_log["logging"][2] == "0": logger.debug(f"log was triggered, but logging is disabled. API: {self.chat_log}") return - logs_channel = await self.bot.fetch_channel(self.chat_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.chat_log["logging"][2]) # This guy here makes sure we use the displayed name inside the guild. if message_after.author.nick is None: diff --git a/src/bot/cogs/logging/logging_name_changes.py b/src/bot/cogs/logging/logging_name_changes.py index 76efc6b..b8f56e6 100644 --- a/src/bot/cogs/logging/logging_name_changes.py +++ b/src/bot/cogs/logging/logging_name_changes.py @@ -31,7 +31,10 @@ class LoggingNameChanges(commands.Cog): def __init__(self, bot): self.bot = bot self.user_log = self.bot.api.get_one_log_setting("4") # User_log - + if self.user_log['status'] != 'ok': + raise RuntimeError("Failed to fetch user log settings from API.") + + @commands.Cog.listener() async def on_member_update(self, before, after): """ @@ -54,11 +57,11 @@ async def on_member_update(self, before, after): username_after = after.nick if before.nick != after.nick and before.nick is not None: - if self.user_log[0]["status"] == "ok": - if self.user_log[0]["logging"][2] == "0": + if self.user_log["status"] == "ok": + if self.user_log["logging"][2] == "0": logger.debug(f"log was triggered, but logging is disabled. API: {self.user_log}") return - logs_channel = await self.bot.fetch_channel(self.user_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.user_log["logging"][2]) embed = embed_name_change(username_before, username_after) diff --git a/src/bot/cogs/logging/logging_roles.py b/src/bot/cogs/logging/logging_roles.py index 52c01e9..36f6980 100644 --- a/src/bot/cogs/logging/logging_roles.py +++ b/src/bot/cogs/logging/logging_roles.py @@ -44,7 +44,10 @@ class LoggingRoles(commands.Cog): def __init__(self, bot): self.bot = bot self.mod_log = self.bot.api.get_one_log_setting("5") # mod_log - + if self.mod_log['status'] != 'ok': + raise RuntimeError("Failed to fetch mod log settings from API.") + logger.info("LoggingRoles cog initialized") + @commands.Cog.listener() async def on_member_update(self, before, after): """ @@ -63,11 +66,11 @@ async def on_member_update(self, before, after): responsible_member = audit_log.user changed_roles = [] - if self.mod_log[0]["status"] == "ok": - if self.mod_log[0]["logging"][2] == "0": + if self.mod_log["status"] == "ok": + if self.mod_log["logging"][2] == "0": logger.debug(f"log was triggered, but logging is disabled. API: {self.mod_log}") return - logs_channel = await self.bot.fetch_channel(self.mod_log[0]["logging"][2]) + logs_channel = await self.bot.fetch_channel(self.mod_log["logging"][2]) if len(before.roles) > len(after.roles): for role in before.roles: diff --git a/src/bot/cogs/moderation/admin_purge.py b/src/bot/cogs/moderation/admin_purge.py index 2599d83..a7840f2 100644 --- a/src/bot/cogs/moderation/admin_purge.py +++ b/src/bot/cogs/moderation/admin_purge.py @@ -28,13 +28,13 @@ def embed_info(message): def api_request_is_ok(request): - if request[0]["status"] == "ok": + if request["status"] == "ok": return True return False def logging_is_activated(request): - if request[0]["logging"][2] == "0": + if request["logging"][2] == "0": return False return True @@ -64,12 +64,12 @@ async def purge_messages(self, interaction: discord.Interaction, amount: int): if api_request_is_ok(self.log_channel_req): logger.info(f"{interaction.user.name} is purging {amount} messages from " - f"the {self.log_channel_req[0]['logging'][1]}") + f"the {self.log_channel_req['logging'][1]}") await interaction.response.defer() await interaction.channel.purge(limit=amount + 1) if logging_is_activated(self.log_channel_req): - logging_channel = await self.bot.fetch_channel(self.log_channel_req[0]["logging"][2]) + logging_channel = await self.bot.fetch_channel(self.log_channel_req["logging"][2]) await logging_channel.send(f"{amount} messages purged" f" from {interaction.channel.mention}" diff --git a/src/bot/cogs/moderation/admin_quarantine.py b/src/bot/cogs/moderation/admin_quarantine.py index 7efabe8..e10cc9c 100644 --- a/src/bot/cogs/moderation/admin_quarantine.py +++ b/src/bot/cogs/moderation/admin_quarantine.py @@ -67,10 +67,16 @@ class AdminQuarantine(commands.Cog): def __init__(self, bot): self.bot = bot - self.naughty_role = self.bot.api.get_one_role('7')[0]['roles'][2] # quarantine role ID - self.verified_role = self.bot.api.get_one_role('6')[0]['roles'][2] # Verification role ID - self.mod_log = self.bot.api.get_one_log_setting("5") # mod_log - + self.naughty_role = self.bot.api.get_one_role('7') + self.verified_role = self.bot.api.get_one_role('6') + self.mod_log = self.bot.api.get_one_log_setting('5') # mod_log + if self.mod_log['status'] != 'ok' or self.naughty_role['status'] != 'ok' or self.verified_role['status'] != 'ok': + raise RuntimeError("Failed to fetch mod log settings from API.") + else: + self.naughty_role = self.naughty_role['roles'][2] + self.verified_role = self.verified_role['roles'][2] + self.mod_log = self.mod_log['logging'][2] + @app_commands.command() @is_moderator() @is_master_guild() @@ -94,7 +100,7 @@ async def quarantine(self, interaction: discord.Interaction, target: discord.Mem if not target.guild_permissions.administrator: message_counter = 0 - mod_log = await self.bot.fetch_channel(self.mod_log[0]["logging"][2]) + mod_log = await self.bot.fetch_channel(self.mod_log["logging"][2]) verified_role = get(interaction.guild.roles, id=int(self.verified_role)) naughty_role = get(interaction.guild.roles, id=int(self.naughty_role)) @@ -144,7 +150,7 @@ async def release(self, interaction: discord.Interaction, target: discord.Member await interaction.response.defer() logger.info(f"{interaction.user.name} used the release command on {target.name}") if not target.bot: - mod_log = await self.bot.fetch_channel(self.mod_log[0]["logging"][2]) + mod_log = await self.bot.fetch_channel(self.mod_log["logging"][2]) verified_role = get(interaction.guild.roles, id=int(self.verified_role)) naughty_role = get(interaction.guild.roles, id=int(self.naughty_role)) diff --git a/src/bot/cogs/verification/verification_dropdown.py b/src/bot/cogs/verification/verification_dropdown.py index 16eaa56..010dc15 100644 --- a/src/bot/cogs/verification/verification_dropdown.py +++ b/src/bot/cogs/verification/verification_dropdown.py @@ -31,10 +31,10 @@ def embed_verified_success(name, amount): class VerificationSelector(discord.ui.Select): def __init__(self, bot): self.bot = bot - self.verified_role = self.bot.api.get_one_role('6')[0]['roles'][2] - self.join_log = self.bot.api.get_one_log_setting('2')[0]['logging'][2] - self.verification_log = self.bot.api.get_one_log_setting('1')[0]['logging'][2] - self.verification_channel = self.bot.api.get_one_setting('1')[0]['setting'][2] + self.verified_role = self.bot.api.get_one_role('6')['roles'][2] + self.join_log = self.bot.api.get_one_log_setting('2')['logging'][2] + self.verification_log = self.bot.api.get_one_log_setting('1')['logging'][2] + self.verification_channel = self.bot.api.get_one_setting('1')['setting'][2] self.robot = [discord.SelectOption( label="I'm a robot." @@ -97,9 +97,9 @@ class Verification(commands.Cog): def __init__(self, bot): self.bot = bot # Passed in from main.py - self.join_log = self.bot.api.get_one_log_setting('4')[0]['logging'][2] - self.verification_channel = self.bot.api.get_one_setting('1')[0]['setting'][2] - self.verified_role = self.bot.api.get_one_role('6')[0]['roles'][2] + self.join_log = self.bot.api.get_one_log_setting('4')['logging'][2] + self.verification_channel = self.bot.api.get_one_setting('1')['setting'][2] + self.verified_role = self.bot.api.get_one_role('6')['roles'][2] @commands.command() async def verify(self, ctx): diff --git a/src/bot/cogs/verification/verification_on_join.py b/src/bot/cogs/verification/verification_on_join.py index 2cc9591..0c3fc3b 100644 --- a/src/bot/cogs/verification/verification_on_join.py +++ b/src/bot/cogs/verification/verification_on_join.py @@ -19,11 +19,11 @@ class LoggingVerification(commands.Cog): def __init__(self, bot): self.bot = bot - self.verification_channel = self.bot.api.get_one_setting('1')[0]['setting'][2] - self.verification_log = self.bot.api.get_one_log_setting('1')[0]['logging'][2] - self.join_log = self.bot.api.get_one_log_setting('2')[0]['logging'][2] - self.verified_role = self.bot.api.get_one_role('6')[0]['roles'][2] - self.naughty_role = self.bot.api.get_one_role('7')[0]['roles'][2] + self.verification_channel = self.bot.api.get_one_setting('1')['setting'][2] + self.verification_log = self.bot.api.get_one_log_setting('1')['logging'][2] + self.join_log = self.bot.api.get_one_log_setting('2')['logging'][2] + self.verified_role = self.bot.api.get_one_role('6')['roles'][2] + self.naughty_role = self.bot.api.get_one_role('7')['roles'][2] async def log_unverified_join(self, member, logging_channel): await logging_channel.send(f"<@{member.id}> joined, but has not verified.") @@ -59,7 +59,6 @@ async def kick_if_not_verified(self, member, time_to_kick, logging_channel): async def on_member_join(self, member: discord.Member): guild = member.guild guild_id = member.guild.id - if guild_id != int(os.getenv("MASTER_GUILD")): logger.warning("on_member_join fired, but not in master guild. Ignoring event.") return diff --git a/src/bot/core/api_helper.py b/src/bot/core/api_helper.py index 7b84620..904b7e1 100644 --- a/src/bot/core/api_helper.py +++ b/src/bot/core/api_helper.py @@ -162,3 +162,42 @@ def update_points(self, user_id, amount): def top_10(self): logger.debug("Bot called the top_10 endpoint.") return requests.get(f"{self.api}/points/top10").json() + + + ################## + ## Ticket ## + ################## + + def get_ticket(self, thread_id=None): + """Retrieves ticket(s) from the database""" + logger.debug(f"Bot called the get_ticket endpoint. Thread ID: {thread_id if thread_id else 'All tickets'}") + if thread_id is None: + return requests.get(f"{self.api}/tickets").json() + else: + return requests.get(f"{self.api}/tickets/{thread_id}").json() + + def add_ticket(self,thread_id, user_id, channel_id): + """Adds a new ticket to the database""" + data = { + 'thread_id': thread_id, + 'creator_id': user_id, + 'channel_id': channel_id, + } + logger.debug(f"Bot called the add_ticket endpoint. sending data: {data}") + + return requests.post(f"{self.api}/tickets", json=data).json() + + def update_ticket_status(self, thread_id, new_status): + """Updates the status of an existing ticket in the database""" + logger.debug(f"Bot called the update_ticket_status endpoint. Thread ID: {thread_id} - New Status: {new_status}") + data = { + 'status': new_status + } + return requests.put(f"{self.api}/tickets/{thread_id}", json=data).json() + + def delete_ticket(self, thread_id): + """Deletes a ticket from the database""" + logger.debug(f"Bot called the delete_ticket endpoint. Thread ID: {thread_id}") + return requests.delete(f"{self.api}/tickets/{thread_id}").json() + + \ No newline at end of file diff --git a/src/bot/main.py b/src/bot/main.py index ff146ba..78a4ea1 100644 --- a/src/bot/main.py +++ b/src/bot/main.py @@ -10,7 +10,10 @@ discord.VoiceClient.warn_nacl = False logger = logging.getLogger(__name__) -setup_logger(level=int(os.getenv("BOT_LOG_LEVEL")), stream_logs=bool(os.getenv("STREAM_LOGS"))) +setup_logger( + level=int(os.getenv("BOT_LOG_LEVEL", "20")), + stream_logs=os.getenv("STREAM_LOGS", "true").lower() == "true" +) intents = discord.Intents.all() bot = commands.Bot(command_prefix=os.getenv("PREFIX"), intents=intents) @@ -87,9 +90,9 @@ def boink() -> None: logger.debug('Loading Token from arg.') bot.run(token) - elif os.environ['TOKEN'] is not None: # if not in args, check the env vars + elif token := os.environ.get('TOKEN'): # if not in args, check the env vars logger.debug('Loading Token from environment variable.') - bot.run(os.environ['TOKEN']) + bot.run(token) else: logger.critical('You must include a bot token...') diff --git a/src/db/init.sql b/src/db/init.sql index fb0fea7..15b096d 100644 --- a/src/db/init.sql +++ b/src/db/init.sql @@ -17,7 +17,8 @@ VALUES ('Verification Channel', '0'), ('Quarantine Channel', '0'), ('Staff Channel', '0'), - ('Bot Spam Channel', '0'); + ('Bot Spam Channel', '0'), + ('Ticket Channel', '0'); -- Create logging table @@ -62,4 +63,14 @@ CREATE TABLE IF NOT EXISTS users ( points int NOT NULL ); ALTER TABLE users - ADD CONSTRAINT unique_discord_id UNIQUE (discord_id); \ No newline at end of file + ADD CONSTRAINT unique_discord_id UNIQUE (discord_id); + +-- Create open tickets table +CREATE TABLE IF NOT EXISTS tickets ( + id SERIAL PRIMARY KEY, + thread_id BIGINT NOT NULL UNIQUE, + channel_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(50) DEFAULT 'open' +); \ No newline at end of file diff --git a/src/db/migrations.sql b/src/db/migrations.sql index 640af50..daaa685 100644 --- a/src/db/migrations.sql +++ b/src/db/migrations.sql @@ -1,5 +1,23 @@ BEGIN; - -- Put all your migrations commands here + -- Put all your migrations commands here. + -- It is HIGHLY recommended to use EXISTS guards to not + -- accidentally apply migrations more than once. + + INSERT INTO serversettings (name, value) + SELECT 'Ticket Channel', '0' + WHERE NOT EXISTS ( + SELECT * FROM serversettings + WHERE name='Ticket Channel' + ); + + CREATE TABLE IF NOT EXISTS tickets ( + id SERIAL PRIMARY KEY, + thread_id BIGINT NOT NULL UNIQUE, + channel_id BIGINT NOT NULL, + creator_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(50) DEFAULT 'open' + ); COMMIT;