diff --git a/requirements.txt b/requirements.txt index e3dc74a8..e7b5a9e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ flask>=1.0.3 markdown2>=2.3.8 mcstatus>=2.2.1 sortedcontainers>=2.1.0 +sentry-sdk>=0.11.1 diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index e5e4f27c..fb9d5c73 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -1,5 +1,6 @@ import discord import typing +import sentry_sdk import logging as _logging from .generic import GenericBot from ..utils import * @@ -113,9 +114,17 @@ class DiscordBot(GenericBot): except KeyError: # Skip the message return + # Prepare data + data = self._Data(interface=command.interface, message=message) # Call the command with message.channel.typing(): - await command.run(CommandArgs(parameters), self._Data(interface=command.interface, message=message)) + try: + await command.run(CommandArgs(parameters), data=data) + except Exception as e: + sentry_sdk.capture_exception(e) + error_message = f"⛔️ {e.__class__.__name__}\n" + error_message += '\n'.join(e.args) + await data.reply(error_message) async def on_ready(cli): log.debug("Connection successful, client is ready") @@ -189,9 +198,11 @@ class DiscordBot(GenericBot): discord_config: DiscordConfig, royalnet_config: typing.Optional[RoyalnetConfig] = None, database_config: typing.Optional[DatabaseConfig] = None, + sentry_dsn: typing.Optional[str] = None, commands: typing.List[typing.Type[Command]] = None): super().__init__(royalnet_config=royalnet_config, database_config=database_config, + sentry_dsn=sentry_dsn, commands=commands) self._discord_config = discord_config self._init_client() diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index 18df4b8d..78188f96 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -2,6 +2,7 @@ import sys import typing import asyncio import logging +import sentry_sdk from ..utils import * from ..network import * from ..database import * @@ -124,25 +125,34 @@ class GenericBot: royalnet_config: typing.Optional[RoyalnetConfig] = None, database_config: typing.Optional[DatabaseConfig] = None, commands: typing.List[typing.Type[Command]] = None, + sentry_dsn: typing.Optional[str] = None, loop: asyncio.AbstractEventLoop = None): if loop is None: self.loop = asyncio.get_event_loop() else: self.loop = loop - if database_config is None: - self.alchemy = None - self.master_table = None - self.identity_table = None - self.identity_column = None + if sentry_dsn: + log.debug("Sentry int") + self.sentry = sentry_sdk.init(sentry_dsn) else: - self._init_database(commands=commands, database_config=database_config) - if commands is None: - commands = [] - self._init_commands(commands) - if royalnet_config is None: - self.network = None - else: - self._init_royalnet(royalnet_config=royalnet_config) + log.debug("Sentry integration not enabled") + try: + if database_config is None: + self.alchemy = None + self.master_table = None + self.identity_table = None + self.identity_column = None + else: + self._init_database(commands=commands, database_config=database_config) + if commands is None: + commands = [] + self._init_commands(commands) + if royalnet_config is None: + self.network = None + else: + self._init_royalnet(royalnet_config=royalnet_config) + except Exception as e: + sentry_sdk.capture_exception(e) async def run(self): """A blocking coroutine that should make the bot start listening to commands and requests.""" diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index 2247bea4..18225e94 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -1,6 +1,8 @@ import telegram import telegram.utils.request import typing +import asyncio +import sentry_sdk import logging as _logging from .generic import GenericBot from ..utils import * @@ -74,9 +76,11 @@ class TelegramBot(GenericBot): telegram_config: TelegramConfig, royalnet_config: typing.Optional[RoyalnetConfig] = None, database_config: typing.Optional[DatabaseConfig] = None, + sentry_dsn: typing.Optional[str] = None, commands: typing.List[typing.Type[Command]] = None): super().__init__(royalnet_config=royalnet_config, database_config=database_config, + sentry_dsn=sentry_dsn, commands=commands) self._telegram_config = telegram_config self._init_client() @@ -107,16 +111,27 @@ class TelegramBot(GenericBot): except KeyError: # Skip the message return + # Prepare data + data = self._Data(interface=command.interface, update=update) # Run the command - await command.run(CommandArgs(parameters), self._Data(interface=command.interface, update=update)) + try: + await command.run(CommandArgs(parameters), data) + except Exception as e: + sentry_sdk.capture_exception(e) + error_message = f"⛔️ [b]{e.__class__.__name__}[/b]\n" + error_message += '\n'.join(e.args) + await data.reply(error_message) async def run(self): while True: # Get the latest 100 updates try: - last_updates: typing.List[telegram.Update] = await asyncify(self.client.get_updates, offset=self._offset, timeout=60) - except telegram.error.TimedOut: - continue + last_updates: typing.List[telegram.Update] = await asyncify(self.client.get_updates, + offset=self._offset, + timeout=60) + except telegram.error.TelegramError as error: + sentry_sdk.capture_exception(error) + await asyncio.sleep(5) # Handle updates for update in last_updates: # noinspection PyAsyncCall diff --git a/royalnet/commands/debug/__init__.py b/royalnet/commands/debug/__init__.py new file mode 100644 index 00000000..f30b8a26 --- /dev/null +++ b/royalnet/commands/debug/__init__.py @@ -0,0 +1,3 @@ +from .debug_error import DebugErrorCommand + +__all__ = ["DebugErrorCommand"] diff --git a/royalnet/commands/debug/debug_error.py b/royalnet/commands/debug/debug_error.py new file mode 100644 index 00000000..35e79b55 --- /dev/null +++ b/royalnet/commands/debug/debug_error.py @@ -0,0 +1,13 @@ +import typing +from ..command import Command +from ..commandargs import CommandArgs +from ..commanddata import CommandData + + +class DebugErrorCommand(Command): + name: str = "debug_error" + + description: str = "Causa un'eccezione nel bot." + + async def run(self, args: CommandArgs, data: CommandData) -> None: + raise Exception("debug_error command was called") diff --git a/royalnet/royalgames.py b/royalnet/royalgames.py index 1c4ec9a4..4b3042c6 100644 --- a/royalnet/royalgames.py +++ b/royalnet/royalgames.py @@ -3,8 +3,10 @@ import os import asyncio import logging +import sentry_sdk from royalnet.bots import DiscordBot, DiscordConfig, TelegramBot, TelegramConfig from royalnet.commands.royalgames import * +from royalnet.commands.debug import * from royalnet.network import RoyalnetServer, RoyalnetConfig from royalnet.database import DatabaseConfig from royalnet.database.tables import Royal, Telegram, Discord @@ -38,9 +40,12 @@ commands = [ DndspellCommand ] +sentry_dsn = os.environ.get("SENTRY_DSN") + # noinspection PyUnreachableCode if __debug__: log.setLevel(logging.DEBUG) + commands = [*commands, DebugErrorCommand] else: log.setLevel(logging.INFO) @@ -54,10 +59,12 @@ print("Starting bots...") ds_bot = DiscordBot(discord_config=DiscordConfig(os.environ["DS_AK"]), royalnet_config=RoyalnetConfig(f"ws://{address}:{port}", os.environ["MASTER_KEY"]), database_config=DatabaseConfig(os.environ["DB_PATH"], Royal, Discord, "discord_id"), + sentry_dsn=sentry_dsn, commands=commands) tg_bot = TelegramBot(telegram_config=TelegramConfig(os.environ["TG_AK"]), royalnet_config=RoyalnetConfig(f"ws://{address}:{port}", os.environ["MASTER_KEY"]), database_config=DatabaseConfig(os.environ["DB_PATH"], Royal, Telegram, "tg_id"), + sentry_dsn=sentry_dsn, commands=commands) loop.create_task(tg_bot.run()) loop.create_task(ds_bot.run()) diff --git a/setup.py b/setup.py index 48402a00..3c0be22d 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,8 @@ setuptools.setup( "flask>=1.0.3", "markdown2>=2.3.8", "mcstatus>=2.2.1", - "sortedcontainers>=2.1.0"], + "sortedcontainers>=2.1.0", + "sentry-sdk>=0.11.1"], python_requires=">=3.7", classifiers=[ "Development Status :: 3 - Alpha",