From 9669556a3902b7c0fa55c0fe940402f54ccad673 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:56:13 +0200 Subject: [PATCH] Handle various file related issues (#76) * Create state files if they don't exist yet * Add notifications helper to message bot managers * Inform bot managers about errors if possible * Handle JSONDecodeErrors including empty files --- robocop_ng/__main__.py | 24 +++++++++++++ robocop_ng/helpers/disabled_ids.py | 16 ++++++++- robocop_ng/helpers/macros.py | 15 +++++++- robocop_ng/helpers/notifications.py | 53 +++++++++++++++++++++++++++++ robocop_ng/helpers/restrictions.py | 27 +++++++++++---- robocop_ng/helpers/robocronp.py | 18 ++++++++-- robocop_ng/helpers/roles.py | 14 +++++++- robocop_ng/helpers/userlogs.py | 18 ++++++++-- 8 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 robocop_ng/helpers/notifications.py diff --git a/robocop_ng/__main__.py b/robocop_ng/__main__.py index b9f780d..b0c1cd1 100755 --- a/robocop_ng/__main__.py +++ b/robocop_ng/__main__.py @@ -8,6 +8,8 @@ import discord from discord.ext import commands from discord.ext.commands import CommandError, Context +from robocop_ng.helpers.notifications import report_critical_error + if len(sys.argv[1:]) != 1: sys.stderr.write("usage: ") sys.exit(1) @@ -63,6 +65,9 @@ for wanted_json_idx in range(len(wanted_jsons)): wanted_jsons[wanted_json_idx] = os.path.join( state_dir, wanted_jsons[wanted_json_idx] ) + if not os.path.isfile(wanted_jsons[wanted_json_idx]): + with open(wanted_jsons[wanted_json_idx], "w") as file: + file.write("{}") intents = discord.Intents.all() intents.typing = False @@ -137,6 +142,25 @@ async def on_command(ctx): async def on_error(event: str, *args, **kwargs): log.exception(f"Error on {event}:") + exception = sys.exception() + is_report_allowed = any( + [ + not isinstance(exception, x) + for x in [ + discord.RateLimited, + discord.GatewayNotFound, + discord.InteractionResponded, + discord.LoginFailure, + ] + ] + ) + if exception is not None and is_report_allowed: + await report_critical_error( + bot, + exception, + additional_info={"Event": event, "args": args, "kwargs": kwargs}, + ) + @bot.event async def on_command_error(ctx: Context, error: CommandError): diff --git a/robocop_ng/helpers/disabled_ids.py b/robocop_ng/helpers/disabled_ids.py index 97d6bd5..c587b51 100644 --- a/robocop_ng/helpers/disabled_ids.py +++ b/robocop_ng/helpers/disabled_ids.py @@ -2,6 +2,8 @@ import json import os from typing import Union +from robocop_ng.helpers.notifications import report_critical_error + def get_disabled_ids_path(bot) -> str: return os.path.join(bot.state_dir, "data/disabled_ids.json") @@ -22,7 +24,19 @@ def is_ro_section_valid(ro_section: dict[str, str]) -> bool: def get_disabled_ids(bot) -> dict[str, dict[str, Union[str, dict[str, str]]]]: if os.path.isfile(get_disabled_ids_path(bot)): with open(get_disabled_ids_path(bot), "r") as f: - disabled_ids = json.load(f) + try: + disabled_ids = json.load(f) + except json.JSONDecodeError as e: + content = f.read() + report_critical_error( + bot, + e, + additional_info={ + "file": {"length": len(content), "content": content} + }, + ) + return {} + # Migration code if "app_id" in disabled_ids.keys(): old_disabled_ids = disabled_ids.copy() diff --git a/robocop_ng/helpers/macros.py b/robocop_ng/helpers/macros.py index 7d12502..f1fc243 100644 --- a/robocop_ng/helpers/macros.py +++ b/robocop_ng/helpers/macros.py @@ -2,6 +2,8 @@ import json import os from typing import Optional, Union +from robocop_ng.helpers.notifications import report_critical_error + def get_macros_path(bot): return os.path.join(bot.state_dir, "data/macros.json") @@ -10,7 +12,18 @@ def get_macros_path(bot): def get_macros_dict(bot) -> dict[str, dict[str, Union[list[str], str]]]: if os.path.isfile(get_macros_path(bot)): with open(get_macros_path(bot), "r") as f: - macros = json.load(f) + try: + macros = json.load(f) + except json.JSONDecodeError as e: + content = f.read() + report_critical_error( + bot, + e, + additional_info={ + "file": {"length": len(content), "content": content} + }, + ) + return {} # Migration code if "aliases" not in macros.keys(): diff --git a/robocop_ng/helpers/notifications.py b/robocop_ng/helpers/notifications.py new file mode 100644 index 0000000..02628cf --- /dev/null +++ b/robocop_ng/helpers/notifications.py @@ -0,0 +1,53 @@ +import json +from typing import Optional, Union + +from discord import Message, MessageReference, PartialMessage + + +MessageReferenceTypes = Union[Message, MessageReference, PartialMessage] + + +async def notify_management( + bot, message: str, reference_message: Optional[MessageReferenceTypes] = None +): + log_channel = await bot.get_channel_safe(bot.config.botlog_channel) + bot_manager_role = log_channel.guild.get_role(bot.config.bot_manager_role_id) + + notification_message = f"{bot_manager_role.mention}:\n" + + if reference_message is not None and reference_message.channel != log_channel: + notification_message += f"Message reference: {reference_message.jump_url}\n" + notification_message += message + + return await log_channel.send(notification_message) + else: + notification_message += message + + return await log_channel.send( + notification_message, + reference=reference_message, + mention_author=False, + ) + + +async def report_critical_error( + bot, + error: BaseException, + reference_message: Optional[MessageReferenceTypes] = None, + additional_info: Optional[dict] = None, +): + message = "⛔ A critical error occurred!" + + if additional_info is not None: + message += f""" + ```json + {json.dumps(additional_info)} + ```""" + + message += f""" + Exception: + ``` + {error} + ```""" + + return await notify_management(bot, message, reference_message) diff --git a/robocop_ng/helpers/restrictions.py b/robocop_ng/helpers/restrictions.py index a5bc1d4..9a229db 100644 --- a/robocop_ng/helpers/restrictions.py +++ b/robocop_ng/helpers/restrictions.py @@ -1,14 +1,28 @@ import json import os +from robocop_ng.helpers.notifications import report_critical_error + def get_restrictions_path(bot): return os.path.join(bot.state_dir, "data/restrictions.json") def get_restrictions(bot): - with open(get_restrictions_path(bot), "r") as f: - return json.load(f) + if os.path.isfile(get_restrictions_path(bot)): + with open(get_restrictions_path(bot), "r") as f: + try: + return json.load(f) + except json.JSONDecodeError as e: + content = f.read() + report_critical_error( + bot, + e, + additional_info={ + "file": {"length": len(content), "content": content} + }, + ) + return {} def set_restrictions(bot, contents): @@ -18,11 +32,10 @@ def set_restrictions(bot, contents): def get_user_restrictions(bot, uid): uid = str(uid) - with open(get_restrictions_path(bot), "r") as f: - rsts = json.load(f) - if uid in rsts: - return rsts[uid] - return [] + rsts = get_restrictions(bot) + if uid in rsts: + return rsts[uid] + return [] def add_restriction(bot, uid, rst): diff --git a/robocop_ng/helpers/robocronp.py b/robocop_ng/helpers/robocronp.py index 938fb4e..97334cd 100644 --- a/robocop_ng/helpers/robocronp.py +++ b/robocop_ng/helpers/robocronp.py @@ -2,14 +2,28 @@ import json import math import os +from robocop_ng.helpers.notifications import report_critical_error + def get_crontab_path(bot): return os.path.join(bot.state_dir, "data/robocronptab.json") def get_crontab(bot): - with open(get_crontab_path(bot), "r") as f: - return json.load(f) + if os.path.isfile(get_crontab_path(bot)): + with open(get_crontab_path(bot), "r") as f: + try: + return json.load(f) + except json.JSONDecodeError as e: + content = f.read() + report_critical_error( + bot, + e, + additional_info={ + "file": {"length": len(content), "content": content} + }, + ) + return {} def set_crontab(bot, contents): diff --git a/robocop_ng/helpers/roles.py b/robocop_ng/helpers/roles.py index f6bcac1..6c18d05 100644 --- a/robocop_ng/helpers/roles.py +++ b/robocop_ng/helpers/roles.py @@ -2,6 +2,8 @@ import json import os.path import os +from robocop_ng.helpers.notifications import report_critical_error + def get_persistent_roles_path(bot): return os.path.join(bot.state_dir, "data/persistent_roles.json") @@ -10,7 +12,17 @@ def get_persistent_roles_path(bot): def get_persistent_roles(bot) -> dict[str, list[str]]: if os.path.isfile(get_persistent_roles_path(bot)): with open(get_persistent_roles_path(bot), "r") as f: - return json.load(f) + try: + return json.load(f) + except json.JSONDecodeError as e: + content = f.read() + report_critical_error( + bot, + e, + additional_info={ + "file": {"length": len(content), "content": content} + }, + ) return {} diff --git a/robocop_ng/helpers/userlogs.py b/robocop_ng/helpers/userlogs.py index 643a1df..6541e49 100644 --- a/robocop_ng/helpers/userlogs.py +++ b/robocop_ng/helpers/userlogs.py @@ -2,6 +2,8 @@ import json import os import time +from robocop_ng.helpers.notifications import report_critical_error + userlog_event_types = { "warns": "Warn", "bans": "Ban", @@ -16,8 +18,20 @@ def get_userlog_path(bot): def get_userlog(bot): - with open(get_userlog_path(bot), "r") as f: - return json.load(f) + if os.path.isfile(get_userlog_path(bot)): + with open(get_userlog_path(bot), "r") as f: + try: + return json.load(f) + except json.JSONDecodeError as e: + content = f.read() + report_critical_error( + bot, + e, + additional_info={ + "file": {"length": len(content), "content": content} + }, + ) + return {} def set_userlog(bot, contents):