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:
commit
0715a655cd
17 changed files with 277 additions and 129 deletions
|
@ -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+"
|
||||||
|
|
|
@ -2,12 +2,20 @@
|
||||||
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,
|
||||||
|
]
|
||||||
|
|
||||||
|
# noinspection PyUnreachableCode
|
||||||
|
if __debug__:
|
||||||
|
available_commands = [
|
||||||
|
*available_commands,
|
||||||
ExceptionCommand,
|
ExceptionCommand,
|
||||||
ExceventCommand,
|
ExceventCommand,
|
||||||
|
KeyboardtestCommand,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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!")
|
||||||
|
|
28
royalnet/backpack/commands/keyboardtest.py
Normal file
28
royalnet/backpack/commands/keyboardtest.py
Normal 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.")
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
18
royalnet/commands/keyboardkey.py
Normal file
18
royalnet/commands/keyboardkey.py
Normal 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)
|
|
@ -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,12 +200,21 @@ 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"[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]")
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from .matrixserf import MatrixSerf
|
from .matrixserf import MatrixSerf
|
||||||
|
from .escape import escape
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"MatrixSerf",
|
"MatrixSerf",
|
||||||
|
"escape",
|
||||||
]
|
]
|
15
royalnet/serf/matrix/escape.py
Normal file
15
royalnet/serf/matrix/escape.py
Normal 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]", "```")
|
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
semantic = "5.2.1"
|
semantic = "5.2.4"
|
||||||
|
|
Loading…
Reference in a new issue