diff --git a/pyproject.toml b/pyproject.toml index 33d80fad..c84121c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.10.4" + version = "5.11.0" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/backpack/commands/royalnetsync.py b/royalnet/backpack/commands/royalnetsync.py index 1d382ca2..99c77bbf 100644 --- a/royalnet/backpack/commands/royalnetsync.py +++ b/royalnet/backpack/commands/royalnetsync.py @@ -2,6 +2,9 @@ from typing import * import royalnet import royalnet.commands as rc import royalnet.utils as ru +import royalnet.serf.telegram +import royalnet.serf.discord +import royalnet.serf.matrix from ..tables.telegram import Telegram from ..tables.discord import Discord @@ -27,7 +30,7 @@ class RoyalnetsyncCommand(rc.Command): if not successful: raise rc.InvalidInputError(f"Invalid password!") - if self.interface.name == "telegram": + if isinstance(self.serf, royalnet.serf.telegram.TelegramSerf): import telegram message: telegram.Message = data.message from_user: telegram.User = message.from_user @@ -53,7 +56,7 @@ class RoyalnetsyncCommand(rc.Command): await data.session_commit() await data.reply(f"↔️ Account {tg_user} synced to {author}!") - elif self.interface.name == "discord": + elif isinstance(self.serf, royalnet.serf.discord.DiscordSerf): import discord message: discord.Message = data.message author: discord.User = message.author @@ -79,8 +82,8 @@ class RoyalnetsyncCommand(rc.Command): await data.session_commit() await data.reply(f"↔️ Account {ds_user} synced to {author}!") - elif self.interface.name == "matrix": + elif isinstance(self.serf, royalnet.serf.matrix.MatrixSerf): raise rc.UnsupportedError(f"{self} hasn't been implemented for Matrix yet") else: - raise rc.UnsupportedError(f"Unknown interface: {self.interface.name}") + raise rc.UnsupportedError(f"Unknown interface: {self.serf.__class__.__qualname__}") diff --git a/royalnet/backpack/commands/royalnetversion.py b/royalnet/backpack/commands/royalnetversion.py index c4583194..51a62799 100644 --- a/royalnet/backpack/commands/royalnetversion.py +++ b/royalnet/backpack/commands/royalnetversion.py @@ -1,4 +1,4 @@ -import royalnet.version +import pkg_resources from royalnet.commands import * @@ -7,12 +7,16 @@ class RoyalnetversionCommand(Command): description: str = "Display the current Royalnet version." + @property + def royalnet_version(self): + return pkg_resources.get_distribution("royalnet").version + async def run(self, args: CommandArgs, data: CommandData) -> None: # noinspection PyUnreachableCode if __debug__: message = f"ℹ️ Royalnet [url=https://github.com/Steffo99/royalnet/]Unreleased[/url]\n" else: - message = f"ℹ️ Royalnet [url=https://github.com/Steffo99/royalnet/releases/tag/{royalnet.version.semantic}]{royalnet.version.semantic}[/url]\n" + message = f"ℹ️ Royalnet [url=https://github.com/Steffo99/royalnet/releases/tag/{self.royalnet_version}]{self.royalnet_version}[/url]\n" if "69" in royalnet.version.semantic: message += "(Nice.)" await data.reply(message) diff --git a/royalnet/backpack/events/exception.py b/royalnet/backpack/events/exception.py index eb03b62a..46db58c8 100644 --- a/royalnet/backpack/events/exception.py +++ b/royalnet/backpack/events/exception.py @@ -5,6 +5,6 @@ class ExceptionEvent(Event): name = "exception" def run(self, **kwargs): - if not self.interface.config["exc_debug"]: - raise UserError(f"{self.interface.prefix}{self.name} is not enabled.") + if not self.config["exc_debug"]: + raise UserError(f"{self.__class__.__name__} is not enabled.") raise Exception(f"{self.name} event was called") diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index ab5b49f0..df453e3a 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -1,6 +1,5 @@ """The subpackage providing all classes related to Royalnet commands.""" -from .commandinterface import CommandInterface from .command import Command from .commanddata import CommandData from .commandargs import CommandArgs @@ -11,7 +10,6 @@ from .keyboardkey import KeyboardKey from .configdict import ConfigDict __all__ = [ - "CommandInterface", "Command", "CommandData", "CommandArgs", diff --git a/royalnet/commands/command.py b/royalnet/commands/command.py index 725af01f..d48f933c 100644 --- a/royalnet/commands/command.py +++ b/royalnet/commands/command.py @@ -1,10 +1,10 @@ +import abc from typing import * -from .commandinterface import CommandInterface from .commandargs import CommandArgs from .commanddata import CommandData -class Command: +class Command(metaclass=abc.ABCMeta): name: str = NotImplemented """The main name of the command. @@ -24,31 +24,23 @@ class Command: """The syntax of the command, to be displayed when a :py:exc:`InvalidInputError` is raised, in the format ``(required_arg) [optional_arg]``.""" - def __init__(self, interface: CommandInterface): - self.interface = interface + def __init__(self, serf, config): + self.serf = serf + self.config = config def __str__(self): - return f"[c]{self.interface.prefix}{self.name}[/c]" - - @property - def serf(self): - """A shortcut for :attr:`.interface.serf`.""" - return self.interface.serf + return f"[c]{self.serf.prefix}{self.name}[/c]" @property def alchemy(self): """A shortcut for :attr:`.interface.alchemy`.""" - return self.interface.alchemy + return self.serf.alchemy @property def loop(self): """A shortcut for :attr:`.interface.loop`.""" - return self.interface.loop - - @property - def config(self): - """A shortcut for :attr:`.interface.config`.""" - return self.interface.config + return self.serf.loop + @abc.abstractmethod async def run(self, args: CommandArgs, data: CommandData) -> None: raise NotImplementedError() diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index 82654738..9a9ba1fd 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -9,27 +9,29 @@ from royalnet.backpack.tables.users import User if TYPE_CHECKING: from .keyboardkey import KeyboardKey - from royalnet.backpack.tables.users import User log = logging.getLogger(__name__) class CommandData: - def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop): - self.loop: aio.AbstractEventLoop = loop - self._interface: CommandInterface = interface + def __init__(self, command): + self.command = command self._session = None # TODO: make this asyncronous... somehow? @property def session(self): if self._session is None: - if self._interface.alchemy is None: + if self.command.serf.alchemy is None: raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") log.debug("Creating Session...") - self._session = self._interface.alchemy.Session() + self._session = self.command.serf.alchemy.Session() return self._session + @property + def loop(self): + return self.command.serf.loop + async def session_commit(self): """Asyncronously commit the :attr:`.session` of this object.""" if self._session: diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py deleted file mode 100644 index eb966e00..00000000 --- a/royalnet/commands/commandinterface.py +++ /dev/null @@ -1,76 +0,0 @@ -from typing import * -import asyncio as aio -from .errors import UnsupportedError -from .configdict import ConfigDict - -if TYPE_CHECKING: - from .event import Event - from .command import Command - from ..alchemy import Alchemy - from ..serf import Serf - from ..constellation import Constellation - - -class CommandInterface: - name: str = NotImplemented - """The name of the :class:`CommandInterface` that's being implemented. - - Examples: - ``telegram``, ``discord``, ``console``...""" - - prefix: str = NotImplemented - """The prefix used by commands on the interface. - - Examples: - ``/`` on Telegram, ``!`` on Discord.""" - - serf: Optional["Serf"] = None - """A reference to the :class:`~royalnet.serf.Serf` that is implementing this :class:`CommandInterface`. - - Example: - A reference to a :class:`~royalnet.serf.telegram.TelegramSerf`.""" - - constellation: Optional["Constellation"] = None - """A reference to the Constellation that is implementing this :class:`CommandInterface`. - - Example: - A reference to a :class:`~royalnet.constellation.Constellation`.""" - - def __init__(self, config: Dict[str, Any]): - self.config: ConfigDict[str, Any] = ConfigDict.convert(config) - """The config section for the pack of the command.""" - - # Will be bound after the command/event has been created - self.command: Optional[Command] = None - self.event: Optional[Event] = None - - @property - def alchemy(self) -> "Alchemy": - """A shortcut for :attr:`.serf.alchemy`.""" - return self.serf.alchemy - - @property - def table(self) -> "Callable": - """A shortcut for :func:`.serf.alchemy.get`. - - Raises: - UnsupportedError: if :attr:`.alchemy` is :const:`None`.""" - if self.alchemy is None: - raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") - return self.alchemy.get - - @property - def loop(self) -> aio.AbstractEventLoop: - """A shortcut for :attr:`.serf.loop`.""" - if self.serf: - return self.serf.loop - raise UnsupportedError("This command is not being run in a serf.") - - async def call_herald_event(self, destination: str, event_name: str, **kwargs) -> dict: - """Call an event function on a different :class:`~royalnet.serf.Serf`. - - Example: - You can run a function on a :class:`~royalnet.serf.discord.DiscordSerf` from a - :class:`~royalnet.serf.telegram.TelegramSerf`. - """ - raise UnsupportedError(f"{self.call_herald_event.__name__} is not supported on this platform.") diff --git a/royalnet/commands/event.py b/royalnet/commands/event.py index bb948d84..c0e8c723 100644 --- a/royalnet/commands/event.py +++ b/royalnet/commands/event.py @@ -1,5 +1,4 @@ import asyncio as aio -from .commandinterface import CommandInterface from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -12,30 +11,19 @@ class Event: name = NotImplemented """The event_name that will trigger this event.""" - def __init__(self, interface: CommandInterface): - """Bind the event to a :class:`~royalnet.serf.Serf`.""" - self.interface: CommandInterface = interface - """The :class:`CommandInterface` available to this :class:`Event`.""" - - @property - def serf(self) -> "Serf": - """A shortcut for :attr:`.interface.serf`.""" - return self.interface.serf + def __init__(self, serf, config): + self.serf = serf + self.config = config @property def alchemy(self): """A shortcut for :attr:`.interface.alchemy`.""" - return self.interface.alchemy + return self.serf.alchemy @property def loop(self) -> aio.AbstractEventLoop: """A shortcut for :attr:`.interface.loop`.""" - return self.interface.loop - - @property - def config(self) -> dict: - """A shortcut for :attr:`.interface.config`.""" - return self.interface.config + return self.serf.loop async def run(self, **kwargs): raise NotImplementedError() diff --git a/royalnet/commands/keyboardkey.py b/royalnet/commands/keyboardkey.py index 0f8c3d94..3999891d 100644 --- a/royalnet/commands/keyboardkey.py +++ b/royalnet/commands/keyboardkey.py @@ -1,15 +1,12 @@ from typing import * -from .commandinterface import CommandInterface from .commanddata import CommandData class KeyboardKey: def __init__(self, - interface: CommandInterface, short: str, text: str, callback: Callable[[CommandData], Awaitable[None]]): - self.interface: CommandInterface = interface self.short: str = short self.text: str = text self.callback: Callable[[CommandData], Awaitable[None]] = callback diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index c9642643..33afc138 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -20,6 +20,7 @@ class Serf(abc.ABC): """An abstract class, to be used as base to implement Royalnet bots on multiple interfaces (such as Telegram or Discord).""" interface_name = NotImplemented + prefix = NotImplemented _master_table: type = rbt.User _identity_table: type = NotImplemented @@ -91,9 +92,6 @@ class Serf(abc.ABC): self.events: Dict[str, Event] = {} """A dictionary containing all :class:`Event` that can be handled by this :class:`Serf`.""" - self.Interface: Type[CommandInterface] = self.interface_factory() - """The :class:`CommandInterface` class of this Serf.""" - self.commands: Dict[str, Command] = {} """The :class:`dict` connecting each command name to its :class:`Command` object.""" @@ -138,91 +136,75 @@ class Serf(abc.ABC): """Find a relationship path starting from the master table and ending at the identity table, and return it.""" return ra.table_dfs(self.master_table, self.identity_table) - def interface_factory(self) -> Type[CommandInterface]: - """Create the :class:`CommandInterface` class for the Serf.""" - - # noinspection PyMethodParameters - class GenericInterface(CommandInterface): - alchemy: ra.Alchemy = self.alchemy - serf: "Serf" = self - - async def call_herald_event(ci, destination: str, event_name: str, **kwargs) -> Dict: - """Send a :class:`royalherald.Request` to a specific destination, and wait for a - :class:`royalherald.Response`.""" - if self.herald is None: - raise UnsupportedError("`royalherald` is not enabled on this serf.") - request: rh.Request = rh.Request(handler=event_name, data=kwargs) - response: rh.Response = await self.herald.request(destination=destination, request=request) - if isinstance(response, rh.ResponseFailure): - if response.name == "no_event": - raise ProgramError(f"There is no event named {event_name} in {destination}.") - elif response.name == "error_in_event": - if response.extra_info["type"] == "CommandError": - raise CommandError(response.extra_info["message"]) - elif response.extra_info["type"] == "UserError": - raise UserError(response.extra_info["message"]) - elif response.extra_info["type"] == "InvalidInputError": - raise InvalidInputError(response.extra_info["message"]) - elif response.extra_info["type"] == "UnsupportedError": - raise UnsupportedError(response.extra_info["message"]) - elif response.extra_info["type"] == "ConfigurationError": - raise ConfigurationError(response.extra_info["message"]) - elif response.extra_info["type"] == "ExternalError": - raise ExternalError(response.extra_info["message"]) - else: - raise ProgramError(f"Invalid error in Herald event '{event_name}':\n" - f"[b]{response.extra_info['type']}[/b]\n" - f"{response.extra_info['message']}") - elif response.name == "unhandled_exception_in_event": - raise ProgramError(f"Unhandled exception in Herald event '{event_name}':\n" - f"[b]{response.extra_info['type']}[/b]\n" - f"{response.extra_info['message']}") - else: - raise ProgramError(f"Unknown response in Herald event '{event_name}':\n" - f"[b]{response.name}[/b]" - f"[p]{response}[/p]") - elif isinstance(response, rh.ResponseSuccess): - return response.data + async def call_herald_event(self, destination: str, event_name: str, **kwargs) -> Dict: + """Send a :class:`royalherald.Request` to a specific destination, and wait for a + :class:`royalherald.Response`.""" + if self.herald is None: + raise UnsupportedError("`royalherald` is not enabled on this serf.") + request: rh.Request = rh.Request(handler=event_name, data=kwargs) + response: rh.Response = await self.herald.request(destination=destination, request=request) + if isinstance(response, rh.ResponseFailure): + if response.name == "no_event": + raise ProgramError(f"There is no event named {event_name} in {destination}.") + elif response.name == "error_in_event": + if response.extra_info["type"] == "CommandError": + raise CommandError(response.extra_info["message"]) + elif response.extra_info["type"] == "UserError": + raise UserError(response.extra_info["message"]) + elif response.extra_info["type"] == "InvalidInputError": + raise InvalidInputError(response.extra_info["message"]) + elif response.extra_info["type"] == "UnsupportedError": + raise UnsupportedError(response.extra_info["message"]) + elif response.extra_info["type"] == "ConfigurationError": + raise ConfigurationError(response.extra_info["message"]) + elif response.extra_info["type"] == "ExternalError": + raise ExternalError(response.extra_info["message"]) else: - raise ProgramError(f"Other Herald Link returned unknown response:\n" - f"[p]{response}[/p]") - - return GenericInterface + raise ProgramError(f"Invalid error in Herald event '{event_name}':\n" + f"[b]{response.extra_info['type']}[/b]\n" + f"{response.extra_info['message']}") + elif response.name == "unhandled_exception_in_event": + raise ProgramError(f"Unhandled exception in Herald event '{event_name}':\n" + f"[b]{response.extra_info['type']}[/b]\n" + f"{response.extra_info['message']}") + else: + raise ProgramError(f"Unknown response in Herald event '{event_name}':\n" + f"[b]{response.name}[/b]" + f"[p]{response}[/p]") + elif isinstance(response, rh.ResponseSuccess): + return response.data + else: + raise ProgramError(f"Other Herald Link returned unknown response:\n" + f"[p]{response}[/p]") def register_commands(self, commands: List[Type[Command]], pack_cfg: Dict[str, Any]) -> None: """Initialize and register all commands passed as argument.""" # Instantiate the Commands for SelectedCommand in commands: - # Create a new interface - interface = self.Interface(config=pack_cfg) # Try to instantiate the command try: - command = SelectedCommand(interface) + command = SelectedCommand(serf=self, config=pack_cfg) except Exception as e: log.error(f"Skipping: " f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.") ru.sentry_exc(e) continue - # Link the interface to the command - interface.command = command # Warn if the command would be overriding something - if f"{self.Interface.prefix}{SelectedCommand.name}" in self.commands: + if SelectedCommand.name in self.commands: log.info(f"Overriding (already defined): " - f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}") + f"{SelectedCommand.__qualname__} -> {SelectedCommand.name}") else: log.debug(f"Registering: " - f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}") + f"{SelectedCommand.__qualname__} -> {SelectedCommand.name}") # Register the command in the commands dict - self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command + self.commands[SelectedCommand.name] = command # Register aliases, but don't override anything for alias in SelectedCommand.aliases: - if f"{interface.prefix}{alias}" not in self.commands: - log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") - self.commands[f"{interface.prefix}{alias}"] = \ - self.commands[f"{interface.prefix}{SelectedCommand.name}"] + if alias not in self.commands: + log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {alias}") + self.commands[alias] = self.commands[SelectedCommand.name] else: - log.warning( - f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") + log.warning(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {alias}") def init_herald(self, herald_cfg: Dict[str, Any]): """Create a :class:`Link` and bind :class:`Event`.""" @@ -231,11 +213,9 @@ class Serf(abc.ABC): def register_events(self, events: List[Type[Event]], pack_cfg: Dict[str, Any]): for SelectedEvent in events: - # Create a new interface - interface = self.Interface(config=pack_cfg) # Initialize the event try: - event = SelectedEvent(interface) + event = SelectedEvent(serf=self, config=pack_cfg) except Exception as e: log.error(f"Skipping: " f"{SelectedEvent.__qualname__} - {e.__class__.__qualname__} in the initialization.") @@ -285,7 +265,7 @@ class Serf(abc.ABC): await command.run(CommandArgs(parameters), data) except InvalidInputError as e: await data.reply(f"⚠️ {e.message}\n" - f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]") + f"Syntax: [c]{self.prefix}{command.name} {command.syntax}[/c]") except UserError as e: await data.reply(f"⚠️ {e.message}") except UnsupportedError as e: diff --git a/royalnet/serf/telegram/escape.py b/royalnet/serf/telegram/escape.py index ebcc1771..b14c9e4e 100644 --- a/royalnet/serf/telegram/escape.py +++ b/royalnet/serf/telegram/escape.py @@ -1,11 +1,13 @@ +from typing import * import re -def escape(string: str) -> str: +def escape(string: Optional[str]) -> Optional[str]: """Escape a string to be sent through Telegram (as HTML), and format it using RoyalCode. Warning: Currently escapes everything, even items in code blocks.""" + url_pattern = re.compile(r"\[url=(.*?)](.*?)\[/url]") url_replacement = r'\2' diff --git a/royalnet/serf/telegram/telegramserf.py b/royalnet/serf/telegram/telegramserf.py index 62ad651e..4e570908 100644 --- a/royalnet/serf/telegram/telegramserf.py +++ b/royalnet/serf/telegram/telegramserf.py @@ -24,6 +24,7 @@ log = logging.getLogger(__name__) class TelegramSerf(Serf): """A Serf that connects to `Telegram `_ as a bot.""" interface_name = "telegram" + prefix = "/" _identity_table = rbt.Telegram _identity_column = "tg_id" @@ -90,25 +91,13 @@ class TelegramSerf(Serf): break return None - def interface_factory(self) -> Type[rc.CommandInterface]: - # noinspection PyPep8Naming - GenericInterface = super().interface_factory() - - # noinspection PyMethodParameters - class TelegramInterface(GenericInterface): - name = self.interface_name - prefix = "/" - - return TelegramInterface - def message_data_factory(self) -> Type[rc.CommandData]: # noinspection PyMethodParameters class TelegramMessageData(rc.CommandData): def __init__(data, - interface: rc.CommandInterface, - loop: aio.AbstractEventLoop, + command: rc.Command, message: telegram.Message): - super().__init__(interface=interface, loop=loop) + super().__init__(command=command) data.message: telegram.Message = message async def reply(data, text: str): @@ -171,10 +160,9 @@ class TelegramSerf(Serf): # noinspection PyMethodParameters class TelegramKeyboardData(rc.CommandData): def __init__(data, - interface: rc.CommandInterface, - loop: aio.AbstractEventLoop, + command: rc.Command, cbq: telegram.CallbackQuery): - super().__init__(interface=interface, loop=loop) + super().__init__(command=command) data.cbq: telegram.CallbackQuery = cbq async def reply(data, text: str): @@ -250,7 +238,7 @@ class TelegramSerf(Serf): # Send a typing notification await self.api_call(message.chat.send_action, telegram.ChatAction.TYPING) # Prepare data - data = self.MessageData(interface=command.interface, loop=self.loop, message=message) + data = self.MessageData(command=command, message=message) # Call the command await self.call(command, data, parameters) @@ -260,7 +248,7 @@ class TelegramSerf(Serf): await self.api_call(cbq.answer, text="⚠️ This keyboard has expired.", show_alert=True) return key: rc.KeyboardKey = self.key_callbacks[uid] - data: rc.CommandData = self.CallbackData(interface=key.interface, loop=self.loop, cbq=cbq) + data: rc.CommandData = self.CallbackData(command=command, cbq=cbq) await self.press(key, data) def register_keyboard_key(self, identifier: str, key: rc.KeyboardKey): diff --git a/royalnet/version.py b/royalnet/version.py index 667c3b11..2ee9ff43 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.10.4" +semantic = "5.11.0"