diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index df9a5fd7..f6b0d318 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -2,7 +2,6 @@ import discord import typing import logging as _logging from .generic import GenericBot -from royalnet.commands import NullCommand from ..utils import asyncify, Call, Command, discord_escape from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError @@ -32,7 +31,7 @@ class DiscordBot(GenericBot): log.debug(f"Creating music_data dict") self.music_data: typing.Dict[discord.Guild, playmodes.PlayMode] = {} - def _call_factory(self) -> typing.Type[Call]: + def _interface_factory(self) -> typing.Type[Call]: log.debug(f"Creating DiscordCall") # noinspection PyMethodParameters diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index 16d18f78..351d7056 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -2,10 +2,11 @@ import sys import typing import asyncio import logging -from ..utils import Command, NetworkHandler, Call -from royalnet.commands import NullCommand -from ..network import RoyalnetLink, Request, Response, ResponseError, RoyalnetConfig +from ..utils import NetworkHandler +from ..network import RoyalnetLink, Request, Response, ResponseSuccess, ResponseError, RoyalnetConfig from ..database import Alchemy, DatabaseConfig, relationshiplinkchain +from ..commands import Command, CommandInterface +from ..error import * log = logging.getLogger(__name__) @@ -28,17 +29,40 @@ class GenericBot: for command in commands: lower_command_name = command.command_name.lower() self.commands[f"{command_prefix}{lower_command_name}"] = command - self.network_handlers = {**self.network_handlers, **command.network_handler_dict()} self.missing_command: typing.Type[Command] = missing_command self.error_command: typing.Type[Command] = error_command log.debug(f"Successfully generated commands") - def _call_factory(self) -> typing.Type[Call]: - """Create the TelegramCall class, representing a command call. It should inherit from :py:class:`royalnet.utils.Call`. + def _interface_factory(self) -> typing.Type[CommandInterface]: + """Create a :py:class:`royalnet.commands.CommandInterface` type and return it. Returns: - The created TelegramCall class.""" - raise NotImplementedError() + The created :py:class:`royalnet.commands.CommandInterface` type.""" + + # noinspection PyAbstractClass,PyMethodParameters + class GenericInterface(CommandInterface): + alchemy = self.alchemy + bot = self + + def register_net_handler(ci, message_type: str, network_handler: typing.Callable): + self.network_handlers[message_type] = network_handler + + async def net_request(ci, request: Request, destination: str) -> dict: + if self.network is None: + raise InvalidConfigError("Royalnet is not enabled on this bot") + response_dict: dict = await self.network.request(request.to_dict(), destination) + if "type" not in response_dict: + raise RoyalnetResponseError("Response is missing a type") + elif response_dict["type"] == "ResponseSuccess": + response: typing.Union[ResponseSuccess, ResponseError] = ResponseSuccess.from_dict(response_dict) + elif response_dict["type"] == "ResponseError": + response = ResponseError.from_dict(response_dict) + else: + raise RoyalnetResponseError("Response type is unknown") + response.raise_on_error() + return response.data + + return GenericInterface def _init_royalnet(self, royalnet_config: RoyalnetConfig): """Create a :py:class:`royalnet.network.RoyalnetLink`, and run it as a :py:class:`asyncio.Task`.""" @@ -117,7 +141,7 @@ class GenericBot: if commands is None: commands = [] self._init_commands(command_prefix, commands, missing_command=missing_command, error_command=error_command) - self._Call = self._call_factory() + self._Call = self._interface_factory() if royalnet_config is None: self.network = None else: diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index a1914dd8..aef0da07 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -3,11 +3,11 @@ import telegram.utils.request import typing import logging as _logging from .generic import GenericBot -from royalnet.commands import NullCommand -from ..utils import asyncify, Call, Command, telegram_escape +from ..utils import asyncify, telegram_escape from ..error import UnregisteredError, InvalidConfigError, RoyalnetResponseError from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError from ..database import DatabaseConfig +from ..commands import CommandInterface log = _logging.getLogger(__name__) @@ -21,7 +21,6 @@ class TelegramConfig: class TelegramBot(GenericBot): """A bot that connects to `Telegram `_.""" - interface_name = "telegram" def _init_client(self): """Create the :py:class:`telegram.Bot`, and set the starting offset.""" @@ -30,43 +29,29 @@ class TelegramBot(GenericBot): self.client = telegram.Bot(self._telegram_config.token, request=request) self._offset: int = -100 - def _call_factory(self) -> typing.Type[Call]: + def _interface_factory(self) -> typing.Type[CommandInterface]: + GenericInterface = super()._interface_factory() + # noinspection PyMethodParameters - class TelegramCall(Call): - interface_name = self.interface_name - interface_obj = self - interface_prefix = "/" + class TelegramInterface(GenericInterface): + name = "telegram" + prefix = "/" alchemy = self.alchemy - async def reply(call, text: str): - await asyncify(call.channel.send_message, telegram_escape(text), + async def reply(ci, extra: dict, text: str): + await asyncify(ci.channel.send_message, telegram_escape(text), parse_mode="HTML", disable_web_page_preview=True) - async def net_request(call, request: Request, destination: str) -> dict: - if self.network is None: - raise InvalidConfigError("Royalnet is not enabled on this bot") - response_dict: dict = await self.network.request(request.to_dict(), destination) - if "type" not in response_dict: - raise RoyalnetResponseError("Response is missing a type") - elif response_dict["type"] == "ResponseSuccess": - response: typing.Union[ResponseSuccess, ResponseError] = ResponseSuccess.from_dict(response_dict) - elif response_dict["type"] == "ResponseError": - response = ResponseError.from_dict(response_dict) - else: - raise RoyalnetResponseError("Response type is unknown") - response.raise_on_error() - return response.data - - async def get_author(call, error_if_none=False): - update: telegram.Update = call.kwargs["update"] + async def get_author(ci, extra: dict, error_if_none=False): + update: telegram.Update = extra["update"] user: telegram.User = update.effective_user if user is None: if error_if_none: raise UnregisteredError("No author for this message") return None - query = call.session.query(self.master_table) + query = ci.session.query(self.master_table) for link in self.identity_chain: query = query.join(link.mapper.class_) query = query.filter(self.identity_column == user.id) diff --git a/royalnet/commands/command.py b/royalnet/commands/command.py index a24669dc..0e224ac9 100644 --- a/royalnet/commands/command.py +++ b/royalnet/commands/command.py @@ -1,6 +1,7 @@ import typing from ..error import UnsupportedError from .commandinterface import CommandInterface +from .commandargs import CommandArgs class Command: @@ -21,8 +22,5 @@ class Command: def __init__(self, interface: CommandInterface): self.interface = interface - async def common(self) -> None: + async def run(self, args: CommandArgs, **extra) -> None: raise UnsupportedError(f"Command {self.name} can't be called on {self.interface.name}.") - - def __getattr__(self, item) -> typing.Callable: - return self.common diff --git a/royalnet/utils/commandargs.py b/royalnet/commands/commandargs.py similarity index 100% rename from royalnet/utils/commandargs.py rename to royalnet/commands/commandargs.py diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py index f74404a1..ae39f5a1 100644 --- a/royalnet/commands/commandinterface.py +++ b/royalnet/commands/commandinterface.py @@ -13,32 +13,35 @@ class CommandInterface: def __init__(self, alias: str): self.session = self.alchemy.Session() - async def reply(self, text: str) -> None: + def register_net_handler(self, message_type: str, network_handler: typing.Callable): + """Register a new handler for messages received through Royalnet.""" + raise NotImplementedError() + + async def reply(self, extra: dict, text: str) -> None: """Send a text message to the channel where the call was made. Parameters: + extra: The ``extra`` dict passed to the Command text: The text to be sent, possibly formatted in the weird undescribed markup that I'm using.""" raise NotImplementedError() - async def net_request(self, message, destination: str) -> dict: + async def net_request(self, extra: dict, message, destination: str) -> dict: """Send data through a :py:class:`royalnet.network.RoyalnetLink` and wait for a :py:class:`royalnet.network.Reply`. Parameters: + extra: The ``extra`` dict passed to the Command message: The data to be sent. Must be :py:mod:`pickle`-able. destination: The destination of the request, either in UUID format or node name.""" raise NotImplementedError() - async def get_author(self, error_if_none: bool = False): + async def get_author(self, extra: dict, error_if_none: bool = False): """Try to find the identifier of the user that sent the message. That probably means, the database row identifying the user. Parameters: + extra: The ``extra`` dict passed to the Command error_if_none: Raise a :py:exc:`royalnet.error.UnregisteredError` if this is True and the call has no author. Raises: :py:exc:`royalnet.error.UnregisteredError` if ``error_if_none`` is set to True and no author is found.""" raise NotImplementedError() - - def register_net_handler(self, message_type: str, network_handler: typing.Callable): - """Register a new handler for messages received through Royalnet.""" - raise NotImplementedError()