diff --git a/.gitignore b/.gitignore index 779a33ad..b0f5ad08 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,7 @@ config.ini .idea/ .vscode/ __pycache__ -diario.json -libopus-0.dll -music.opus -opusfiles/ +downloads/ ignored/ markovmodels/ logs/ diff --git a/royalnet/audio/__init__.py b/royalnet/audio/__init__.py index 6f10a592..3c4a9547 100644 --- a/royalnet/audio/__init__.py +++ b/royalnet/audio/__init__.py @@ -2,4 +2,4 @@ from .playmodes import PlayMode, Playlist, Pool from .youtubedl import YtdlFile, YtdlInfo from .royalaudiofile import RoyalAudioFile -__all__ = ["PlayMode", "Playlist", "Pool", "YtdlFile", "YtdlInfo"] +__all__ = ["PlayMode", "Playlist", "Pool", "YtdlFile", "YtdlInfo", "RoyalAudioFile"] diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index 4331e778..31226655 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -4,10 +4,11 @@ import typing import logging as _logging import sys from ..commands import NullCommand -from ..commands.summon import SummonMessage, SummonSuccessful, SummonError -from ..commands.play import PlayMessage, PlaySuccessful, PlayError -from ..utils import asyncify, Call, Command, UnregisteredError -from ..network import RoyalnetLink, Message +from ..commands.summon import SummonMessage +from ..commands.play import PlayMessage +from ..utils import asyncify, Call, Command +from royalnet.error import UnregisteredError +from ..network import RoyalnetLink, Message, RequestSuccessful, RequestError from ..database import Alchemy, relationshiplinkchain from ..audio import RoyalAudioFile @@ -146,12 +147,12 @@ class DiscordBot: if channel.name == message.channel_name: matching_channels.append(channel) if len(matching_channels) == 0: - return SummonError("No channels with a matching name found") + return RequestError("No channels with a matching name found") elif len(matching_channels) > 1: - return SummonError("Multiple channels with a matching name found") + return RequestError("Multiple channels with a matching name found") matching_channel = matching_channels[0] await self.bot.vc_connect_or_move(matching_channel) - return SummonSuccessful() + return RequestSuccessful() async def nh_play(self, message: PlayMessage): # TODO: actually do what's intended to do @@ -163,7 +164,7 @@ class DiscordBot: for voice_client in self.bot.voice_clients: voice_client: discord.VoiceClient voice_client.play(audio_source) - return PlaySuccessful() + return RequestError() async def run(self): await self.bot.login(self.token) diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index 32a27c7a..cf45ab72 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -4,7 +4,8 @@ import typing import logging as _logging import sys from ..commands import NullCommand -from ..utils import asyncify, Call, Command, UnregisteredError +from ..utils import asyncify, Call, Command +from royalnet.error import UnregisteredError from ..network import RoyalnetLink, Message from ..database import Alchemy, relationshiplinkchain diff --git a/royalnet/commands/dateparser.py b/royalnet/commands/dateparser.py index ac2bf6c7..f7f44522 100644 --- a/royalnet/commands/dateparser.py +++ b/royalnet/commands/dateparser.py @@ -1,6 +1,7 @@ import datetime import dateparser -from ..utils import Command, Call, InvalidInputError +from ..utils import Command, Call +from ..error import InvalidInputError class DateparserCommand(Command): @@ -11,9 +12,7 @@ class DateparserCommand(Command): @classmethod async def common(cls, call: Call): - if len(call.args) == 0: - raise InvalidInputError("Missing arg") - text = " ".join(call.args) + text = call.args.joined(require_at_least=1) date: datetime.datetime = dateparser.parse(text) if date is None: await call.reply("🕕 La data inserita non è valida.") diff --git a/royalnet/commands/diario.py b/royalnet/commands/diario.py index c078cffc..98859009 100644 --- a/royalnet/commands/diario.py +++ b/royalnet/commands/diario.py @@ -4,7 +4,8 @@ import telegram import typing import os import aiohttp -from ..utils import Command, Call, InvalidInputError, InvalidConfigError, ExternalError +from ..utils import Command, Call +from ..error import InvalidInputError, InvalidConfigError, ExternalError from ..database.tables import Royal, Diario, Alias from ..utils import asyncify diff --git a/royalnet/commands/error_handler.py b/royalnet/commands/error_handler.py index de90fb5c..ff3b2d82 100644 --- a/royalnet/commands/error_handler.py +++ b/royalnet/commands/error_handler.py @@ -1,6 +1,13 @@ import traceback from logging import Logger -from ..utils import Command, CommandArgs, Call, InvalidInputError, UnsupportedError, UnregisteredError +from ..utils import Command, CommandArgs, Call +from ..error import NoneFoundError, \ + TooManyFoundError, \ + UnregisteredError, \ + UnsupportedError, \ + InvalidInputError, \ + InvalidConfigError, \ + ExternalError class ErrorHandlerCommand(Command): @@ -16,13 +23,28 @@ class ErrorHandlerCommand(Command): except InvalidInputError: await call.reply("⚠️ Questo comando non può essere chiamato da solo.") return - if e_type == InvalidInputError: - command = call.kwargs["previous_command"] - await call.reply(f"⚠️ Sintassi non valida.\nSintassi corretta: [c]{call.interface_prefix}{command.command_name} {command.command_syntax}[/c]") + if e_type == NoneFoundError: + await call.reply("⚠️ L'elemento richiesto non è stato trovato.") + return + if e_type == TooManyFoundError: + await call.reply("⚠️ La richiesta effettuata è ambigua, pertanto è stata annullata.") return if e_type == UnregisteredError: await call.reply("⚠️ Devi essere registrato a Royalnet per usare questo comando!") return + if e_type == UnsupportedError: + await call.reply("⚠️ Il comando richiesto non è disponibile tramite questa interfaccia.") + return + if e_type == InvalidInputError: + command = call.kwargs["previous_command"] + await call.reply(f"⚠️ Sintassi non valida.\nSintassi corretta: [c]{call.interface_prefix}{command.command_name} {command.command_syntax}[/c]") + return + if e_type == InvalidConfigError: + await call.reply("⚠️ Il bot non è stato configurato correttamente, quindi questo comando non può essere eseguito. L'errore è stato segnalato all'amministratore.") + return + if e_type == ExternalError: + await call.reply("⚠️ Una risorsa esterna necessaria per l'esecuzione del comando non ha funzionato correttamente, quindi il comando è stato annullato.") + return await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{e_type.__name__}[/b]\n{e_value}") formatted_tb: str = '\n'.join(traceback.format_tb(e_tb)) call.logger.error(f"Unhandled exception - {e_type.__name__}: {e_value}\n{formatted_tb}") diff --git a/royalnet/commands/ping.py b/royalnet/commands/ping.py index df280dd7..489ea900 100644 --- a/royalnet/commands/ping.py +++ b/royalnet/commands/ping.py @@ -1,5 +1,6 @@ import asyncio -from ..utils import Command, Call, InvalidInputError +from ..utils import Command, Call +from royalnet.error import InvalidInputError class PingCommand(Command): diff --git a/royalnet/commands/play.py b/royalnet/commands/play.py index f4d0dd47..09c569f1 100644 --- a/royalnet/commands/play.py +++ b/royalnet/commands/play.py @@ -1,6 +1,6 @@ import typing from ..utils import Command, Call -from ..network import Message +from ..network import Message, RequestSuccessful, RequestError class PlayMessage(Message): @@ -8,15 +8,6 @@ class PlayMessage(Message): self.url: str = url -class PlaySuccessful(Message): - pass - - -class PlayError(Message): - def __init__(self, reason: str): - self.reason: str = reason - - class PlayCommand(Command): command_name = "play" command_description = "Riproduce una canzone in chat vocale." @@ -25,11 +16,11 @@ class PlayCommand(Command): @classmethod async def common(cls, call: Call): url: str = call.args[0] - response: typing.Union[PlaySuccessful, PlayError] = await call.net_request(PlayMessage(url), "discord") - if isinstance(response, PlayError): - await call.reply(f"⚠️ Si è verificato un'errore nella richiesta di riproduzione:\n[c]{response.reason}[/c]") - return - elif isinstance(response, PlaySuccessful): + response: typing.Union[RequestSuccessful, RequestError] = await call.net_request(PlayMessage(url), "discord") + if isinstance(response, RequestSuccessful): await call.reply(f"✅ Richiesta la riproduzione di [c]{url}[/c].") return + elif isinstance(response, RequestError): + await call.reply(f"⚠️ Si è verificato un'errore nella richiesta di riproduzione:\n[c]{response.reason}[/c]") + return raise TypeError(f"Received unexpected response in the PlayCommand: {response.__class__.__name__}") diff --git a/royalnet/commands/summon.py b/royalnet/commands/summon.py index 5b43bf0c..8951b167 100644 --- a/royalnet/commands/summon.py +++ b/royalnet/commands/summon.py @@ -1,7 +1,7 @@ import typing import discord from ..utils import Command, Call -from ..network import Message +from ..network import Message, RequestSuccessful, RequestError class SummonMessage(Message): @@ -9,15 +9,6 @@ class SummonMessage(Message): self.channel_name: str = channel_name -class SummonSuccessful(Message): - pass - - -class SummonError(Message): - def __init__(self, reason: str): - self.reason: str = reason - - class SummonCommand(Command): command_name = "summon" @@ -27,11 +18,11 @@ class SummonCommand(Command): @classmethod async def common(cls, call: Call): channel_name: str = call.args[0].lstrip("#") - response: typing.Union[SummonSuccessful, SummonError] = await call.net_request(SummonMessage(channel_name), "discord") - if isinstance(response, SummonError): - await call.reply(f"⚠️ Si è verificato un'errore nella richiesta di connessione:\n[c]{response.reason}[/c]") + response: typing.Union[RequestSuccessful, RequestError] = await call.net_request(SummonMessage(channel_name), "discord") + if isinstance(response, RequestError): + await call.reply(f"⚠️ Si è verificato un'errore nella richiesta di connessione:\n[c]{response.exc}[/c]") return - elif isinstance(response, SummonSuccessful): + elif isinstance(response, RequestSuccessful): await call.reply(f"✅ Mi sono connesso in [c]#{channel_name}[/c].") return raise TypeError(f"Received unexpected response type while summoning the bot: {response.__class__.__name__}") diff --git a/royalnet/commands/sync.py b/royalnet/commands/sync.py index 8601a3a3..128867c9 100644 --- a/royalnet/commands/sync.py +++ b/royalnet/commands/sync.py @@ -1,7 +1,8 @@ import typing from telegram import Update, User from discord import Message, Member -from ..utils import Command, Call, asyncify, UnsupportedError +from ..utils import Command, Call, asyncify +from royalnet.error import UnsupportedError from ..database.tables import Royal, Telegram, Discord diff --git a/royalnet/error.py b/royalnet/error.py new file mode 100644 index 00000000..206ad0aa --- /dev/null +++ b/royalnet/error.py @@ -0,0 +1,26 @@ +class NoneFoundError(Exception): + """The element that was being looked for was not found.""" + + +class TooManyFoundError(Exception): + """Multiple elements matching the request were found, and only one was expected.""" + + +class UnregisteredError(Exception): + """The command required a registered user, and the user was not registered.""" + + +class UnsupportedError(Exception): + """The command is not supported for the specified interface.""" + + +class InvalidInputError(Exception): + """The command has received invalid input and cannot complete.""" + + +class InvalidConfigError(Exception): + """The bot has not been configured correctly, therefore the command can not function.""" + + +class ExternalError(Exception): + """Something went wrong in a non-Royalnet component and the command execution cannot be completed.""" diff --git a/royalnet/network/__init__.py b/royalnet/network/__init__.py index 1f279e15..af1615d7 100644 --- a/royalnet/network/__init__.py +++ b/royalnet/network/__init__.py @@ -1,4 +1,4 @@ -from .messages import Message, ServerErrorMessage, InvalidSecretEM, InvalidDestinationEM, InvalidPackageEM +from .messages import Message, ServerErrorMessage, InvalidSecretEM, InvalidDestinationEM, InvalidPackageEM, RequestSuccessful, RequestError from .packages import Package from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError from .royalnetserver import RoyalnetServer @@ -13,4 +13,6 @@ __all__ = ["Message", "NotConnectedError", "NotIdentifiedError", "Package", - "RoyalnetServer"] + "RoyalnetServer", + "RequestSuccessful", + "RequestError"] diff --git a/royalnet/network/messages.py b/royalnet/network/messages.py index 97a95563..49dd5af1 100644 --- a/royalnet/network/messages.py +++ b/royalnet/network/messages.py @@ -23,3 +23,12 @@ class InvalidPackageEM(ServerErrorMessage): class InvalidDestinationEM(InvalidPackageEM): pass + + +class RequestSuccessful(Message): + pass + + +class RequestError(Message): + def __init__(self, exc: Exception): + self.exc = exc diff --git a/royalnet/network/royalnetlink.py b/royalnet/network/royalnetlink.py index a2bfb34f..bffe8db4 100644 --- a/royalnet/network/royalnetlink.py +++ b/royalnet/network/royalnetlink.py @@ -5,7 +5,7 @@ import functools import typing import pickle import logging as _logging -from .messages import Message, ServerErrorMessage +from .messages import Message, ServerErrorMessage, RequestError from .packages import Package loop = asyncio.get_event_loop() @@ -29,7 +29,7 @@ class NetworkError(Exception): class PendingRequest: def __init__(self): self.event: asyncio.Event = asyncio.Event() - self.data: Message = None + self.data: typing.Optional[Message] = None def __repr__(self): if self.event.is_set(): @@ -139,8 +139,11 @@ class RoyalnetLink: # Package is a request assert isinstance(package, Package) log.debug(f"Received request {package.source_conv_id}: {package}") - response = await self.request_handler(package.data) - if response is not None: - response_package: Package = package.reply(response) - await self.send(response_package) - log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}") + try: + response = await self.request_handler(package.data) + except Exception as exc: + response = RequestError(exc=exc) + return + response_package: Package = package.reply(response) + await self.send(response_package) + log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}") diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index 65c2718c..fa04fc33 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -1,10 +1,10 @@ from .asyncify import asyncify -from .call import Call, UnregisteredError -from .command import Command, CommandArgs, InvalidInputError, UnsupportedError, InvalidConfigError, ExternalError +from .call import Call +from .command import Command, CommandArgs from .safeformat import safeformat from .classdictjanitor import cdj from .sleepuntil import sleep_until from .plusformat import plusformat -__all__ = ["asyncify", "Call", "Command", "safeformat", "InvalidInputError", "UnsupportedError", "CommandArgs", - "cdj", "InvalidConfigError", "ExternalError", "sleep_until", "UnregisteredError", "plusformat"] +__all__ = ["asyncify", "Call", "Command", "safeformat", "CommandArgs", + "cdj", "sleep_until", "plusformat"] diff --git a/royalnet/utils/call.py b/royalnet/utils/call.py index a3e15f6a..a91bd618 100644 --- a/royalnet/utils/call.py +++ b/royalnet/utils/call.py @@ -10,10 +10,6 @@ if typing.TYPE_CHECKING: loop = asyncio.get_event_loop() -class UnregisteredError(Exception): - pass - - class Call: """A command call. Still an abstract class, subbots should create a new call from this.""" diff --git a/royalnet/utils/command.py b/royalnet/utils/command.py index e8ff76ee..cdb8cec3 100644 --- a/royalnet/utils/command.py +++ b/royalnet/utils/command.py @@ -1,25 +1,12 @@ import re import typing + +from royalnet.error import InvalidInputError + if typing.TYPE_CHECKING: from .call import Call -class UnsupportedError(Exception): - """The command is not supported for the specified source.""" - - -class InvalidInputError(Exception): - """The command has received invalid input and cannot complete.""" - - -class InvalidConfigError(Exception): - """The bot has not been configured correctly, therefore the command can not function.""" - - -class ExternalError(Exception): - """Something went wrong in a non-Royalnet component and the command cannot be executed fully.""" - - class CommandArgs(list): """The arguments of a command. Raises InvalidInputError if the requested argument does not exist.""" @@ -36,8 +23,13 @@ class CommandArgs(list): raise InvalidInputError(f'Tried to get invalid [{item}] slice from CommandArgs') raise ValueError(f"Invalid type passed to CommandArgs.__getattr__: {type(item)}") + def joined(self, *, require_at_least=0): + if len(self) < require_at_least: + raise InvalidInputError("Not enough arguments") + return " ".join(self) + def match(self, pattern: typing.Pattern) -> typing.Match: - text = " ".join(self) + text = self.joined() match = re.match(pattern, text) if match is None: raise InvalidInputError("Pattern didn't match")