1
Fork 0
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:
Steffo 2019-11-25 13:31:28 +01:00
parent f63520f385
commit a2f2aa6855
11 changed files with 257 additions and 198 deletions

11
poetry.lock generated
View file

@ -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 = [

View file

@ -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>",

View file

@ -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]

View file

@ -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

View file

@ -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`.

View file

@ -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()

View file

@ -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

View file

@ -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.")

View file

@ -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:

View file

@ -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")

View file

@ -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"]