1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

Start work to remove interfaces

This commit is contained in:
Steffo 2020-07-26 19:27:05 +02:00
parent 7845091bb7
commit f2ff31d155
14 changed files with 100 additions and 222 deletions

View file

@ -5,7 +5,7 @@
[tool.poetry] [tool.poetry]
name = "royalnet" name = "royalnet"
version = "5.10.4" version = "5.11.0"
description = "A multipurpose bot and web framework" description = "A multipurpose bot and web framework"
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"] authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
license = "AGPL-3.0+" license = "AGPL-3.0+"

View file

@ -2,6 +2,9 @@ from typing import *
import royalnet import royalnet
import royalnet.commands as rc import royalnet.commands as rc
import royalnet.utils as ru import royalnet.utils as ru
import royalnet.serf.telegram
import royalnet.serf.discord
import royalnet.serf.matrix
from ..tables.telegram import Telegram from ..tables.telegram import Telegram
from ..tables.discord import Discord from ..tables.discord import Discord
@ -27,7 +30,7 @@ class RoyalnetsyncCommand(rc.Command):
if not successful: if not successful:
raise rc.InvalidInputError(f"Invalid password!") raise rc.InvalidInputError(f"Invalid password!")
if self.interface.name == "telegram": if isinstance(self.serf, royalnet.serf.telegram.TelegramSerf):
import telegram import telegram
message: telegram.Message = data.message message: telegram.Message = data.message
from_user: telegram.User = message.from_user from_user: telegram.User = message.from_user
@ -53,7 +56,7 @@ class RoyalnetsyncCommand(rc.Command):
await data.session_commit() await data.session_commit()
await data.reply(f"↔️ Account {tg_user} synced to {author}!") 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 import discord
message: discord.Message = data.message message: discord.Message = data.message
author: discord.User = message.author author: discord.User = message.author
@ -79,8 +82,8 @@ class RoyalnetsyncCommand(rc.Command):
await data.session_commit() await data.session_commit()
await data.reply(f"↔️ Account {ds_user} synced to {author}!") 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") raise rc.UnsupportedError(f"{self} hasn't been implemented for Matrix yet")
else: else:
raise rc.UnsupportedError(f"Unknown interface: {self.interface.name}") raise rc.UnsupportedError(f"Unknown interface: {self.serf.__class__.__qualname__}")

View file

@ -1,4 +1,4 @@
import royalnet.version import pkg_resources
from royalnet.commands import * from royalnet.commands import *
@ -7,12 +7,16 @@ class RoyalnetversionCommand(Command):
description: str = "Display the current Royalnet version." 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: async def run(self, args: CommandArgs, data: CommandData) -> None:
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
if __debug__: if __debug__:
message = f" Royalnet [url=https://github.com/Steffo99/royalnet/]Unreleased[/url]\n" message = f" Royalnet [url=https://github.com/Steffo99/royalnet/]Unreleased[/url]\n"
else: 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: if "69" in royalnet.version.semantic:
message += "(Nice.)" message += "(Nice.)"
await data.reply(message) await data.reply(message)

View file

@ -5,6 +5,6 @@ class ExceptionEvent(Event):
name = "exception" name = "exception"
def run(self, **kwargs): def run(self, **kwargs):
if not self.interface.config["exc_debug"]: if not self.config["exc_debug"]:
raise UserError(f"{self.interface.prefix}{self.name} is not enabled.") raise UserError(f"{self.__class__.__name__} is not enabled.")
raise Exception(f"{self.name} event was called") raise Exception(f"{self.name} event was called")

View file

@ -1,6 +1,5 @@
"""The subpackage providing all classes related to Royalnet commands.""" """The subpackage providing all classes related to Royalnet commands."""
from .commandinterface import CommandInterface
from .command import Command from .command import Command
from .commanddata import CommandData from .commanddata import CommandData
from .commandargs import CommandArgs from .commandargs import CommandArgs
@ -11,7 +10,6 @@ from .keyboardkey import KeyboardKey
from .configdict import ConfigDict from .configdict import ConfigDict
__all__ = [ __all__ = [
"CommandInterface",
"Command", "Command",
"CommandData", "CommandData",
"CommandArgs", "CommandArgs",

View file

@ -1,10 +1,10 @@
import abc
from typing import * from typing import *
from .commandinterface import CommandInterface
from .commandargs import CommandArgs from .commandargs import CommandArgs
from .commanddata import CommandData from .commanddata import CommandData
class Command: class Command(metaclass=abc.ABCMeta):
name: str = NotImplemented name: str = NotImplemented
"""The main name of the command. """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, """The syntax of the command, to be displayed when a :py:exc:`InvalidInputError` is raised,
in the format ``(required_arg) [optional_arg]``.""" in the format ``(required_arg) [optional_arg]``."""
def __init__(self, interface: CommandInterface): def __init__(self, serf, config):
self.interface = interface self.serf = serf
self.config = config
def __str__(self): def __str__(self):
return f"[c]{self.interface.prefix}{self.name}[/c]" return f"[c]{self.serf.prefix}{self.name}[/c]"
@property
def serf(self):
"""A shortcut for :attr:`.interface.serf`."""
return self.interface.serf
@property @property
def alchemy(self): def alchemy(self):
"""A shortcut for :attr:`.interface.alchemy`.""" """A shortcut for :attr:`.interface.alchemy`."""
return self.interface.alchemy return self.serf.alchemy
@property @property
def loop(self): def loop(self):
"""A shortcut for :attr:`.interface.loop`.""" """A shortcut for :attr:`.interface.loop`."""
return self.interface.loop return self.serf.loop
@property
def config(self):
"""A shortcut for :attr:`.interface.config`."""
return self.interface.config
@abc.abstractmethod
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
raise NotImplementedError() raise NotImplementedError()

View file

@ -9,27 +9,29 @@ from royalnet.backpack.tables.users import User
if TYPE_CHECKING: if TYPE_CHECKING:
from .keyboardkey import KeyboardKey from .keyboardkey import KeyboardKey
from royalnet.backpack.tables.users import User
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CommandData: class CommandData:
def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop): def __init__(self, command):
self.loop: aio.AbstractEventLoop = loop self.command = command
self._interface: CommandInterface = interface
self._session = None self._session = None
# TODO: make this asyncronous... somehow? # TODO: make this asyncronous... somehow?
@property @property
def session(self): def session(self):
if self._session is None: 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") raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
log.debug("Creating Session...") log.debug("Creating Session...")
self._session = self._interface.alchemy.Session() self._session = self.command.serf.alchemy.Session()
return self._session return self._session
@property
def loop(self):
return self.command.serf.loop
async def session_commit(self): async def session_commit(self):
"""Asyncronously commit the :attr:`.session` of this object.""" """Asyncronously commit the :attr:`.session` of this object."""
if self._session: if self._session:

View file

@ -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.")

View file

@ -1,5 +1,4 @@
import asyncio as aio import asyncio as aio
from .commandinterface import CommandInterface
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@ -12,30 +11,19 @@ class Event:
name = NotImplemented name = NotImplemented
"""The event_name that will trigger this event.""" """The event_name that will trigger this event."""
def __init__(self, interface: CommandInterface): def __init__(self, serf, config):
"""Bind the event to a :class:`~royalnet.serf.Serf`.""" self.serf = serf
self.interface: CommandInterface = interface self.config = config
"""The :class:`CommandInterface` available to this :class:`Event`."""
@property
def serf(self) -> "Serf":
"""A shortcut for :attr:`.interface.serf`."""
return self.interface.serf
@property @property
def alchemy(self): def alchemy(self):
"""A shortcut for :attr:`.interface.alchemy`.""" """A shortcut for :attr:`.interface.alchemy`."""
return self.interface.alchemy return self.serf.alchemy
@property @property
def loop(self) -> aio.AbstractEventLoop: def loop(self) -> aio.AbstractEventLoop:
"""A shortcut for :attr:`.interface.loop`.""" """A shortcut for :attr:`.interface.loop`."""
return self.interface.loop return self.serf.loop
@property
def config(self) -> dict:
"""A shortcut for :attr:`.interface.config`."""
return self.interface.config
async def run(self, **kwargs): async def run(self, **kwargs):
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,15 +1,12 @@
from typing import * from typing import *
from .commandinterface import CommandInterface
from .commanddata import CommandData from .commanddata import CommandData
class KeyboardKey: class KeyboardKey:
def __init__(self, def __init__(self,
interface: CommandInterface,
short: str, short: str,
text: str, text: str,
callback: Callable[[CommandData], Awaitable[None]]): callback: Callable[[CommandData], Awaitable[None]]):
self.interface: CommandInterface = interface
self.short: str = short self.short: str = short
self.text: str = text self.text: str = text
self.callback: Callable[[CommandData], Awaitable[None]] = callback self.callback: Callable[[CommandData], Awaitable[None]] = callback

View file

@ -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 """An abstract class, to be used as base to implement Royalnet bots on multiple interfaces (such as Telegram or
Discord).""" Discord)."""
interface_name = NotImplemented interface_name = NotImplemented
prefix = NotImplemented
_master_table: type = rbt.User _master_table: type = rbt.User
_identity_table: type = NotImplemented _identity_table: type = NotImplemented
@ -91,9 +92,6 @@ class Serf(abc.ABC):
self.events: Dict[str, Event] = {} self.events: Dict[str, Event] = {}
"""A dictionary containing all :class:`Event` that can be handled by this :class:`Serf`.""" """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] = {} self.commands: Dict[str, Command] = {}
"""The :class:`dict` connecting each command name to its :class:`Command` object.""" """The :class:`dict` connecting each command name to its :class:`Command` object."""
@ -138,15 +136,7 @@ class Serf(abc.ABC):
"""Find a relationship path starting from the master table and ending at the identity table, and return it.""" """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) return ra.table_dfs(self.master_table, self.identity_table)
def interface_factory(self) -> Type[CommandInterface]: async def call_herald_event(self, destination: str, event_name: str, **kwargs) -> Dict:
"""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 """Send a :class:`royalherald.Request` to a specific destination, and wait for a
:class:`royalherald.Response`.""" :class:`royalherald.Response`."""
if self.herald is None: if self.herald is None:
@ -187,42 +177,34 @@ class Serf(abc.ABC):
raise ProgramError(f"Other Herald Link returned unknown response:\n" raise ProgramError(f"Other Herald Link returned unknown response:\n"
f"[p]{response}[/p]") f"[p]{response}[/p]")
return GenericInterface
def register_commands(self, commands: List[Type[Command]], pack_cfg: Dict[str, Any]) -> None: def register_commands(self, commands: List[Type[Command]], pack_cfg: Dict[str, Any]) -> None:
"""Initialize and register all commands passed as argument.""" """Initialize and register all commands passed as argument."""
# Instantiate the Commands # Instantiate the Commands
for SelectedCommand in commands: for SelectedCommand in commands:
# Create a new interface
interface = self.Interface(config=pack_cfg)
# Try to instantiate the command # Try to instantiate the command
try: try:
command = SelectedCommand(interface) command = SelectedCommand(serf=self, config=pack_cfg)
except Exception as e: except Exception as e:
log.error(f"Skipping: " log.error(f"Skipping: "
f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.") f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.")
ru.sentry_exc(e) ru.sentry_exc(e)
continue continue
# Link the interface to the command
interface.command = command
# Warn if the command would be overriding something # 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): " log.info(f"Overriding (already defined): "
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}") f"{SelectedCommand.__qualname__} -> {SelectedCommand.name}")
else: else:
log.debug(f"Registering: " log.debug(f"Registering: "
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}") f"{SelectedCommand.__qualname__} -> {SelectedCommand.name}")
# Register the command in the commands dict # 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 # Register aliases, but don't override anything
for alias in SelectedCommand.aliases: for alias in SelectedCommand.aliases:
if f"{interface.prefix}{alias}" not in self.commands: if alias not in self.commands:
log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {alias}")
self.commands[f"{interface.prefix}{alias}"] = \ self.commands[alias] = self.commands[SelectedCommand.name]
self.commands[f"{interface.prefix}{SelectedCommand.name}"]
else: else:
log.warning( log.warning(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {alias}")
f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
def init_herald(self, herald_cfg: Dict[str, Any]): def init_herald(self, herald_cfg: Dict[str, Any]):
"""Create a :class:`Link` and bind :class:`Event`.""" """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]): def register_events(self, events: List[Type[Event]], pack_cfg: Dict[str, Any]):
for SelectedEvent in events: for SelectedEvent in events:
# Create a new interface
interface = self.Interface(config=pack_cfg)
# Initialize the event # Initialize the event
try: try:
event = SelectedEvent(interface) event = SelectedEvent(serf=self, config=pack_cfg)
except Exception as e: except Exception as e:
log.error(f"Skipping: " log.error(f"Skipping: "
f"{SelectedEvent.__qualname__} - {e.__class__.__qualname__} in the initialization.") f"{SelectedEvent.__qualname__} - {e.__class__.__qualname__} in the initialization.")
@ -285,7 +265,7 @@ class Serf(abc.ABC):
await command.run(CommandArgs(parameters), data) await command.run(CommandArgs(parameters), data)
except InvalidInputError as e: except InvalidInputError as e:
await data.reply(f"⚠️ {e.message}\n" 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: except UserError as e:
await data.reply(f"⚠️ {e.message}") await data.reply(f"⚠️ {e.message}")
except UnsupportedError as e: except UnsupportedError as e:

View file

@ -1,11 +1,13 @@
from typing import *
import re 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. """Escape a string to be sent through Telegram (as HTML), and format it using RoyalCode.
Warning: Warning:
Currently escapes everything, even items in code blocks.""" Currently escapes everything, even items in code blocks."""
url_pattern = re.compile(r"\[url=(.*?)](.*?)\[/url]") url_pattern = re.compile(r"\[url=(.*?)](.*?)\[/url]")
url_replacement = r'<a href="\1">\2</a>' url_replacement = r'<a href="\1">\2</a>'

View file

@ -24,6 +24,7 @@ log = logging.getLogger(__name__)
class TelegramSerf(Serf): class TelegramSerf(Serf):
"""A Serf that connects to `Telegram <https://telegram.org/>`_ as a bot.""" """A Serf that connects to `Telegram <https://telegram.org/>`_ as a bot."""
interface_name = "telegram" interface_name = "telegram"
prefix = "/"
_identity_table = rbt.Telegram _identity_table = rbt.Telegram
_identity_column = "tg_id" _identity_column = "tg_id"
@ -90,25 +91,13 @@ class TelegramSerf(Serf):
break break
return None 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]: def message_data_factory(self) -> Type[rc.CommandData]:
# noinspection PyMethodParameters # noinspection PyMethodParameters
class TelegramMessageData(rc.CommandData): class TelegramMessageData(rc.CommandData):
def __init__(data, def __init__(data,
interface: rc.CommandInterface, command: rc.Command,
loop: aio.AbstractEventLoop,
message: telegram.Message): message: telegram.Message):
super().__init__(interface=interface, loop=loop) super().__init__(command=command)
data.message: telegram.Message = message data.message: telegram.Message = message
async def reply(data, text: str): async def reply(data, text: str):
@ -171,10 +160,9 @@ class TelegramSerf(Serf):
# noinspection PyMethodParameters # noinspection PyMethodParameters
class TelegramKeyboardData(rc.CommandData): class TelegramKeyboardData(rc.CommandData):
def __init__(data, def __init__(data,
interface: rc.CommandInterface, command: rc.Command,
loop: aio.AbstractEventLoop,
cbq: telegram.CallbackQuery): cbq: telegram.CallbackQuery):
super().__init__(interface=interface, loop=loop) super().__init__(command=command)
data.cbq: telegram.CallbackQuery = cbq data.cbq: telegram.CallbackQuery = cbq
async def reply(data, text: str): async def reply(data, text: str):
@ -250,7 +238,7 @@ class TelegramSerf(Serf):
# Send a typing notification # Send a typing notification
await self.api_call(message.chat.send_action, telegram.ChatAction.TYPING) await self.api_call(message.chat.send_action, telegram.ChatAction.TYPING)
# Prepare data # Prepare data
data = self.MessageData(interface=command.interface, loop=self.loop, message=message) data = self.MessageData(command=command, message=message)
# Call the command # Call the command
await self.call(command, data, parameters) 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) await self.api_call(cbq.answer, text="⚠️ This keyboard has expired.", show_alert=True)
return return
key: rc.KeyboardKey = self.key_callbacks[uid] 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) await self.press(key, data)
def register_keyboard_key(self, identifier: str, key: rc.KeyboardKey): def register_keyboard_key(self, identifier: str, key: rc.KeyboardKey):

View file

@ -1 +1 @@
semantic = "5.10.4" semantic = "5.11.0"