diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index efe82784..3af8100d 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -2,6 +2,7 @@ import telegram import asyncio import typing import logging as _logging +import sys from ..commands import NullCommand from ..utils import asyncify, Call, Command from ..network import RoyalnetLink, Message @@ -120,9 +121,17 @@ class TelegramBot: command = self.missing_command # Call the command try: - return await self.Call(message.chat, command, *parameters, update=update).run() + return await self.Call(message.chat, command, parameters, + update=update).run() except Exception as exc: - return await self.Call(message.chat, self.error_command, *parameters, update=update, exception=exc, previous_command=command).run() + try: + return await self.Call(message.chat, self.error_command, parameters, + update=update, + exception_info=sys.exc_info(), + previous_command=command, + log=log).run() + except Exception as exc2: + log.error(f"Exception in error handler command: {exc2}") async def handle_net_request(self, message: Message): pass diff --git a/royalnet/commands/ciaoruozi.py b/royalnet/commands/ciaoruozi.py index c33ca88c..1174ce2d 100644 --- a/royalnet/commands/ciaoruozi.py +++ b/royalnet/commands/ciaoruozi.py @@ -8,8 +8,8 @@ class CiaoruoziCommand(Command): command_title = "Saluta Ruozi, anche se non Γ¨ piΓΉ in RYG." command_syntax = "" - async def telegram(self, call: Call, args: CommandArgs): - update: Update = args.kwargs["update"] + async def telegram(self, call: Call): + update: Update = call.kwargs["update"] user: User = update.effective_user if user.id == 112437036: await call.reply("πŸ‘‹ Ciao me!") diff --git a/royalnet/commands/color.py b/royalnet/commands/color.py index e76ed11c..d43d7332 100644 --- a/royalnet/commands/color.py +++ b/royalnet/commands/color.py @@ -7,7 +7,7 @@ class ColorCommand(Command): command_title = "Invia un colore in chat...?" command_syntax = "" - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): await call.reply(""" [i]I am sorry, unknown error occured during working with your request, Admin were notified[/i] """) diff --git a/royalnet/commands/debug_author.py b/royalnet/commands/debug_author.py index 011c10a6..e3b3285c 100644 --- a/royalnet/commands/debug_author.py +++ b/royalnet/commands/debug_author.py @@ -10,7 +10,7 @@ class DebugAuthorCommand(Command): require_alchemy_tables = {Royal, Telegram} - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): author = await call.get_author() if author is None: await call.reply(f"☁️ L'autore di questa chiamata Γ¨ sconosciuto.") diff --git a/royalnet/commands/debug_create.py b/royalnet/commands/debug_create.py index d69c5d18..4804a2ab 100644 --- a/royalnet/commands/debug_create.py +++ b/royalnet/commands/debug_create.py @@ -10,10 +10,10 @@ class DebugCreateCommand(Command): require_alchemy_tables = {Royal, Alias} - async def common(self, call: Call, args: CommandArgs): - royal = call.alchemy.Royal(username=args[0], role="Member") + async def common(self, call: Call): + royal = call.alchemy.Royal(username=call.args[0], role="Member") call.session.add(royal) alias = call.alchemy.Alias(royal=royal, alias=royal.username) call.session.add(alias) await asyncify(call.session.commit) - await call.reply(f"βœ… Utente {royal} creato!") + await call.reply(f"βœ… Utente [c]{royal}[/c] creato!") diff --git a/royalnet/commands/diario.py b/royalnet/commands/diario.py index b4339828..55f35574 100644 --- a/royalnet/commands/diario.py +++ b/royalnet/commands/diario.py @@ -13,18 +13,28 @@ class DiarioCommand(Command): require_alchemy_tables = {Royal, Diario, Alias} - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): + # Find the creator of the quotes + creator = await call.get_author() + if creator is None: + await call.reply("⚠️ Devi essere registrato a Royalnet per usare questo comando!") + return # Recreate the full sentence - text = " ".join(args.args) + raw_text = " ".join(call.args) # Pass the sentence through the diario regex - match = re.match(r'(!)? *["Β«β€˜β€œβ€›β€Ÿβ›βγ€οΌ‚`]([^"]+)["Β»β€™β€βœβžγ€žοΌ‚`] *(?:(?:-{1,2}|β€”) *([\w ]+))?(?:, *([^ ].*))?', text) + match = re.match(r'(!)? *["Β«β€˜β€œβ€›β€Ÿβ›βγ€οΌ‚`]([^"]+)["Β»β€™β€βœβžγ€žοΌ‚`] *(?:(?:-{1,2}|β€”) *([\w ]+))?(?:, *([^ ].*))?', raw_text) # Find the corresponding matches - if match is None: - raise InvalidInputError("No match found.") - spoiler = bool(match.group(1)) - text = match.group(2) - quoted = match.group(3) - context = match.group(4) + if match is not None: + spoiler = bool(match.group(1)) + text = match.group(2) + quoted = match.group(3) + context = match.group(4) + # Otherwise, consider everything part of the text + else: + spoiler = False + text = raw_text + quoted = None + context = None timestamp = datetime.datetime.now() # Find if there's a Royalnet account associated with the quoted name if quoted is not None: @@ -32,8 +42,6 @@ class DiarioCommand(Command): else: quoted_alias = None quoted_account = quoted_alias.royal if quoted_alias is not None else None - # Find the creator of the quotes - creator = await call.get_author() # Create the diario quote diario = call.alchemy.Diario(creator=creator, quoted_account=quoted_account, @@ -45,4 +53,4 @@ class DiarioCommand(Command): spoiler=spoiler) call.session.add(diario) await asyncify(call.session.commit) - await call.reply(f"βœ… Aggiunto al diario!") + await call.reply(f"βœ… {str(diario)}") diff --git a/royalnet/commands/error_handler.py b/royalnet/commands/error_handler.py index 68b7c0ae..787d8ec9 100644 --- a/royalnet/commands/error_handler.py +++ b/royalnet/commands/error_handler.py @@ -1,5 +1,6 @@ -from ..utils import Command, CommandArgs, Call, InvalidInputError - +import traceback +from logging import Logger +from ..utils import Command, CommandArgs, Call, InvalidInputError, UnsupportedError class ErrorHandlerCommand(Command): @@ -7,14 +8,20 @@ class ErrorHandlerCommand(Command): command_title = "Gestisce gli errori causati dagli altri comandi." command_syntax = "" - async def telegram(self, call: Call, args: CommandArgs): + async def common(self, call: Call): + raise UnsupportedError() + + async def telegram(self, call: Call): try: - exc = args.kwargs["exception"] + e_type, e_value, e_tb = call.kwargs["exception_info"] except InvalidInputError: await call.reply("⚠️ Questo comando non puΓ² essere chiamato da solo.") return - if isinstance(exc, InvalidInputError): - command = args.kwargs["previous_command"] + if e_type == InvalidInputError: + command = call.kwargs["previous_command"] await call.reply(f"⚠️ Sintassi non valida.\nSintassi corretta:[c]/{command.command_name} {command.command_syntax}[/c]") return - await call.reply("❌ Eccezione non gestita durante l'esecuzione del comando.") + await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{e_type.__name__}[/b]\n{e_value}") + log: Logger = call.kwargs["log"] + formatted_tb: str = '\n'.join(traceback.format_tb(e_tb)) + log.error(f"Unhandled exception - {e_type.__name__}: {e_value}\n{formatted_tb}") diff --git a/royalnet/commands/null.py b/royalnet/commands/null.py index 6d281c1d..baa97875 100644 --- a/royalnet/commands/null.py +++ b/royalnet/commands/null.py @@ -7,5 +7,5 @@ class NullCommand(Command): command_title = "Non fa nulla." command_syntax = "" - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): pass diff --git a/royalnet/commands/ping.py b/royalnet/commands/ping.py index da7a99b6..c5a70fbb 100644 --- a/royalnet/commands/ping.py +++ b/royalnet/commands/ping.py @@ -7,5 +7,5 @@ class PingCommand(Command): command_title = "Ping pong!" command_syntax = "" - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): await call.reply("πŸ“ Pong!") diff --git a/royalnet/commands/ship.py b/royalnet/commands/ship.py index 635357aa..2734fd7a 100644 --- a/royalnet/commands/ship.py +++ b/royalnet/commands/ship.py @@ -11,11 +11,11 @@ class ShipCommand(Command): command_title = "Crea una ship tra due cose." command_syntax = "(uno) (due)" - async def common(self, call: Call, args: CommandArgs): - name_one = args[0] - name_two = args[1] + async def common(self, call: Call): + name_one = call.args[0] + name_two = call.args[1] if name_two == "+": - name_two = args[2] + name_two = call.args[2] name_one = name_one.lower() name_two = name_two.lower() # Get all letters until the first vowel, included diff --git a/royalnet/commands/smecds.py b/royalnet/commands/smecds.py index 0f200573..52d08659 100644 --- a/royalnet/commands/smecds.py +++ b/royalnet/commands/smecds.py @@ -57,6 +57,6 @@ class SmecdsCommand(Command): command_title = "Secondo me, Γ¨ colpa dello stagista..." command_syntax = "" - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): ds = random.sample(DS_LIST, 1)[0] return await call.reply(safeformat(SMECDS, ds=ds)) diff --git a/royalnet/commands/sync.py b/royalnet/commands/sync.py index 3c67cdd2..f4863834 100644 --- a/royalnet/commands/sync.py +++ b/royalnet/commands/sync.py @@ -12,17 +12,17 @@ class SyncCommand(Command): require_alchemy_tables = {Royal, Telegram} - async def common(self, call: Call, args: CommandArgs): + async def common(self, call: Call): raise UnsupportedError() - async def telegram(self, call: Call, args: CommandArgs): - update: Update = args.kwargs["update"] + async def telegram(self, call: Call): + update: Update = call.kwargs["update"] # Find the user user: typing.Optional[User] = update.effective_user if user is None: raise ValueError("Trying to sync a None user.") # Find the Royal - royal = await asyncify(call.session.query(call.alchemy.Royal).filter_by(username=args[0]).one_or_none) + royal = await asyncify(call.session.query(call.alchemy.Royal).filter_by(username=call.args[0]).one_or_none) if royal is None: await call.reply("⚠️ Non esiste alcun account Royalnet con quel nome.") return @@ -46,4 +46,4 @@ class SyncCommand(Command): telegram.tg_username = user.username await call.reply(f"βœ… Dati di [c]{str(telegram)}[/c] aggiornati.") # Commit the session - await asyncify(call.session.commit()) + await asyncify(call.session.commit) diff --git a/royalnet/database/relationshiplinkchain.py b/royalnet/database/relationshiplinkchain.py index 2ab72934..ef7b5446 100644 --- a/royalnet/database/relationshiplinkchain.py +++ b/royalnet/database/relationshiplinkchain.py @@ -15,7 +15,7 @@ def relationshiplinkchain(starting_class, ending_class) -> typing.Optional[tuple if _relationship.mapper in inspected: continue result = search(_relationship.mapper, chain + (_relationship,)) - if len(result) == 0: + if len(result) != 0: return result return () diff --git a/royalnet/database/tables/diario.py b/royalnet/database/tables/diario.py index 149b2e5f..720789e4 100644 --- a/royalnet/database/tables/diario.py +++ b/royalnet/database/tables/diario.py @@ -1,3 +1,4 @@ +import re from sqlalchemy import Column, \ Integer, \ Text, \ @@ -13,7 +14,7 @@ class Diario: __tablename__ = "diario" diario_id = Column(Integer, primary_key=True) - creator_id = Column(Integer, ForeignKey("royals.uid")) + creator_id = Column(Integer, ForeignKey("royals.uid"), nullable=False) quoted_account_id = Column(Integer, ForeignKey("royals.uid")) quoted = Column(String) text = Column(Text, nullable=False) @@ -27,3 +28,23 @@ class Diario: def __repr__(self): return f"" + + def __str__(self): + # TODO: support media_url + text = f"Riga #{self.diario_id}" + text += f" (salvata da {self.creator.username}" + text += f" alle {self.timestamp.strftime('%Y-%m-%d %H:%M')}):\n" + if self.spoiler: + hidden = re.sub("\w", "β–ˆ", self.text) + text += f"\"{hidden}\"\n" + else: + text += f"[b]\"{self.text}\"[/b]\n" + if self.quoted_account is not None: + text += f" β€”{self.quoted_account.username}" + elif self.quoted is not None: + text += f" β€”{self.quoted}" + else: + text += f" β€”Anonimo" + if self.context: + text += f", [i]{self.context}[/i]" + return text diff --git a/royalnet/utils/call.py b/royalnet/utils/call.py index e722049a..c7cfca70 100644 --- a/royalnet/utils/call.py +++ b/royalnet/utils/call.py @@ -32,10 +32,10 @@ class Call: raise NotImplementedError() # These parameters / methods should be left alone - def __init__(self, channel, command: Command, *args, **kwargs): + def __init__(self, channel, command: typing.Type[Command], command_args: list, **kwargs): self.channel = channel self.command = command - self.args = args + self.args = CommandArgs(command_args) self.kwargs = kwargs self.session = None @@ -56,7 +56,7 @@ class Call: except AttributeError: coroutine = getattr(self.command, "common") try: - result = await coroutine(self.command, self, CommandArgs(*self.args, **self.kwargs)) + result = await coroutine(self.command, self) finally: await self.session_end() return result diff --git a/royalnet/utils/command.py b/royalnet/utils/command.py index ec08c119..61bcaf31 100644 --- a/royalnet/utils/command.py +++ b/royalnet/utils/command.py @@ -13,23 +13,20 @@ class InvalidInputError(Exception): pass -class CommandArgs: +class CommandArgs(list): """The arguments of a command. Raises InvalidInputError if the requested argument does not exist.""" - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs def __getitem__(self, item): if isinstance(item, int): try: - return self.args[item] + return super().__getitem__(item) except IndexError: raise InvalidInputError(f'Tried to get missing [{item}] arg from CommandArgs') - elif isinstance(item, str): + if isinstance(item, slice): try: - return self.kwargs[item] + return super().__getitem__(item) except IndexError: - raise InvalidInputError(f'Tried to get missing ["{item}"] kwarg from CommandArgs') + raise InvalidInputError(f'Tried to get invalid [{item}] slice from CommandArgs') raise ValueError(f"Invalid type passed to CommandArgs.__getattr__: {type(item)}") @@ -42,5 +39,5 @@ class Command: require_alchemy_tables: typing.Set = set() - async def common(self, call: "Call", args: CommandArgs): + async def common(self, call: "Call"): raise NotImplementedError()