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

Merge branch 'matrix'

This commit is contained in:
Steffo 2020-01-16 02:26:33 +01:00
commit 0715a655cd
17 changed files with 277 additions and 129 deletions

View file

@ -5,7 +5,7 @@
[tool.poetry] [tool.poetry]
name = "royalnet" name = "royalnet"
version = "5.2.1" version = "5.2.4"
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,13 +2,21 @@
from .version import VersionCommand from .version import VersionCommand
from .exception import ExceptionCommand from .exception import ExceptionCommand
from .excevent import ExceventCommand from .excevent import ExceventCommand
from .keyboardtest import KeyboardtestCommand
# Enter the commands of your Pack here! # Enter the commands of your Pack here!
available_commands = [ available_commands = [
VersionCommand, VersionCommand,
ExceptionCommand,
ExceventCommand,
] ]
# noinspection PyUnreachableCode
if __debug__:
available_commands = [
*available_commands,
ExceptionCommand,
ExceventCommand,
KeyboardtestCommand,
]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__
__all__ = [command.__name__ for command in available_commands] __all__ = [command.__name__ for command in available_commands]

View file

@ -8,6 +8,4 @@ class ExceptionCommand(Command):
description: str = "Raise an exception in the command." description: str = "Raise an exception in the command."
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
if not self.interface.cfg["exc_debug"]:
raise UserError(f"{self.interface.prefix}{self.name} is not enabled.")
raise Exception(f"{self.interface.prefix}{self.name} was called") raise Exception(f"{self.interface.prefix}{self.name} was called")

View file

@ -8,7 +8,5 @@ class ExceventCommand(Command):
description: str = "Call an event that raises an exception." description: str = "Call an event that raises an exception."
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
if not self.interface.cfg["exc_debug"]:
raise UserError(f"{self.interface.prefix}{self.name} is not enabled.")
await self.interface.call_herald_event(self.interface.name, "exception") await self.interface.call_herald_event(self.interface.name, "exception")
await data.reply("✅ Event called!") await data.reply("✅ Event called!")

View file

@ -0,0 +1,28 @@
from typing import *
from royalnet.commands import *
import functools
import asyncio
class KeyboardtestCommand(Command):
name: str = "keyboardtest"
description: str = "Create a new keyboard with the specified keys."
syntax: str = "{keys}+"
@staticmethod
async def echo(data: CommandData, echo: str):
await data.reply(echo)
async def run(self, args: CommandArgs, data: CommandData) -> None:
keys = []
for arg in args:
# noinspection PyTypeChecker
keys.append(KeyboardKey(interface=self.interface,
short=arg[0],
text=arg,
callback=functools.partial(self.echo, echo=arg)))
async with data.keyboard("This is a test keyboard.", keys):
await asyncio.sleep(10)
await data.reply("The keyboard is no longer in scope.")

View file

@ -10,6 +10,7 @@ from .errors import CommandError, \
ExternalError, \ ExternalError, \
UserError, \ UserError, \
ProgramError ProgramError
from .keyboardkey import KeyboardKey
__all__ = [ __all__ = [
"CommandInterface", "CommandInterface",
@ -23,5 +24,6 @@ __all__ = [
"ExternalError", "ExternalError",
"UserError", "UserError",
"ProgramError", "ProgramError",
"Event" "Event",
"KeyboardKey",
] ]

View file

@ -1,37 +1,47 @@
from asyncio import AbstractEventLoop import contextlib
from typing import Optional, TYPE_CHECKING import logging
import asyncio as aio
from typing import *
from sqlalchemy.orm.session import Session
from .errors import UnsupportedError from .errors import UnsupportedError
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
from ..utils import asyncify import royalnet.utils as ru
from sqlalchemy.orm.session import Session if TYPE_CHECKING:
from .keyboardkey import KeyboardKey
log = logging.getLogger(__name__)
class CommandData: class CommandData:
def __init__(self, interface: CommandInterface, session: Optional[Session], loop: AbstractEventLoop): def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop):
self._interface: CommandInterface = interface self._interface: CommandInterface = interface
self._session: Optional[Session] = session self.loop: aio.AbstractEventLoop = loop
self.loop: AbstractEventLoop = loop self._session = None
@property @property
def session(self) -> Session: def session(self):
"""Get the :class:`~royalnet.alchemy.Alchemy` :class:`Session`, if it is available.
Raises:
UnsupportedError: if no session is available."""
if self._session is None: if self._session is None:
raise UnsupportedError("'session' is not supported") if self._interface.alchemy is None:
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
self._session = ru.asyncify(self._interface.alchemy.Session)
return self._session return self._session
async def session_commit(self): async def session_commit(self):
"""Commit the changes to the session.""" if self._session:
await asyncify(self.session.commit) log.warning("Session had to be created to be committed")
# noinspection PyUnresolvedReferences
await ru.asyncify(self.session.commit)
async def session_close(self):
if self._session is not None:
await ru.asyncify(self._session.close)
async def reply(self, text: str) -> None: async def reply(self, text: str) -> None:
"""Send a text message to the channel where the call was made. """Send a text message to the channel where the call was made.
Parameters: Parameters:
text: The text to be sent, possibly formatted in the weird undescribed markup that I'm using.""" text: The text to be sent, possibly formatted in the weird undescribed markup that I'm using."""
raise UnsupportedError("'reply' is not supported") raise UnsupportedError(f"'{self.reply.__name__}' is not supported")
async def get_author(self, error_if_none: bool = False): async def get_author(self, error_if_none: bool = False):
"""Try to find the identifier of the user that sent the message. """Try to find the identifier of the user that sent the message.
@ -39,7 +49,7 @@ class CommandData:
Parameters: Parameters:
error_if_none: Raise an exception if this is True and the call has no author.""" error_if_none: Raise an exception if this is True and the call has no author."""
raise UnsupportedError("'get_author' is not supported") raise UnsupportedError(f"'{self.get_author.__name__}' is not supported")
async def delete_invoking(self, error_if_unavailable=False) -> None: async def delete_invoking(self, error_if_unavailable=False) -> None:
"""Delete the invoking message, if supported by the interface. """Delete the invoking message, if supported by the interface.
@ -49,4 +59,9 @@ class CommandData:
Parameters: Parameters:
error_if_unavailable: if True, raise an exception if the message cannot been deleted.""" error_if_unavailable: if True, raise an exception if the message cannot been deleted."""
if error_if_unavailable: if error_if_unavailable:
raise UnsupportedError("'delete_invoking' is not supported") raise UnsupportedError(f"'{self.delete_invoking.__name__}' is not supported")
@contextlib.asynccontextmanager
async def keyboard(self, text, keys: List["KeyboardKey"]):
yield
raise UnsupportedError(f"{self.keyboard.__name__} is not supported")

View file

@ -1,5 +1,5 @@
from typing import * from typing import *
from asyncio import AbstractEventLoop import asyncio as aio
from .errors import UnsupportedError from .errors import UnsupportedError
if TYPE_CHECKING: if TYPE_CHECKING:
from .event import Event from .event import Event
@ -40,7 +40,7 @@ class CommandInterface:
return self.serf.alchemy return self.serf.alchemy
@property @property
def loop(self) -> AbstractEventLoop: def loop(self) -> aio.AbstractEventLoop:
"""A shortcut for :attr:`.serf.loop`.""" """A shortcut for :attr:`.serf.loop`."""
if self.serf: if self.serf:
return self.serf.loop return self.serf.loop
@ -63,4 +63,4 @@ class CommandInterface:
You can run a function on a :class:`~royalnet.serf.discord.DiscordSerf` from a You can run a function on a :class:`~royalnet.serf.discord.DiscordSerf` from a
:class:`~royalnet.serf.telegram.TelegramSerf`. :class:`~royalnet.serf.telegram.TelegramSerf`.
""" """
raise UnsupportedError(f"{self.call_herald_event.__name__} is not supported on this platform") raise UnsupportedError(f"{self.call_herald_event.__name__} is not supported on this platform.")

View file

@ -0,0 +1,18 @@
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
async def press(self, data: CommandData):
await self.callback(data)

View file

@ -177,17 +177,16 @@ class Constellation:
constellation = self constellation = self
async def call_herald_event(ci, destination: str, event_name: str, **kwargs) -> Dict: async def call_herald_event(ci, destination: str, event_name: str, **kwargs) -> Dict:
"""Send a :class:`rh.Request` to a specific destination, and wait for a """Send a :class:`royalherald.Request` to a specific destination, and wait for a
:class:`rh.Response`.""" :class:`royalherald.Response`."""
if self.herald is None: if self.herald is None:
raise rc.UnsupportedError("`royalherald` is not enabled on this Constellation.") raise rc.UnsupportedError("`royalherald` is not enabled on this serf.")
request: rh.Request = rh.Request(handler=event_name, data=kwargs) request: rh.Request = rh.Request(handler=event_name, data=kwargs)
response: rh.Response = await self.herald.request(destination=destination, request=request) response: rh.Response = await self.herald.request(destination=destination, request=request)
if isinstance(response, rh.ResponseFailure): if isinstance(response, rh.ResponseFailure):
if response.name == "no_event": if response.name == "no_event":
raise rc.CommandError(f"There is no event named {event_name} in {destination}.") raise rc.ProgramError(f"There is no event named {event_name} in {destination}.")
elif response.name == "exception_in_event": elif response.name == "error_in_event":
# TODO: pretty sure there's a better way to do this
if response.extra_info["type"] == "CommandError": if response.extra_info["type"] == "CommandError":
raise rc.CommandError(response.extra_info["message"]) raise rc.CommandError(response.extra_info["message"])
elif response.extra_info["type"] == "UserError": elif response.extra_info["type"] == "UserError":
@ -201,13 +200,22 @@ class Constellation:
elif response.extra_info["type"] == "ExternalError": elif response.extra_info["type"] == "ExternalError":
raise rc.ExternalError(response.extra_info["message"]) raise rc.ExternalError(response.extra_info["message"])
else: else:
raise TypeError(f"Herald action call returned invalid error:\n" raise rc.ProgramError(f"Invalid error in Herald event '{event_name}':\n"
f"[p]{response}[/p]") f"[b]{response.extra_info['type']}[/b]\n"
f"{response.extra_info['message']}")
elif response.name == "unhandled_exception_in_event":
raise rc.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 rc.ProgramError(f"Unknown response in Herald event '{event_name}':\n"
f"[b]{response.name}[/b]"
f"[p]{response}[/p]")
elif isinstance(response, rh.ResponseSuccess): elif isinstance(response, rh.ResponseSuccess):
return response.data return response.data
else: else:
raise TypeError(f"Other Herald Link returned unknown response:\n" raise rc.ProgramError(f"Other Herald Link returned unknown response:\n"
f"[p]{response}[/p]") f"[p]{response}[/p]")
return GenericInterface return GenericInterface

View file

@ -3,7 +3,7 @@ import logging
import warnings import warnings
from typing import * from typing import *
import royalnet.backpack as rb import royalnet.backpack as rb
from royalnet.commands import * import royalnet.commands as rc
from royalnet.utils import asyncify from royalnet.utils import asyncify
from royalnet.serf import Serf from royalnet.serf import Serf
from .escape import escape from .escape import escape
@ -65,7 +65,9 @@ class DiscordSerf(Serf):
self.voice_players: List[VoicePlayer] = [] self.voice_players: List[VoicePlayer] = []
"""A :class:`list` of the :class:`VoicePlayer` in use by this :class:`DiscordSerf`.""" """A :class:`list` of the :class:`VoicePlayer` in use by this :class:`DiscordSerf`."""
def interface_factory(self) -> Type[CommandInterface]: self.Data: Type[rc.CommandData] = self.data_factory()
def interface_factory(self) -> Type[rc.CommandInterface]:
# noinspection PyPep8Naming # noinspection PyPep8Naming
GenericInterface = super().interface_factory() GenericInterface = super().interface_factory()
@ -76,15 +78,14 @@ class DiscordSerf(Serf):
return DiscordInterface return DiscordInterface
def data_factory(self) -> Type[CommandData]: def data_factory(self) -> Type[rc.CommandData]:
# noinspection PyMethodParameters,PyAbstractClass # noinspection PyMethodParameters,PyAbstractClass
class DiscordData(CommandData): class DiscordData(rc.CommandData):
def __init__(data, def __init__(data,
interface: CommandInterface, interface: rc.CommandInterface,
session,
loop: aio.AbstractEventLoop, loop: aio.AbstractEventLoop,
message: "discord.Message"): message: "discord.Message"):
super().__init__(interface=interface, session=session, loop=loop) super().__init__(interface=interface, loop=loop)
data.message = message data.message = message
async def reply(data, text: str): async def reply(data, text: str):
@ -98,7 +99,7 @@ class DiscordSerf(Serf):
query = query.filter(self.identity_column == user.id) query = query.filter(self.identity_column == user.id)
result = await asyncify(query.one_or_none) result = await asyncify(query.one_or_none)
if result is None and error_if_none: if result is None and error_if_none:
raise CommandError("You must be registered to use this command.") raise rc.CommandError("You must be registered to use this command.")
return result return result
async def delete_invoking(data, error_if_unavailable=False): async def delete_invoking(data, error_if_unavailable=False):
@ -138,7 +139,7 @@ class DiscordSerf(Serf):
else: else:
session = None session = None
# Prepare data # Prepare data
data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message) data = self.Data(interface=command.interface, loop=self.loop, message=message)
# Call the command # Call the command
await self.call(command, data, parameters) await self.call(command, data, parameters)
# Close the alchemy session # Close the alchemy session

View file

@ -1,5 +1,7 @@
from .matrixserf import MatrixSerf from .matrixserf import MatrixSerf
from .escape import escape
__all__ = [ __all__ = [
"MatrixSerf", "MatrixSerf",
] "escape",
]

View file

@ -0,0 +1,15 @@
def escape(string: str) -> str:
"""Escape a string to be sent through Matrix, and format it using RoyalCode.
Underlines are currently unsupported.
Warning:
Currently escapes everything, even items in code blocks."""
return string.replace("[b]", "**") \
.replace("[/b]", "**") \
.replace("[i]", "_") \
.replace("[/i]", "_") \
.replace("[c]", "`") \
.replace("[/c]", "`") \
.replace("[p]", "```") \
.replace("[/p]", "```")

View file

@ -6,6 +6,7 @@ import royalnet.backpack as rb
import royalnet.commands as rc import royalnet.commands as rc
import royalnet.utils as ru import royalnet.utils as ru
from ..serf import Serf from ..serf import Serf
from .escape import escape
try: try:
@ -50,6 +51,8 @@ class MatrixSerf(Serf):
self._started_timestamp: Optional[int] = None self._started_timestamp: Optional[int] = None
self.Data: Type[rc.CommandData] = self.data_factory()
def interface_factory(self) -> Type[rc.CommandInterface]: def interface_factory(self) -> Type[rc.CommandInterface]:
# noinspection PyPep8Naming # noinspection PyPep8Naming
GenericInterface = super().interface_factory() GenericInterface = super().interface_factory()
@ -63,21 +66,20 @@ class MatrixSerf(Serf):
def data_factory(self) -> Type[rc.CommandData]: def data_factory(self) -> Type[rc.CommandData]:
# noinspection PyMethodParameters,PyAbstractClass # noinspection PyMethodParameters,PyAbstractClass
class DiscordData(rc.CommandData): class MatrixData(rc.CommandData):
def __init__(data, def __init__(data,
interface: rc.CommandInterface, interface: rc.CommandInterface,
session,
loop: aio.AbstractEventLoop, loop: aio.AbstractEventLoop,
room: nio.MatrixRoom, room: nio.MatrixRoom,
event: nio.Event): event: nio.Event):
super().__init__(interface=interface, session=session, loop=loop) super().__init__(interface=interface, loop=loop)
data.room: nio.MatrixRoom = room data.room: nio.MatrixRoom = room
data.event: nio.Event = event data.event: nio.Event = event
async def reply(data, text: str): async def reply(data, text: str):
await self.client.room_send(room_id=data.room.room_id, message_type="m.room.message", content={ await self.client.room_send(room_id=data.room.room_id, message_type="m.room.message", content={
"msgtype": "m.text", "msgtype": "m.text",
"body": text "body": escape(text)
}) })
async def get_author(data, error_if_none=False): async def get_author(data, error_if_none=False):
@ -93,7 +95,7 @@ class MatrixSerf(Serf):
# Delete invoking does not really make sense on Matrix # Delete invoking does not really make sense on Matrix
return DiscordData return MatrixData
async def handle_message(self, room: "nio.MatrixRoom", event: "nio.RoomMessageText"): async def handle_message(self, room: "nio.MatrixRoom", event: "nio.RoomMessageText"):
# Skip events happened before the startup of the Serf # Skip events happened before the startup of the Serf
@ -122,7 +124,7 @@ class MatrixSerf(Serf):
else: else:
session = None session = None
# Prepare data # Prepare data
data = self.Data(interface=command.interface, session=session, loop=self.loop, room=room, event=event) data = self.Data(interface=command.interface, loop=self.loop, room=room, event=event)
# Call the command # Call the command
await self.call(command, data, parameters) await self.call(command, data, parameters)
# Close the alchemy session # Close the alchemy session

View file

@ -102,9 +102,6 @@ class Serf:
self.Interface: Type[CommandInterface] = self.interface_factory() self.Interface: Type[CommandInterface] = self.interface_factory()
"""The :class:`CommandInterface` class of this Serf.""" """The :class:`CommandInterface` class of this Serf."""
self.Data: Type[CommandData] = self.data_factory()
"""The :class:`CommandData` 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."""
@ -188,6 +185,10 @@ class Serf:
raise ProgramError(f"Unhandled exception in Herald event '{event_name}':\n" raise ProgramError(f"Unhandled exception in Herald event '{event_name}':\n"
f"[b]{response.extra_info['type']}[/b]\n" f"[b]{response.extra_info['type']}[/b]\n"
f"{response.extra_info['message']}") 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): elif isinstance(response, rh.ResponseSuccess):
return response.data return response.data
else: else:
@ -196,10 +197,6 @@ class Serf:
return GenericInterface return GenericInterface
def data_factory(self) -> Type[CommandData]:
"""Create the :class:`CommandData` for the Serf."""
raise NotImplementedError()
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
@ -312,6 +309,34 @@ class Serf:
except Exception as e: except Exception as e:
ru.sentry_exc(e) ru.sentry_exc(e)
await data.reply(f"⛔️ [b]{e.__class__.__name__}[/b]\n" + '\n'.join(e.args)) await data.reply(f"⛔️ [b]{e.__class__.__name__}[/b]\n" + '\n'.join(e.args))
finally:
await data.session_close()
async def press(self, key: KeyboardKey, data: CommandData):
log.info(f"Calling key_callback: {repr(key)}")
try:
await key.press(data)
except InvalidInputError as e:
await data.reply(f"⚠️ {e.message}\n"
f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]")
except UserError as e:
await data.reply(f"⚠️ {e.message}")
except UnsupportedError as e:
await data.reply(f"⚠️ {e.message}")
except ExternalError as e:
await data.reply(f"⚠️ {e.message}")
except ConfigurationError as e:
await data.reply(f"⚠️ {e.message}")
except ProgramError as e:
await data.reply(f"⛔️ {e.message}")
except CommandError as e:
await data.reply(f"⚠️ {e.message}")
except Exception as e:
ru.sentry_exc(e)
await data.reply(f"⛔️ [b]{e.__class__.__name__}[/b]\n" + '\n'.join(e.args))
finally:
await data.session_close()
async def run(self): async def run(self):
"""A coroutine that starts the event loop and handles command calls.""" """A coroutine that starts the event loop and handles command calls."""

View file

@ -1,8 +1,10 @@
import contextlib
import logging import logging
import asyncio as aio import asyncio as aio
import uuid
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.utils import asyncify import royalnet.utils as ru
import royalnet.backpack as rb import royalnet.backpack as rb
from .escape import escape from .escape import escape
from ..serf import Serf from ..serf import Serf
@ -57,6 +59,11 @@ class TelegramSerf(Serf):
self.update_offset: int = -100 self.update_offset: int = -100
"""The current `update offset <https://core.telegram.org/bots/api#getupdates>`_.""" """The current `update offset <https://core.telegram.org/bots/api#getupdates>`_."""
self.key_callbacks: Dict[str, rc.KeyboardKey] = {}
self.MessageData: Type[rc.CommandData] = self.message_data_factory()
self.CallbackData: Type[rc.CommandData] = self.callback_data_factory()
@staticmethod @staticmethod
async def api_call(f: Callable, *args, **kwargs) -> Optional: async def api_call(f: Callable, *args, **kwargs) -> Optional:
"""Call a :class:`telegram.Bot` method safely, without getting a mess of errors raised. """Call a :class:`telegram.Bot` method safely, without getting a mess of errors raised.
@ -64,7 +71,7 @@ class TelegramSerf(Serf):
The method may return None if it was decided that the call should be skipped.""" The method may return None if it was decided that the call should be skipped."""
while True: while True:
try: try:
return await asyncify(f, *args, **kwargs) return await ru.asyncify(f, *args, **kwargs)
except telegram.error.TimedOut as error: except telegram.error.TimedOut as error:
log.debug(f"Timed out during {f.__qualname__} (retrying immediatly): {error}") log.debug(f"Timed out during {f.__qualname__} (retrying immediatly): {error}")
continue continue
@ -84,11 +91,11 @@ class TelegramSerf(Serf):
continue continue
except Exception as error: except Exception as error:
log.error(f"{error.__class__.__qualname__} during {f} (skipping): {error}") log.error(f"{error.__class__.__qualname__} during {f} (skipping): {error}")
TelegramSerf.sentry_exc(error) ru.sentry_exc(error)
break break
return None return None
def interface_factory(self) -> Type[CommandInterface]: def interface_factory(self) -> Type[rc.CommandInterface]:
# noinspection PyPep8Naming # noinspection PyPep8Naming
GenericInterface = super().interface_factory() GenericInterface = super().interface_factory()
@ -99,54 +106,103 @@ class TelegramSerf(Serf):
return TelegramInterface return TelegramInterface
def data_factory(self) -> Type[CommandData]: def message_data_factory(self) -> Type[rc.CommandData]:
# noinspection PyMethodParameters # noinspection PyMethodParameters
class TelegramData(CommandData): class TelegramMessageData(rc.CommandData):
def __init__(data, def __init__(data,
interface: CommandInterface, interface: rc.CommandInterface,
session,
loop: aio.AbstractEventLoop, loop: aio.AbstractEventLoop,
update: telegram.Update): message: telegram.Message):
super().__init__(interface=interface, session=session, loop=loop) super().__init__(interface=interface, loop=loop)
data.update = update data.message: telegram.Message = message
async def reply(data, text: str): async def reply(data, text: str):
await self.api_call(data.update.effective_chat.send_message, await self.api_call(data.message.chat.send_message,
escape(text), escape(text),
parse_mode="HTML", parse_mode="HTML",
disable_web_page_preview=True) disable_web_page_preview=True)
async def get_author(data, error_if_none=False): async def get_author(data, error_if_none=False):
if data.update.message is not None: user: Optional[telegram.User] = data.message.from_user
user: telegram.User = data.update.message.from_user
elif data.update.callback_query is not None:
user: telegram.User = data.update.callback_query.from_user
else:
raise CommandError("Command caller can not be determined")
if user is None: if user is None:
if error_if_none: if error_if_none:
raise CommandError("No command caller for this message") raise rc.CommandError("No command caller for this message")
return None return None
query = data.session.query(self.master_table) query = data.session.query(self.master_table)
for link in self.identity_chain: for link in self.identity_chain:
query = query.join(link.mapper.class_) query = query.join(link.mapper.class_)
query = query.filter(self.identity_column == user.id) query = query.filter(self.identity_column == user.id)
result = await asyncify(query.one_or_none) result = await ru.asyncify(query.one_or_none)
if result is None and error_if_none: if result is None and error_if_none:
raise CommandError("Command caller is not registered") raise rc.CommandError("Command caller is not registered")
return result return result
async def delete_invoking(data, error_if_unavailable=False) -> None: async def delete_invoking(data, error_if_unavailable=False) -> None:
message: telegram.Message = data.update.message await self.api_call(data.message.delete)
await self.api_call(message.delete)
return TelegramData @contextlib.asynccontextmanager
async def keyboard(data, text: str, keys: List[rc.KeyboardKey]):
tg_rows = []
key_uids = []
for key in keys:
uid: str = str(uuid.uuid4())
key_uids.append(uid)
self.key_callbacks[uid] = key
tg_button: telegram.InlineKeyboardButton = telegram.InlineKeyboardButton(key.text,
callback_data=uid)
tg_row: List[telegram.InlineKeyboardButton] = [tg_button]
tg_rows.append(tg_row)
tg_markup: telegram.InlineKeyboardMarkup = telegram.InlineKeyboardMarkup(tg_rows)
message: telegram.Message = await self.api_call(data.message.chat.send_message,
escape(text),
parse_mode="HTML",
disable_web_page_preview=True,
reply_markup=tg_markup)
yield
await self.api_call(message.edit_reply_markup, reply_markup=None)
for uid in key_uids:
del self.key_callbacks[uid]
return TelegramMessageData
def callback_data_factory(self) -> Type[rc.CommandData]:
# noinspection PyMethodParameters
class TelegramKeyboardData(rc.CommandData):
def __init__(data,
interface: rc.CommandInterface,
loop: aio.AbstractEventLoop,
cbq: telegram.CallbackQuery):
super().__init__(interface=interface, loop=loop)
data.cbq: telegram.CallbackQuery = cbq
async def reply(data, text: str):
await self.api_call(data.cbq.answer,
escape(text))
async def get_author(data, error_if_none=False):
user = data.cbq.from_user
if user is None:
if error_if_none:
raise rc.CommandError("No command caller for this message")
return None
query = data.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)
result = await ru.asyncify(query.one_or_none)
if result is None and error_if_none:
raise rc.CommandError("Command caller is not registered")
return result
return TelegramKeyboardData
async def answer_cbq(self, cbq, text, alert=False):
await self.api_call(cbq.answer, text=text, show_alert=alert)
async def handle_update(self, update: telegram.Update): async def handle_update(self, update: telegram.Update):
"""Delegate :class:`telegram.Update` handling to the correct message type submethod.""" """Delegate :class:`telegram.Update` handling to the correct message type submethod."""
if update.message is not None: if update.message is not None:
await self.handle_message(update) await self.handle_message(update.message)
elif update.edited_message is not None: elif update.edited_message is not None:
pass pass
elif update.channel_post is not None: elif update.channel_post is not None:
@ -158,7 +214,7 @@ class TelegramSerf(Serf):
elif update.chosen_inline_result is not None: elif update.chosen_inline_result is not None:
pass pass
elif update.callback_query is not None: elif update.callback_query is not None:
pass await self.handle_callback_query(update.callback_query)
elif update.shipping_query is not None: elif update.shipping_query is not None:
pass pass
elif update.pre_checkout_query is not None: elif update.pre_checkout_query is not None:
@ -168,9 +224,8 @@ class TelegramSerf(Serf):
else: else:
log.warning(f"Unknown update type: {update}") log.warning(f"Unknown update type: {update}")
async def handle_message(self, update: telegram.Update): async def handle_message(self, message: telegram.Message):
"""What should be done when a :class:`telegram.Message` is received?""" """What should be done when a :class:`telegram.Message` is received?"""
message: telegram.Message = update.message
text: str = message.text text: str = message.text
# Try getting the caption instead # Try getting the caption instead
if text is None: if text is None:
@ -191,46 +246,19 @@ class TelegramSerf(Serf):
# Skip the message # Skip the message
return return
# Send a typing notification # Send a typing notification
await self.api_call(update.message.chat.send_action, telegram.ChatAction.TYPING) await self.api_call(message.chat.send_action, telegram.ChatAction.TYPING)
# Prepare data # Prepare data
if self.alchemy is not None: data = self.MessageData(interface=command.interface, loop=self.loop, message=message)
session = await asyncify(self.alchemy.Session)
else:
session = None
# Prepare data
data = self.Data(interface=command.interface, session=session, loop=self.loop, update=update)
# Call the command # Call the command
await self.call(command, data, parameters) await self.call(command, data, parameters)
# Close the alchemy session
if session is not None:
await asyncify(session.close)
async def handle_edited_message(self, update: telegram.Update): async def handle_callback_query(self, cbq: telegram.CallbackQuery):
pass uid = cbq.data
if uid not in self.key_callbacks:
async def handle_channel_post(self, update: telegram.Update): await self.api_call(cbq.answer, text="⚠️ This keyboard has expired.", show_alert=True)
pass key: rc.KeyboardKey = self.key_callbacks[uid]
data: rc.CommandData = self.CallbackData(interface=key.interface, loop=self.loop, cbq=cbq)
async def handle_edited_channel_post(self, update: telegram.Update): await self.press(key, data)
pass
async def handle_inline_query(self, update: telegram.Update):
pass
async def handle_chosen_inline_result(self, update: telegram.Update):
pass
async def handle_callback_query(self, update: telegram.Update):
pass
async def handle_shipping_query(self, update: telegram.Update):
pass
async def handle_pre_checkout_query(self, update: telegram.Update):
pass
async def handle_poll(self, update: telegram.Update):
pass
async def run(self): async def run(self):
await super().run() await super().run()

View file

@ -1 +1 @@
semantic = "5.2.1" semantic = "5.2.4"