diff --git a/royalnet/__init__.py b/royalnet/__init__.py index 1977c838..71fbe7d4 100644 --- a/royalnet/__init__.py +++ b/royalnet/__init__.py @@ -1,3 +1,4 @@ -from . import audio, bots, commands, database, network, utils, error, web, version +from . import audio, bots, database, network, utils, error, web, version +from royalnet import commands __all__ = ["audio", "bots", "commands", "database", "network", "utils", "error", "web", "version"] diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index 2c73b29d..df9a5fd7 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -1,9 +1,8 @@ import discord -import asyncio import typing import logging as _logging from .generic import GenericBot -from ..commands import NullCommand +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 diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index 05aaf66d..16d18f78 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -3,7 +3,7 @@ import typing import asyncio import logging from ..utils import Command, NetworkHandler, Call -from ..commands import NullCommand +from royalnet.commands import NullCommand from ..network import RoyalnetLink, Request, Response, ResponseError, RoyalnetConfig from ..database import Alchemy, DatabaseConfig, relationshiplinkchain diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index 7cccfe39..a1914dd8 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -1,10 +1,9 @@ import telegram import telegram.utils.request -import asyncio import typing import logging as _logging from .generic import GenericBot -from ..commands import NullCommand +from royalnet.commands import NullCommand from ..utils import asyncify, Call, Command, telegram_escape from ..error import UnregisteredError, InvalidConfigError, RoyalnetResponseError from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index f6712cdd..a9ec2b40 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -1,39 +1,4 @@ -"""Commands that can be used in bots. +from .commandinterface import CommandInterface +from .command import Command -These probably won't suit your needs, as they are tailored for the bots of the Royal Games gaming community, but they may be useful to develop new ones.""" - -from .null import NullCommand -from .ping import PingCommand -from .ship import ShipCommand -from .smecds import SmecdsCommand -from .ciaoruozi import CiaoruoziCommand -from .color import ColorCommand -from .sync import SyncCommand -from .diario import DiarioCommand -from .rage import RageCommand -from .dateparser import DateparserCommand -from .author import AuthorCommand -from .reminder import ReminderCommand -from .kvactive import KvactiveCommand -from .kv import KvCommand -from .kvroll import KvrollCommand -from .videoinfo import VideoinfoCommand -from .summon import SummonCommand -from .play import PlayCommand -from .skip import SkipCommand -from .playmode import PlaymodeCommand -from .videochannel import VideochannelCommand -from .missing import MissingCommand -from .cv import CvCommand -from .pause import PauseCommand -from .queue import QueueCommand -from .royalnetprofile import RoyalnetprofileCommand -from .id import IdCommand -from .dlmusic import DlmusicCommand - - -__all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand", - "SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand", - "KvactiveCommand", "KvCommand", "KvrollCommand", "VideoinfoCommand", "SummonCommand", "PlayCommand", - "SkipCommand", "PlaymodeCommand", "VideochannelCommand", "MissingCommand", "CvCommand", "PauseCommand", - "QueueCommand", "RoyalnetprofileCommand", "IdCommand", "DlmusicCommand"] +__all__ = ["CommandInterface", "Command"] diff --git a/royalnet/commands/command.py b/royalnet/commands/command.py new file mode 100644 index 00000000..a24669dc --- /dev/null +++ b/royalnet/commands/command.py @@ -0,0 +1,28 @@ +import typing +from ..error import UnsupportedError +from .commandinterface import CommandInterface + + +class Command: + name: str = NotImplemented + """The main name of the command. + To have ``/example`` on Telegram, the name should be ``example``.""" + + description: str = NotImplemented + """A small description of the command, to be displayed when the command is being autocompleted.""" + + syntax: str = NotImplemented + """The syntax of the command, to be displayed when a :py:exc:`royalnet.error.InvalidInputError` is raised, + in the format ``(required_arg) [optional_arg]``.""" + + require_alchemy_tables: typing.Set = set() + """A set of :py:class:`royalnet.database` tables that must exist for this command to work.""" + + def __init__(self, interface: CommandInterface): + self.interface = interface + + async def common(self) -> 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/commands/commandinterface.py b/royalnet/commands/commandinterface.py new file mode 100644 index 00000000..f74404a1 --- /dev/null +++ b/royalnet/commands/commandinterface.py @@ -0,0 +1,44 @@ +import typing +if typing.TYPE_CHECKING: + from ..database import Alchemy + from ..bots import GenericBot + + +class CommandInterface: + name: str = NotImplemented + prefix: str = NotImplemented + alchemy: "Alchemy" = NotImplemented + bot: "GenericBot" = NotImplemented + + def __init__(self, alias: str): + self.session = self.alchemy.Session() + + async def reply(self, text: str) -> None: + """Send a text message to the channel where the call was made. + + Parameters: + 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: + """Send data through a :py:class:`royalnet.network.RoyalnetLink` and wait for a :py:class:`royalnet.network.Reply`. + + Parameters: + 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): + """Try to find the identifier of the user that sent the message. + That probably means, the database row identifying the user. + + Parameters: + 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() diff --git a/royalnet/commands/royalgames/__init__.py b/royalnet/commands/royalgames/__init__.py new file mode 100644 index 00000000..33f07a4b --- /dev/null +++ b/royalnet/commands/royalgames/__init__.py @@ -0,0 +1,40 @@ +"""Commands that can be used in bots. + +These probably won't suit your needs, as they are tailored for the bots of the Royal Games gaming community, but they + may be useful to develop new ones.""" + +from .null import NullCommand +from .ping import PingCommand +from .ship import ShipCommand +from .smecds import SmecdsCommand +from .ciaoruozi import CiaoruoziCommand +from .color import ColorCommand +from .sync import SyncCommand +from .diario import DiarioCommand +from .rage import RageCommand +from .dateparser import DateparserCommand +from .author import AuthorCommand +from .reminder import ReminderCommand +from .kvactive import KvactiveCommand +from .kv import KvCommand +from .kvroll import KvrollCommand +from .videoinfo import VideoinfoCommand +from .summon import SummonCommand +from .play import PlayCommand +from .skip import SkipCommand +from .playmode import PlaymodeCommand +from .videochannel import VideochannelCommand +from .missing import MissingCommand +from .cv import CvCommand +from .pause import PauseCommand +from .queue import QueueCommand +from .royalnetprofile import RoyalnetprofileCommand +from .id import IdCommand +from .dlmusic import DlmusicCommand + + +__all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand", + "SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand", + "KvactiveCommand", "KvCommand", "KvrollCommand", "VideoinfoCommand", "SummonCommand", "PlayCommand", + "SkipCommand", "PlaymodeCommand", "VideochannelCommand", "MissingCommand", "CvCommand", "PauseCommand", + "QueueCommand", "RoyalnetprofileCommand", "IdCommand", "DlmusicCommand"] diff --git a/royalnet/commands/author.py b/royalnet/commands/royalgames/author.py similarity index 100% rename from royalnet/commands/author.py rename to royalnet/commands/royalgames/author.py diff --git a/royalnet/commands/ciaoruozi.py b/royalnet/commands/royalgames/ciaoruozi.py similarity index 100% rename from royalnet/commands/ciaoruozi.py rename to royalnet/commands/royalgames/ciaoruozi.py diff --git a/royalnet/commands/color.py b/royalnet/commands/royalgames/color.py similarity index 100% rename from royalnet/commands/color.py rename to royalnet/commands/royalgames/color.py diff --git a/royalnet/commands/cv.py b/royalnet/commands/royalgames/cv.py similarity index 100% rename from royalnet/commands/cv.py rename to royalnet/commands/royalgames/cv.py diff --git a/royalnet/commands/dateparser.py b/royalnet/commands/royalgames/dateparser.py similarity index 100% rename from royalnet/commands/dateparser.py rename to royalnet/commands/royalgames/dateparser.py diff --git a/royalnet/commands/debug_create.py b/royalnet/commands/royalgames/debug_create.py similarity index 100% rename from royalnet/commands/debug_create.py rename to royalnet/commands/royalgames/debug_create.py diff --git a/royalnet/commands/diario.py b/royalnet/commands/royalgames/diario.py similarity index 100% rename from royalnet/commands/diario.py rename to royalnet/commands/royalgames/diario.py diff --git a/royalnet/commands/dlmusic.py b/royalnet/commands/royalgames/dlmusic.py similarity index 100% rename from royalnet/commands/dlmusic.py rename to royalnet/commands/royalgames/dlmusic.py diff --git a/royalnet/commands/error_handler.py b/royalnet/commands/royalgames/error_handler.py similarity index 100% rename from royalnet/commands/error_handler.py rename to royalnet/commands/royalgames/error_handler.py diff --git a/royalnet/commands/id.py b/royalnet/commands/royalgames/id.py similarity index 100% rename from royalnet/commands/id.py rename to royalnet/commands/royalgames/id.py diff --git a/royalnet/commands/kv.py b/royalnet/commands/royalgames/kv.py similarity index 100% rename from royalnet/commands/kv.py rename to royalnet/commands/royalgames/kv.py diff --git a/royalnet/commands/kvactive.py b/royalnet/commands/royalgames/kvactive.py similarity index 100% rename from royalnet/commands/kvactive.py rename to royalnet/commands/royalgames/kvactive.py diff --git a/royalnet/commands/kvroll.py b/royalnet/commands/royalgames/kvroll.py similarity index 100% rename from royalnet/commands/kvroll.py rename to royalnet/commands/royalgames/kvroll.py diff --git a/royalnet/commands/missing.py b/royalnet/commands/royalgames/missing.py similarity index 100% rename from royalnet/commands/missing.py rename to royalnet/commands/royalgames/missing.py diff --git a/royalnet/commands/null.py b/royalnet/commands/royalgames/null.py similarity index 100% rename from royalnet/commands/null.py rename to royalnet/commands/royalgames/null.py diff --git a/royalnet/commands/pause.py b/royalnet/commands/royalgames/pause.py similarity index 100% rename from royalnet/commands/pause.py rename to royalnet/commands/royalgames/pause.py diff --git a/royalnet/commands/ping.py b/royalnet/commands/royalgames/ping.py similarity index 100% rename from royalnet/commands/ping.py rename to royalnet/commands/royalgames/ping.py diff --git a/royalnet/commands/play.py b/royalnet/commands/royalgames/play.py similarity index 100% rename from royalnet/commands/play.py rename to royalnet/commands/royalgames/play.py diff --git a/royalnet/commands/playmode.py b/royalnet/commands/royalgames/playmode.py similarity index 100% rename from royalnet/commands/playmode.py rename to royalnet/commands/royalgames/playmode.py diff --git a/royalnet/commands/queue.py b/royalnet/commands/royalgames/queue.py similarity index 100% rename from royalnet/commands/queue.py rename to royalnet/commands/royalgames/queue.py diff --git a/royalnet/commands/rage.py b/royalnet/commands/royalgames/rage.py similarity index 100% rename from royalnet/commands/rage.py rename to royalnet/commands/royalgames/rage.py diff --git a/royalnet/commands/reminder.py b/royalnet/commands/royalgames/reminder.py similarity index 100% rename from royalnet/commands/reminder.py rename to royalnet/commands/royalgames/reminder.py diff --git a/royalnet/commands/royalnetprofile.py b/royalnet/commands/royalgames/royalnetprofile.py similarity index 100% rename from royalnet/commands/royalnetprofile.py rename to royalnet/commands/royalgames/royalnetprofile.py diff --git a/royalnet/commands/ship.py b/royalnet/commands/royalgames/ship.py similarity index 100% rename from royalnet/commands/ship.py rename to royalnet/commands/royalgames/ship.py diff --git a/royalnet/commands/skip.py b/royalnet/commands/royalgames/skip.py similarity index 100% rename from royalnet/commands/skip.py rename to royalnet/commands/royalgames/skip.py diff --git a/royalnet/commands/smecds.py b/royalnet/commands/royalgames/smecds.py similarity index 100% rename from royalnet/commands/smecds.py rename to royalnet/commands/royalgames/smecds.py diff --git a/royalnet/commands/summon.py b/royalnet/commands/royalgames/summon.py similarity index 100% rename from royalnet/commands/summon.py rename to royalnet/commands/royalgames/summon.py diff --git a/royalnet/commands/sync.py b/royalnet/commands/royalgames/sync.py similarity index 100% rename from royalnet/commands/sync.py rename to royalnet/commands/royalgames/sync.py diff --git a/royalnet/commands/videochannel.py b/royalnet/commands/royalgames/videochannel.py similarity index 100% rename from royalnet/commands/videochannel.py rename to royalnet/commands/royalgames/videochannel.py diff --git a/royalnet/commands/videoinfo.py b/royalnet/commands/royalgames/videoinfo.py similarity index 100% rename from royalnet/commands/videoinfo.py rename to royalnet/commands/royalgames/videoinfo.py diff --git a/royalnet/royalgames.py b/royalnet/royalgames.py index 7882a61e..9ce17d8e 100644 --- a/royalnet/royalgames.py +++ b/royalnet/royalgames.py @@ -6,8 +6,8 @@ import logging from royalnet.bots import DiscordBot, DiscordConfig, TelegramBot, TelegramConfig from royalnet.commands import * # noinspection PyUnresolvedReferences -from royalnet.commands.debug_create import DebugCreateCommand -from royalnet.commands.error_handler import ErrorHandlerCommand +from royalnet.commands import DebugCreateCommand +from royalnet.commands import ErrorHandlerCommand from royalnet.network import RoyalnetServer, RoyalnetConfig from royalnet.database import DatabaseConfig from royalnet.database.tables import Royal, Telegram, Discord diff --git a/royalnet/shareradio.py b/royalnet/shareradio.py index 02335ed6..fd1d0c16 100644 --- a/royalnet/shareradio.py +++ b/royalnet/shareradio.py @@ -6,11 +6,9 @@ import logging from royalnet.bots import DiscordBot, DiscordConfig, TelegramBot, TelegramConfig from royalnet.commands import * # noinspection PyUnresolvedReferences -from royalnet.commands.debug_create import DebugCreateCommand -from royalnet.commands.error_handler import ErrorHandlerCommand +from royalnet.commands import DebugCreateCommand +from royalnet.commands import ErrorHandlerCommand from royalnet.network import RoyalnetServer, RoyalnetConfig -from royalnet.database import DatabaseConfig -from royalnet.database.tables import Royal, Telegram, Discord loop = asyncio.get_event_loop() diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index 353b5fe7..a7b6dc40 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -2,8 +2,6 @@ from .asyncify import asyncify from .escaping import telegram_escape, discord_escape -from .call import Call -from .command import Command from .commandargs import CommandArgs from .safeformat import safeformat from .classdictjanitor import cdj @@ -11,6 +9,6 @@ from .sleepuntil import sleep_until from .networkhandler import NetworkHandler from .formatters import andformat, plusformat, fileformat, ytdldateformat, numberemojiformat -__all__ = ["asyncify", "Call", "Command", "safeformat", "cdj", "sleep_until", "plusformat", "CommandArgs", +__all__ = ["asyncify", "safeformat", "cdj", "sleep_until", "plusformat", "CommandArgs", "NetworkHandler", "andformat", "plusformat", "fileformat", "ytdldateformat", "numberemojiformat", "telegram_escape", "discord_escape"] diff --git a/royalnet/utils/call.py b/royalnet/utils/call.py deleted file mode 100644 index b9ccf58c..00000000 --- a/royalnet/utils/call.py +++ /dev/null @@ -1,100 +0,0 @@ -import typing -import asyncio -from .command import Command -from .commandargs import CommandArgs -if typing.TYPE_CHECKING: - from ..database import Alchemy - - -class Call: - """A command call. An abstract class, sub-bots should create a new call class from this. - - Attributes: - interface_name: The name of the interface that is calling the command. For example, ``telegram``, or ``discord``. - interface_obj: The main object of the interface that is calling the command. For example, the :py:class:`royalnet.bots.TelegramBot` object. - interface_prefix: The command prefix used by the interface. For example, ``/``, or ``!``. - alchemy: The :py:class:`royalnet.database.Alchemy` object associated to this interface. May be None if the interface is not connected to any database.""" - - # These parameters / methods should be overridden - interface_name = NotImplemented - interface_obj = NotImplemented - interface_prefix = NotImplemented - alchemy: "Alchemy" = NotImplemented - - async def reply(self, text: str) -> None: - """Send a text message to the channel where the call was made. - - Parameters: - 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: - """Send data through a :py:class:`royalnet.network.RoyalnetLink` and wait for a :py:class:`royalnet.network.Reply`. - - Parameters: - 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=False): - """Try to find the universal identifier of the user that sent the message. - That probably means, the database row identifying the user. - - Parameters: - 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() - - # These parameters / methods should be left alone - def __init__(self, - channel, - command: typing.Type[Command], - command_args: typing.List[str] = None, - loop: asyncio.AbstractEventLoop = None, - **kwargs): - """Create the call. - - Parameters: - channel: The channel object this call was sent in. - command: The command to be called. - command_args: The arguments to be passed to the command - kwargs: Additional optional keyword arguments that may be passed to the command, possibly specific to the bot. - """ - if command_args is None: - command_args = [] - if loop is None: - self.loop = asyncio.get_event_loop() - else: - self.loop = loop - self.channel = channel - self.command = command - self.args = CommandArgs(command_args) - self.kwargs = kwargs - self.session = None - - async def _session_init(self): - """If the command requires database access, create a :py:class:`royalnet.database.Alchemy` session for this call, otherwise, do nothing.""" - if not self.command.require_alchemy_tables: - return - self.session = await self.loop.run_in_executor(None, self.alchemy.Session) - - async def session_end(self): - """Close the previously created :py:class:`royalnet.database.Alchemy` session for this call (if it was created).""" - if not self.session: - return - self.session.close() - - async def run(self): - """Execute the called command, and return the command result.""" - await self._session_init() - try: - coroutine = getattr(self.command, self.interface_name) - except AttributeError: - coroutine = self.command.common - try: - result = await coroutine(self) - finally: - await self.session_end() - return result diff --git a/royalnet/utils/command.py b/royalnet/utils/command.py deleted file mode 100644 index 7c6a9e7d..00000000 --- a/royalnet/utils/command.py +++ /dev/null @@ -1,35 +0,0 @@ -import typing -from ..error import UnsupportedError -if typing.TYPE_CHECKING: - from .call import Call - from ..utils import NetworkHandler - - -class Command: - """The base class from which all commands should inherit.""" - - command_name: typing.Optional[str] = NotImplemented - """The name of the command. To have ``/example`` on Telegram, the name should be ``example``. If the name is None or empty, the command won't be registered.""" - - command_description: str = NotImplemented - """A small description of the command, to be displayed when the command is being autocompleted.""" - - command_syntax: str = NotImplemented - """The syntax of the command, to be displayed when a :py:exc:`royalnet.error.InvalidInputError` is raised, in the format ``(required_arg) [optional_arg]``.""" - - require_alchemy_tables: typing.Set = set() - """A set of :py:class:`royalnet.database` tables, that must exist for this command to work.""" - - network_handlers: typing.List[typing.Type["NetworkHandler"]] = [] - """A set of :py:class:`royalnet.utils.NetworkHandler`s that must exist for this command to work.""" - - @classmethod - async def common(cls, call: "Call"): - raise UnsupportedError() - - @classmethod - def network_handler_dict(cls): - d = {} - for network_handler in cls.network_handlers: - d[network_handler.message_type] = network_handler - return d