mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
le epic commit
This commit is contained in:
parent
f63520f385
commit
a2f2aa6855
11 changed files with 257 additions and 198 deletions
11
poetry.lock
generated
11
poetry.lock
generated
|
@ -1202,13 +1202,24 @@ websockets = [
|
|||
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
|
||||
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
|
||||
{file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
|
||||
{file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"},
|
||||
{file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"},
|
||||
{file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"},
|
||||
{file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"},
|
||||
{file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"},
|
||||
{file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"},
|
||||
{file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
|
||||
{file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"},
|
||||
{file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"},
|
||||
{file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"},
|
||||
{file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"},
|
||||
{file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"},
|
||||
{file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"},
|
||||
{file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"},
|
||||
{file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"},
|
||||
{file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"},
|
||||
{file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"},
|
||||
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
|
||||
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
|
||||
]
|
||||
yarl = [
|
||||
|
|
|
@ -12,6 +12,9 @@ except ImportError:
|
|||
coloredlogs = None
|
||||
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("-c", "--config-filename", default="./config.toml", type=str,
|
||||
help="The filename of the Royalnet configuration file.")
|
||||
|
@ -31,7 +34,7 @@ def run(config_filename: str):
|
|||
stream_handler.formatter = Formatter("{asctime}\t| {processName}\t| {name}\t| {message}",
|
||||
style="{")
|
||||
royalnet_log.addHandler(stream_handler)
|
||||
royalnet_log.info("Logging: ready")
|
||||
log.info("Logging: ready")
|
||||
|
||||
herald_process: typing.Optional[multiprocessing.Process] = None
|
||||
herald_config = r.herald.Config(name="<server>",
|
||||
|
|
|
@ -4,11 +4,11 @@ from .telegram import Telegram
|
|||
from .discord import Discord
|
||||
|
||||
# Enter the tables of your Pack here!
|
||||
available_tables = [
|
||||
available_tables = {
|
||||
User,
|
||||
Telegram,
|
||||
Discord
|
||||
]
|
||||
}
|
||||
|
||||
# Don't change this, it should automatically generate __all__
|
||||
__all__ = [table.__name__ for table in available_tables]
|
||||
|
|
|
@ -24,9 +24,6 @@ class Command:
|
|||
"""The syntax of the command, to be displayed when a :py:exc:`InvalidInputError` is raised,
|
||||
in the format ``(required_arg) [optional_arg]``."""
|
||||
|
||||
tables: typing.Set = set()
|
||||
"""A set of :mod:`royalnet.alchemy` tables that must exist for this command to work."""
|
||||
|
||||
def __init__(self, interface: CommandInterface):
|
||||
self.interface = interface
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from typing import Optional, TYPE_CHECKING, Awaitable, Any, Callable
|
||||
from typing import *
|
||||
from asyncio import AbstractEventLoop
|
||||
from .errors import UnsupportedError
|
||||
if TYPE_CHECKING:
|
||||
from .event import Event
|
||||
from .command import Command
|
||||
from ..alchemy import Alchemy
|
||||
from ..serf import Serf
|
||||
|
@ -36,8 +37,13 @@ class CommandInterface:
|
|||
"""A shortcut for :attr:`serf.loop`."""
|
||||
return self.serf.loop
|
||||
|
||||
def __init__(self):
|
||||
self.command: Optional[Command] = None # Will be bound after the command has been created
|
||||
def __init__(self, cfg: Dict[str, Any]):
|
||||
self.cfg: Dict[str, Any] = cfg
|
||||
"""The config section for the pack of the command."""
|
||||
|
||||
# Will be bound after the command/event has been created
|
||||
self.command: Optional[Command] = None
|
||||
self.event: Optional[Event] = None
|
||||
|
||||
async def call_herald_event(self, destination: str, event_name: str, **kwargs) -> dict:
|
||||
"""Call an event function on a different :class:`Serf`.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from .commandinterface import CommandInterface
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from serf import Serf
|
||||
|
@ -9,22 +10,20 @@ class Event:
|
|||
name = NotImplemented
|
||||
"""The event_name that will trigger this event."""
|
||||
|
||||
tables: set = set()
|
||||
"""A set of :mod:`royalnet.alchemy` tables that must exist for this event to work."""
|
||||
|
||||
def __init__(self, serf: "Serf"):
|
||||
def __init__(self, interface: CommandInterface):
|
||||
"""Bind the event to a :class:`~royalnet.serf.Serf`."""
|
||||
self.serf: "Serf" = serf
|
||||
self.interface: CommandInterface = interface
|
||||
"""The :class:`CommandInterface` available to this :class:`Event`."""
|
||||
|
||||
@property
|
||||
def alchemy(self):
|
||||
"""A shortcut for :attr:`.serf.alchemy`."""
|
||||
return self.serf.alchemy
|
||||
return self.interface.serf.alchemy
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
"""A shortcut for :attr:`.serf.loop`"""
|
||||
return self.serf.loop
|
||||
return self.interface.serf.loop
|
||||
|
||||
async def run(self, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -8,7 +8,10 @@ class Config:
|
|||
port: int,
|
||||
secret: str,
|
||||
secure: bool = False,
|
||||
path: str = "/"):
|
||||
path: str = "/",
|
||||
*,
|
||||
enabled: ... = ..., # Ignored, but useful to allow creating a config from the config dict
|
||||
):
|
||||
if ":" in name:
|
||||
raise ValueError("Herald names cannot contain colons (:)")
|
||||
self.name = name
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import warnings
|
||||
from typing import Type, Optional, List, Union, Dict
|
||||
from typing import *
|
||||
import royalnet.backpack as rb
|
||||
from royalnet.commands import *
|
||||
from royalnet.utils import asyncify
|
||||
from royalnet.serf import Serf
|
||||
|
@ -33,21 +34,25 @@ class DiscordSerf(Serf):
|
|||
"""A :class:`Serf` that connects to `Discord <https://discordapp.com/>`_ as a bot."""
|
||||
interface_name = "discord"
|
||||
|
||||
def __init__(self, *,
|
||||
token: str,
|
||||
alchemy_config: Optional[AlchemyConfig] = None,
|
||||
commands: List[Type[Command]] = None,
|
||||
events: List[Type[Event]] = None,
|
||||
herald_config: Optional[HeraldConfig] = None):
|
||||
_identity_table = rb.tables.Discord
|
||||
_identity_column = "discord_id"
|
||||
|
||||
def __init__(self,
|
||||
alchemy_cfg: Dict[str, Any],
|
||||
herald_cfg: Dict[str, Any],
|
||||
sentry_cfg: Dict[str, Any],
|
||||
packs_cfg: Dict[str, Any],
|
||||
serf_cfg: Dict[str, Any]):
|
||||
if discord is None:
|
||||
raise ImportError("'discord' extra is not installed")
|
||||
|
||||
super().__init__(alchemy_config=alchemy_config,
|
||||
commands=commands,
|
||||
events=events,
|
||||
herald_config=herald_config)
|
||||
super().__init__(alchemy_cfg=alchemy_cfg,
|
||||
herald_cfg=herald_cfg,
|
||||
sentry_cfg=sentry_cfg,
|
||||
packs_cfg=packs_cfg,
|
||||
serf_cfg=serf_cfg)
|
||||
|
||||
self.token = token
|
||||
self.token = serf_cfg["token"]
|
||||
"""The Discord bot token."""
|
||||
|
||||
self.Client = self.client_factory()
|
||||
|
@ -86,10 +91,10 @@ class DiscordSerf(Serf):
|
|||
|
||||
async def get_author(data, error_if_none=False):
|
||||
user: "discord.Member" = data.message.author
|
||||
query = data.session.query(self._master_table)
|
||||
for link in self._identity_chain:
|
||||
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)
|
||||
query = query.filter(self.identity_column == user.id)
|
||||
result = await asyncify(query.one_or_none)
|
||||
if result is None and error_if_none:
|
||||
raise CommandError("You must be registered to use this command.")
|
||||
|
|
|
@ -1,30 +1,17 @@
|
|||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from asyncio import Task, AbstractEventLoop, get_event_loop
|
||||
from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union, Set
|
||||
import importlib
|
||||
import asyncio as aio
|
||||
from typing import *
|
||||
|
||||
from sqlalchemy.schema import Table
|
||||
from royalnet import __version__ as version
|
||||
|
||||
from royalnet import __version__
|
||||
from royalnet.commands import *
|
||||
from .alchemyconfig import AlchemyConfig
|
||||
|
||||
try:
|
||||
from royalnet.alchemy import Alchemy, table_dfs
|
||||
except ImportError:
|
||||
Alchemy = None
|
||||
table_dfs = None
|
||||
|
||||
try:
|
||||
from royalnet.herald import Response, ResponseSuccess, Broadcast, ResponseFailure, Request, Link
|
||||
from royalnet.herald import Config as HeraldConfig
|
||||
except ImportError:
|
||||
Response = None
|
||||
ResponseSuccess = None
|
||||
Broadcast = None
|
||||
ResponseFailure = None
|
||||
Request = None
|
||||
Link = None
|
||||
HeraldConfig = None
|
||||
import royalnet.alchemy as ra
|
||||
import royalnet.backpack as rb
|
||||
import royalnet.herald as rh
|
||||
|
||||
try:
|
||||
import sentry_sdk
|
||||
|
@ -50,34 +37,91 @@ class Serf:
|
|||
Discord)."""
|
||||
interface_name = NotImplemented
|
||||
|
||||
def __init__(self, *,
|
||||
alchemy_config: Optional[AlchemyConfig] = None,
|
||||
commands: List[Type[Command]] = None,
|
||||
events: List[Type[Event]] = None,
|
||||
herald_config: Optional[HeraldConfig] = None,
|
||||
sentry_dsn: Optional[str] = None):
|
||||
self.alchemy: Optional[Alchemy] = None
|
||||
"""The :class:`Alchemy` object connecting this Serf to the database."""
|
||||
_master_table: type = rb.tables.User
|
||||
_identity_table: type = NotImplemented
|
||||
_identity_column: str = NotImplemented
|
||||
|
||||
self._master_table: Optional[Table] = None
|
||||
def __init__(self,
|
||||
alchemy_cfg: Dict[str, Any],
|
||||
herald_cfg: Dict[str, Any],
|
||||
sentry_cfg: Dict[str, Any],
|
||||
packs_cfg: Dict[str, Any],
|
||||
serf_cfg: Dict[str, Any]):
|
||||
|
||||
# Import packs
|
||||
pack_names = packs_cfg["active"]
|
||||
packs = {}
|
||||
for pack_name in pack_names:
|
||||
log.debug(f"Importing pack: {pack_name}")
|
||||
try:
|
||||
packs[pack_name] = importlib.import_module(pack_name)
|
||||
except ImportError as e:
|
||||
log.error(f"Error during the import of {pack_name}: {e}")
|
||||
# pack_commands = []
|
||||
# try:
|
||||
# pack_commands = pack.available_commands
|
||||
# except AttributeError:
|
||||
# log.warning(f"No commands in pack: {pack_name}")
|
||||
# else:
|
||||
# log.debug(f"Imported: {len(pack_commands)} commands")
|
||||
# commands = [*commands, *pack_commands]
|
||||
# pack_events = []
|
||||
# try:
|
||||
# pack_events = pack.available_events
|
||||
# except AttributeError:
|
||||
# log.warning(f"No events in pack: {pack_name}")
|
||||
# else:
|
||||
# log.debug(f"Imported: {len(pack_events)} events")
|
||||
# events = [*events, *pack_events]
|
||||
# pack_tables = []
|
||||
# try:
|
||||
# pack_tables = pack.available_events
|
||||
# except AttributeError:
|
||||
# log.warning(f"No tables in pack: {pack_name}")
|
||||
# else:
|
||||
# log.debug(f"Imported: {len(pack_tables)} tables")
|
||||
# tables = [*tables, *pack_tables]
|
||||
log.info(f"Packs: {len(packs)} imported")
|
||||
|
||||
self.alchemy: Optional[ra.Alchemy] = None
|
||||
"""The :class:`Alchemy` object connecting this Serf to a database."""
|
||||
|
||||
self.master_table: Optional[Table] = None
|
||||
"""The central table listing all users. It usually is :class:`User`."""
|
||||
|
||||
self._identity_table: Optional[Table] = None
|
||||
self.identity_table: Optional[Table] = None
|
||||
"""The identity table containing the interface data (such as the Telegram user data) and that is in a
|
||||
many-to-one relationship with the master table."""
|
||||
|
||||
# TODO: I'm not sure what this is either
|
||||
self._identity_column: Optional[str] = None
|
||||
self.identity_column: Optional[str] = None
|
||||
|
||||
if Alchemy is None:
|
||||
# Find all tables
|
||||
tables = set()
|
||||
for pack in packs.values():
|
||||
try:
|
||||
tables = tables.union(pack.available_tables)
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
||||
continue
|
||||
|
||||
if ra.Alchemy is None:
|
||||
log.info("Alchemy: not installed")
|
||||
elif alchemy_config is None:
|
||||
elif not alchemy_cfg["enabled"]:
|
||||
log.info("Alchemy: disabled")
|
||||
else:
|
||||
tables = self.find_tables(alchemy_config, commands)
|
||||
self.init_alchemy(alchemy_config, tables)
|
||||
self.init_alchemy(alchemy_cfg["database_url"], tables)
|
||||
log.info(f"Alchemy: {self.alchemy}")
|
||||
|
||||
self.herald: Optional[rh.Link] = None
|
||||
"""The :class:`Link` object connecting the Serf to the rest of the herald network."""
|
||||
|
||||
self.herald_task: Optional[aio.Task] = None
|
||||
"""A reference to the :class:`asyncio.Task` that runs the :class:`Link`."""
|
||||
|
||||
self.events: Dict[str, Event] = {}
|
||||
"""A dictionary containing all :class:`Event` that can be handled by this :class:`Serf`."""
|
||||
|
||||
self.Interface: Type[CommandInterface] = self.interface_factory()
|
||||
"""The :class:`CommandInterface` class of this Serf."""
|
||||
|
||||
|
@ -87,72 +131,59 @@ class Serf:
|
|||
self.commands: Dict[str, Command] = {}
|
||||
"""The :class:`dict` connecting each command name to its :class:`Command` object."""
|
||||
|
||||
if commands is None:
|
||||
commands = []
|
||||
self.register_commands(commands)
|
||||
log.info(f"Commands: total {len(self.commands)}")
|
||||
for pack_name in packs:
|
||||
pack = packs[pack_name]
|
||||
pack_cfg = packs_cfg.get(pack_name, default={})
|
||||
try:
|
||||
events = pack.available_events
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
||||
else:
|
||||
self.register_events(events, pack_cfg)
|
||||
try:
|
||||
commands = pack.available_commands
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_commands` attribute.")
|
||||
else:
|
||||
self.register_commands(commands, pack_cfg)
|
||||
log.info(f"Events: {len(self.commands)} events")
|
||||
log.info(f"Commands: {len(self.commands)} commands")
|
||||
|
||||
self.herald: Optional[Link] = None
|
||||
"""The :class:`Link` object connecting the Serf to the rest of the herald network."""
|
||||
|
||||
self.herald_task: Optional[Task] = None
|
||||
"""A reference to the :class:`asyncio.Task` that runs the :class:`Link`."""
|
||||
|
||||
self.events: Dict[str, Event] = {}
|
||||
"""A dictionary containing all :class:`Event` that can be handled by this :class:`Serf`."""
|
||||
|
||||
if Link is None:
|
||||
if rh.Link is None:
|
||||
log.info("Herald: not installed")
|
||||
elif herald_config is None:
|
||||
elif not herald_cfg["enabled"]:
|
||||
log.info("Herald: disabled")
|
||||
else:
|
||||
self.init_herald(herald_config, events)
|
||||
log.info(f"Herald: {len(self.events)} events bound")
|
||||
self.init_herald(herald_cfg)
|
||||
log.info(f"Herald: enabled")
|
||||
|
||||
self.loop: Optional[AbstractEventLoop] = None
|
||||
self.loop: Optional[aio.AbstractEventLoop] = None
|
||||
"""The event loop this Serf is running on."""
|
||||
|
||||
self.sentry_dsn: Optional[str] = sentry_dsn
|
||||
self.sentry_dsn: Optional[str] = sentry_cfg["dsn"] if sentry_cfg["enabled"] else None
|
||||
"""The Sentry DSN / Token. If :const:`None`, Sentry is disabled."""
|
||||
|
||||
@staticmethod
|
||||
def find_tables(alchemy_config: AlchemyConfig, commands: List[Type[Command]]) -> Set[type]:
|
||||
"""Find the :class:`Table`s required by the Serf.
|
||||
|
||||
Warning:
|
||||
This function will return a wrong result if there are tables between the master table and the identity table
|
||||
that aren't included by a command.
|
||||
|
||||
Returns:
|
||||
A :class:`list` of :class:`Table`s."""
|
||||
# FIXME: breaks if there are nonincluded tables between master and identity.
|
||||
tables = {alchemy_config.master_table, alchemy_config.identity_table}
|
||||
for command in commands:
|
||||
tables = tables.union(command.tables)
|
||||
return tables
|
||||
|
||||
def init_alchemy(self, alchemy_config: AlchemyConfig, tables: Set[type]) -> None:
|
||||
def init_alchemy(self, alchemy_cfg: Dict[str, Any], tables: Set[type]) -> None:
|
||||
"""Create and initialize the :class:`Alchemy` with the required tables, and find the link between the master
|
||||
table and the identity table."""
|
||||
self.alchemy = Alchemy(alchemy_config.database_url, tables)
|
||||
self._master_table = self.alchemy.get(alchemy_config.master_table)
|
||||
self._identity_table = self.alchemy.get(alchemy_config.identity_table)
|
||||
self.alchemy = ra.Alchemy(alchemy_cfg["database_url"], tables)
|
||||
self.master_table = self.alchemy.get(self._master_table)
|
||||
self.identity_table = self.alchemy.get(self._identity_table)
|
||||
# This is fine, as Pycharm doesn't know that identity_table is a class and not an object
|
||||
# noinspection PyArgumentList
|
||||
self._identity_column = self._identity_table.__getattribute__(self._identity_table,
|
||||
alchemy_config.identity_column)
|
||||
self.identity_column = self.identity_table.__getattribute__(self.identity_table, self._identity_column)
|
||||
|
||||
@property
|
||||
def _identity_chain(self) -> tuple:
|
||||
def identity_chain(self) -> tuple:
|
||||
"""Find a relationship path starting from the master table and ending at the identity table, and return it."""
|
||||
return table_dfs(self._master_table, self._identity_table)
|
||||
return ra.table_dfs(self.master_table, self.identity_table)
|
||||
|
||||
def interface_factory(self) -> Type[CommandInterface]:
|
||||
"""Create the :class:`CommandInterface` class for the Serf."""
|
||||
|
||||
# noinspection PyMethodParameters
|
||||
class GenericInterface(CommandInterface):
|
||||
alchemy: Alchemy = self.alchemy
|
||||
alchemy: ra.Alchemy = self.alchemy
|
||||
serf: "Serf" = self
|
||||
|
||||
async def call_herald_event(ci, destination: str, event_name: str, **kwargs) -> Dict:
|
||||
|
@ -160,9 +191,9 @@ class Serf:
|
|||
:class:`royalherald.Response`."""
|
||||
if self.herald is None:
|
||||
raise UnsupportedError("`royalherald` is not enabled on this bot.")
|
||||
request: Request = Request(handler=event_name, data=kwargs)
|
||||
response: Response = await self.herald.request(destination=destination, request=request)
|
||||
if isinstance(response, ResponseFailure):
|
||||
request: rh.Request = rh.Request(handler=event_name, data=kwargs)
|
||||
response: rh.Response = await self.herald.request(destination=destination, request=request)
|
||||
if isinstance(response, rh.ResponseFailure):
|
||||
if response.name == "no_event":
|
||||
raise CommandError(f"There is no event named {event_name} in {destination}.")
|
||||
elif response.name == "exception_in_event":
|
||||
|
@ -182,7 +213,7 @@ class Serf:
|
|||
else:
|
||||
raise TypeError(f"Herald action call returned invalid error:\n"
|
||||
f"[p]{response}[/p]")
|
||||
elif isinstance(response, ResponseSuccess):
|
||||
elif isinstance(response, rh.ResponseSuccess):
|
||||
return response.data
|
||||
else:
|
||||
raise TypeError(f"Other Herald Link returned unknown response:\n"
|
||||
|
@ -194,35 +225,29 @@ class Serf:
|
|||
"""Create the :class:`CommandData` for the Serf."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def register_commands(self, commands: List[Type[Command]]) -> None:
|
||||
"""Initialize and register all commands passed as argument.
|
||||
|
||||
If called again during the execution of the bot, all current commands will be replaced with the new ones.
|
||||
|
||||
Warning:
|
||||
Hot-replacing commands was never tested and probably doesn't work."""
|
||||
log.info(f"Registering {len(commands)} commands...")
|
||||
def register_commands(self, commands: List[Type[Command]], pack_cfg: Dict[str, Any]) -> None:
|
||||
"""Initialize and register all commands passed as argument."""
|
||||
# Instantiate the Commands
|
||||
for SelectedCommand in commands:
|
||||
# Warn if the command would be overriding something
|
||||
if f"{self.Interface.prefix}{SelectedCommand.name}" in self.commands:
|
||||
log.warning(f"Overriding (already defined): "
|
||||
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
|
||||
else:
|
||||
log.debug(f"Registering: "
|
||||
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
|
||||
# Create a new interface
|
||||
interface = self.Interface()
|
||||
interface = self.Interface(cfg=pack_cfg)
|
||||
# Try to instantiate the command
|
||||
try:
|
||||
command = SelectedCommand(interface)
|
||||
except Exception as e:
|
||||
log.error(f"Skipping: "
|
||||
f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.")
|
||||
sentry_sdk.capture_exception(e)
|
||||
self.sentry_exc(e)
|
||||
continue
|
||||
# Link the interface to the command
|
||||
interface.command = command
|
||||
# Warn if the command would be overriding something
|
||||
if f"{self.Interface.prefix}{SelectedCommand.name}" in self.commands:
|
||||
log.info(f"Overriding (already defined): "
|
||||
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
|
||||
else:
|
||||
log.debug(f"Registering: "
|
||||
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
|
||||
# Register the command in the commands dict
|
||||
self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command
|
||||
# Register aliases, but don't override anything
|
||||
|
@ -232,52 +257,65 @@ class Serf:
|
|||
self.commands[f"{interface.prefix}{alias}"] = \
|
||||
self.commands[f"{interface.prefix}{SelectedCommand.name}"]
|
||||
else:
|
||||
log.warning(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
|
||||
log.warning(
|
||||
f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
|
||||
|
||||
def init_herald(self, config: HeraldConfig, events: List[Type[Event]]):
|
||||
"""Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`."""
|
||||
self.herald: Link = Link(config, self.network_handler)
|
||||
log.debug(f"Binding events...")
|
||||
def init_herald(self, herald_cfg: Dict[str, Any]):
|
||||
"""Create a :class:`Link` and bind :class:`Event`."""
|
||||
herald_cfg["name"] = self.interface_name
|
||||
self.herald: rh.Link = rh.Link(rh.Config(**herald_cfg), self.network_handler)
|
||||
|
||||
def register_events(self, events: List[Type[Event]], pack_cfg: Dict[str, Any]):
|
||||
for SelectedEvent in events:
|
||||
log.debug(f"Binding event: {SelectedEvent.name}.")
|
||||
self.events[SelectedEvent.name] = SelectedEvent(self)
|
||||
# Create a new interface
|
||||
interface = self.Interface(cfg=pack_cfg)
|
||||
# Initialize the event
|
||||
try:
|
||||
event = SelectedEvent(interface)
|
||||
except Exception as e:
|
||||
log.error(f"Skipping: "
|
||||
f"{SelectedEvent.__qualname__} - {e.__class__.__qualname__} in the initialization.")
|
||||
self.sentry_exc(e)
|
||||
continue
|
||||
# Register the event
|
||||
if SelectedEvent.name in self.events:
|
||||
log.warning(f"Overriding (already defined): {SelectedEvent.__qualname__} -> {SelectedEvent.name}")
|
||||
else:
|
||||
log.debug(f"Registering: {SelectedEvent.__qualname__} -> {SelectedEvent.name}")
|
||||
self.events[SelectedEvent.name] = event
|
||||
|
||||
async def network_handler(self, message: Union[Request, Broadcast]) -> Response:
|
||||
async def network_handler(self, message: Union[rh.Request, rh.Broadcast]) -> rh.Response:
|
||||
try:
|
||||
event: Event = self.events[message.handler]
|
||||
except KeyError:
|
||||
log.warning(f"No event for '{message.handler}'")
|
||||
return ResponseFailure("no_event", f"This serf does not have any event for {message.handler}.")
|
||||
return rh.ResponseFailure("no_event", f"This serf does not have any event for {message.handler}.")
|
||||
log.debug(f"Event called: {event.name}")
|
||||
if isinstance(message, Request):
|
||||
if isinstance(message, rh.Request):
|
||||
try:
|
||||
response_data = await event.run(**message.data)
|
||||
return ResponseSuccess(data=response_data)
|
||||
return rh.ResponseSuccess(data=response_data)
|
||||
except Exception as e:
|
||||
self.sentry_exc(e)
|
||||
return ResponseFailure("exception_in_event",
|
||||
f"An exception was raised in the event for '{message.handler}'.",
|
||||
extra_info={
|
||||
"type": e.__class__.__qualname__,
|
||||
"message": str(e)
|
||||
})
|
||||
elif isinstance(message, Broadcast):
|
||||
return rh.ResponseFailure("exception_in_event",
|
||||
f"An exception was raised in the event for '{message.handler}'.",
|
||||
extra_info={
|
||||
"type": e.__class__.__qualname__,
|
||||
"message": str(e)
|
||||
})
|
||||
elif isinstance(message, rh.Broadcast):
|
||||
await event.run(**message.data)
|
||||
|
||||
@staticmethod
|
||||
def init_sentry(dsn):
|
||||
# noinspection PyUnreachableCode
|
||||
if __debug__:
|
||||
release = f"royalnet"
|
||||
else:
|
||||
release = f"royalnet=={version}"
|
||||
log.debug("Initializing Sentry...")
|
||||
release = f"royalnet@{__version__}"
|
||||
sentry_sdk.init(dsn,
|
||||
integrations=[AioHttpIntegration(),
|
||||
SqlalchemyIntegration(),
|
||||
LoggingIntegration(event_level=None)],
|
||||
release=release)
|
||||
log.info(f"Sentry: enabled (release {release})")
|
||||
log.info(f"Sentry: {release}")
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
@staticmethod
|
||||
|
@ -343,13 +381,12 @@ class Serf:
|
|||
|
||||
if sentry_sdk is None:
|
||||
log.info("Sentry: not installed")
|
||||
elif serf.sentry_dsn is None:
|
||||
log.info("Sentry: disabled")
|
||||
else:
|
||||
if serf.sentry_dsn is None:
|
||||
log.info("Sentry: disabled")
|
||||
else:
|
||||
serf.init_sentry(serf.sentry_dsn)
|
||||
serf.init_sentry(serf.sentry_dsn)
|
||||
|
||||
serf.loop = get_event_loop()
|
||||
serf.loop = aio.get_event_loop()
|
||||
try:
|
||||
serf.loop.run_until_complete(serf.run())
|
||||
except Exception as e:
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import logging
|
||||
import asyncio
|
||||
from typing import Type, Optional, List, Callable
|
||||
import asyncio as aio
|
||||
from typing import *
|
||||
from royalnet.commands import *
|
||||
from royalnet.utils import asyncify
|
||||
import royalnet.backpack as rb
|
||||
from .escape import escape
|
||||
from ..serf import Serf
|
||||
|
||||
|
@ -17,15 +18,10 @@ except ImportError:
|
|||
|
||||
try:
|
||||
from sqlalchemy.orm.session import Session
|
||||
from ..alchemyconfig import AlchemyConfig
|
||||
except ImportError:
|
||||
Session = None
|
||||
AlchemyConfig = None
|
||||
|
||||
try:
|
||||
from royalnet.herald import Config as HeraldConfig
|
||||
except ImportError:
|
||||
HeraldConfig = None
|
||||
import royalnet.herald as rh
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,25 +30,27 @@ class TelegramSerf(Serf):
|
|||
"""A Serf that connects to `Telegram <https://telegram.org/>`_ as a bot."""
|
||||
interface_name = "telegram"
|
||||
|
||||
def __init__(self, *,
|
||||
token: str,
|
||||
pool_size: int = 8,
|
||||
read_timeout: int = 60,
|
||||
alchemy_config: Optional[AlchemyConfig] = None,
|
||||
commands: List[Type[Command]] = None,
|
||||
events: List[Type[Event]] = None,
|
||||
herald_config: Optional[HeraldConfig] = None,
|
||||
sentry_dsn: Optional[str] = None):
|
||||
_identity_table = rb.tables.Telegram
|
||||
_identity_column = "tg_id"
|
||||
|
||||
def __init__(self,
|
||||
alchemy_cfg: Dict[str, Any],
|
||||
herald_cfg: Dict[str, Any],
|
||||
sentry_cfg: Dict[str, Any],
|
||||
packs_cfg: Dict[str, Any],
|
||||
serf_cfg: Dict[str, Any]):
|
||||
if telegram is None:
|
||||
raise ImportError("'telegram' extra is not installed")
|
||||
|
||||
super().__init__(alchemy_config=alchemy_config,
|
||||
commands=commands,
|
||||
events=events,
|
||||
herald_config=herald_config,
|
||||
sentry_dsn=sentry_dsn)
|
||||
super().__init__(alchemy_cfg=alchemy_cfg,
|
||||
herald_cfg=herald_cfg,
|
||||
sentry_cfg=sentry_cfg,
|
||||
packs_cfg=packs_cfg,
|
||||
serf_cfg=serf_cfg)
|
||||
|
||||
self.client = telegram.Bot(token, request=TRequest(pool_size, read_timeout=read_timeout))
|
||||
self.client = telegram.Bot(serf_cfg["token"],
|
||||
request=TRequest(serf_cfg["pool_size"],
|
||||
read_timeout=serf_cfg["read_timeout"]))
|
||||
"""The :class:`telegram.Bot` instance that will be used from the Serf."""
|
||||
|
||||
self.update_offset: int = -100
|
||||
|
@ -77,11 +75,11 @@ class TelegramSerf(Serf):
|
|||
break
|
||||
except telegram.error.RetryAfter as error:
|
||||
log.warning(f"Rate limited during {f.__qualname__} (retrying in 15s): {error}")
|
||||
await asyncio.sleep(15)
|
||||
await aio.sleep(15)
|
||||
continue
|
||||
except urllib3.exceptions.HTTPError as error:
|
||||
log.warning(f"urllib3 HTTPError during {f.__qualname__} (retrying in 15s): {error}")
|
||||
await asyncio.sleep(15)
|
||||
await aio.sleep(15)
|
||||
continue
|
||||
except Exception as error:
|
||||
log.error(f"{error.__class__.__qualname__} during {f} (skipping): {error}")
|
||||
|
@ -106,7 +104,7 @@ class TelegramSerf(Serf):
|
|||
def __init__(data,
|
||||
interface: CommandInterface,
|
||||
session,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
loop: aio.AbstractEventLoop,
|
||||
update: telegram.Update):
|
||||
super().__init__(interface=interface, session=session, loop=loop)
|
||||
data.update = update
|
||||
|
@ -128,10 +126,10 @@ class TelegramSerf(Serf):
|
|||
if error_if_none:
|
||||
raise CommandError("No command caller for this message")
|
||||
return None
|
||||
query = data.session.query(self._master_table)
|
||||
for link in self._identity_chain:
|
||||
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)
|
||||
query = query.filter(self.identity_column == user.id)
|
||||
result = await asyncify(query.one_or_none)
|
||||
if result is None and error_if_none:
|
||||
raise CommandError("Command caller is not registered")
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# ROYALNET CONFIGURATION FILE
|
||||
|
||||
[Herald]
|
||||
# Please note that either Herald.Local or Herald.Remote should be enabled!
|
||||
# Enable the herald module, allowing different parts of Royalnet to talk to each other
|
||||
# Requires the `herald` extra to be installed
|
||||
enabled = true
|
||||
|
||||
[Herald.Local]
|
||||
# Run locally a Herald web server (websocket) that other parts of Royalnet can connect to
|
||||
# Requires the `herald` extra to be installed
|
||||
enabled = true
|
||||
# The address of the network interface on which the Herald server should listen for connections
|
||||
# If 0.0.0.0, listen for connections on all interfaces
|
||||
|
@ -91,7 +92,7 @@ log_level = "info"
|
|||
# Requires the `sentry` extra to be installed
|
||||
enabled = false
|
||||
# Get one at https://sentry.io/settings/YOUR-ORG/projects/YOUR-PROJECT/keys/
|
||||
sentry_dsn = "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/1111111"
|
||||
dsn = "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/1111111"
|
||||
|
||||
[Packs]
|
||||
# The Python package name of the Packs you want to be usable in Royalnet
|
||||
|
@ -104,10 +105,9 @@ active = [
|
|||
|
||||
# Configuration settings for specific packs
|
||||
# Be aware that packs have access to the whole config file
|
||||
[Pack]
|
||||
[Pack.Backpack]
|
||||
[Packs."royalnet.backpack"]
|
||||
# Enable exception debug commands and stars
|
||||
exc_debug = false
|
||||
|
||||
# Add your packs config here!
|
||||
# [Pack.YourPackName]
|
||||
# [Packs."yourpack"]
|
||||
|
|
Loading…
Reference in a new issue