diff --git a/royalnet/backpack/commands/summon.py b/royalnet/backpack/commands/summon.py index fa8d3767..4d3868cf 100644 --- a/royalnet/backpack/commands/summon.py +++ b/royalnet/backpack/commands/summon.py @@ -21,13 +21,22 @@ class SummonCommand(Command): syntax = "[channelname]" async def run(self, args: CommandArgs, data: CommandData) -> None: + if self.interface.name == "discord": + msg: Optional["discord.Message"] = data.message + member: Optional["discord.Member"] = msg.author + guild: Optional["discord.Guild"] = msg.guild + else: + member = None + guild = None try: - await self.interface.call_herald_action("discord", "discordvoice", { + await self.interface.call_herald_event("discord", "discordvoice", { "operation": "summon", "data": { - "channel_name": args.joined() + "channel_name": args.joined(), + "member_id": member.id if member is not None else None, + "guild_id": guild.id if member is not None else None, } }) except Exception as e: breakpoint() - await data.reply(f"✅ Connesso alla chat vocale.") \ No newline at end of file + await data.reply(f"✅ Connesso alla chat vocale.") diff --git a/royalnet/backpack/events/discordvoice.py b/royalnet/backpack/events/discordvoice.py index 67689cf0..cc535216 100644 --- a/royalnet/backpack/events/discordvoice.py +++ b/royalnet/backpack/events/discordvoice.py @@ -5,6 +5,7 @@ from royalnet.serf import Serf from royalnet.serf.discord import DiscordSerf from royalnet.bard import DiscordBard from royalnet.bard.implementations import * +import weakref try: import discord @@ -17,18 +18,18 @@ class DiscordvoiceEvent(Event): def __init__(self, serf: Serf): super().__init__(serf) - self.bards: Dict["discord.Guild", DiscordBard] = {} + self.bards: weakref.WeakValueDictionary = weakref.WeakValueDictionary() - async def run(self, data: dict): + async def run(self, + operation: str, + data: dict): if not isinstance(self.serf, DiscordSerf): raise ValueError("`discordvoice` event cannot run on other serfs.") - operation = data["operation"] - if operation == "summon": - channel_name: str = data["data"]["channel_name"] - member_id: int = data["data"].get("member_id") - guild_id: int = data["data"].get("guild_id") + channel_name: str = data["channel_name"] + member_id: int = data.get("member_id") + guild_id: int = data.get("guild_id") client: discord.Client = self.serf.client # Get the guild, if it exists @@ -105,9 +106,13 @@ class DiscordvoiceEvent(Event): raise CommandError("The bot is already connected in another channel.\n" " Please disconnect it before resummoning!") + # Create a new bard, if it doesn't already exist + # TODO: does this work? are the voice clients correctly disposed of? + self.bards[channel.guild] = DBQueue() return { "connected": True } + # TODO: play, skip, playmode, remove, something else? else: raise ValueError(f"Invalid operation received: {operation}") diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py index c2a41e12..4f6519d8 100644 --- a/royalnet/commands/commandinterface.py +++ b/royalnet/commands/commandinterface.py @@ -27,28 +27,18 @@ class CommandInterface: A reference to a :class:`~royalnet.serf.telegram.TelegramSerf`.""" @property - def alchemy(self): + def alchemy(self) -> "Alchemy": """A shortcut for :attr:`serf.alchemy`.""" return self.serf.alchemy @property - def loop(self): + def loop(self) -> AbstractEventLoop: """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 register_herald_action(self, - event_name: str, - coroutine: Callable[[Any], Awaitable[dict]]): + async def call_herald_event(self, destination: str, event_name: str, args: dict) -> dict: # TODO: document this - raise UnsupportedError(f"{self.register_herald_action.__name__} is not supported on this platform") - - def unregister_herald_action(self, event_name: str): - # TODO: document this - raise UnsupportedError(f"{self.unregister_herald_action.__name__} is not supported on this platform") - - async def call_herald_action(self, destination: str, event_name: str, args: dict) -> dict: - # TODO: document this - raise UnsupportedError(f"{self.call_herald_action.__name__} is not supported on this platform") + raise UnsupportedError(f"{self.call_herald_event.__name__} is not supported on this platform") diff --git a/royalnet/commands/event.py b/royalnet/commands/event.py index cb7cf635..a923f612 100644 --- a/royalnet/commands/event.py +++ b/royalnet/commands/event.py @@ -12,9 +12,9 @@ class 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, serf: "Serf"): """Bind the event to a :class:`~royalnet.serf.Serf`.""" - self.serf: Serf = serf + self.serf: "Serf" = serf @property def alchemy(self): @@ -26,5 +26,5 @@ class Event: """A shortcut for :attr:`.serf.loop`""" return self.serf.loop - async def run(self, data: dict): + async def run(self, **kwargs): raise NotImplementedError() diff --git a/royalnet/serf/discord/discordserf.py b/royalnet/serf/discord/discordserf.py index 4a9891a5..8e87712f 100644 --- a/royalnet/serf/discord/discordserf.py +++ b/royalnet/serf/discord/discordserf.py @@ -33,14 +33,14 @@ class DiscordSerf(Serf): def __init__(self, *, alchemy_config: Optional[AlchemyConfig] = None, commands: List[Type[Command]] = None, - network_config: Optional[HeraldConfig] = None, + herald_config: Optional[HeraldConfig] = None, secrets_name: str = "__default__"): if discord is None: raise ImportError("'discord' extra is not installed") super().__init__(alchemy_config=alchemy_config, commands=commands, - network_config=network_config, + herald_config=herald_config, secrets_name=secrets_name) self.Client = self.client_factory() diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index 02b34db1..ac1a4ba6 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -47,7 +47,8 @@ class Serf: def __init__(self, *, alchemy_config: Optional[AlchemyConfig] = None, commands: List[Type[Command]] = None, - network_config: Optional[HeraldConfig] = None, + events: List[Type[Event]] = None, + herald_config: Optional[HeraldConfig] = None, secrets_name: str = "__default__"): self.secrets_name = secrets_name @@ -87,22 +88,21 @@ class Serf: self.register_commands(commands) log.info(f"Commands: total {len(self.commands)}") - self.herald_handlers: Dict[str, Callable[["Serf", Any], Awaitable[Optional[dict]]]] = {} - """A :class:`dict` linking :class:`Request` event names to coroutines returning a :class:`dict` that will be - sent as :class:`Response` to the event.""" - 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: log.info("Herald: not installed") - elif network_config is None: + elif herald_config is None: log.info("Herald: disabled") else: - self.init_network(network_config) + self.init_herald(herald_config, events) log.info(f"Herald: {self.herald}") self.loop: Optional[AbstractEventLoop] = None @@ -148,22 +148,7 @@ class Serf: alchemy: Alchemy = self.alchemy serf: "Serf" = self - def register_herald_action(ci, - event_name: str, - coroutine: Callable[[Any], Awaitable[Dict]]) -> None: - """Allow a coroutine to be called when a :class:`royalherald.Request` is received.""" - if self.herald is None: - raise UnsupportedError("`royalherald` is not enabled on this bot.") - self.herald_handlers[event_name] = coroutine - - def unregister_herald_action(ci, event_name: str): - """Disable a previously registered coroutine from being called on reception of a - :class:`royalherald.Request`.""" - if self.herald is None: - raise UnsupportedError("`royalherald` is not enabled on this bot.") - del self.herald_handlers[event_name] - - async def call_herald_action(ci, destination: str, event_name: str, args: Dict) -> Dict: + async def call_herald_event(ci, destination: str, event_name: str, args: Dict) -> Dict: """Send a :class:`royalherald.Request` to a specific destination, and wait for a :class:`royalherald.Response`.""" if self.herald is None: @@ -228,34 +213,36 @@ class Serf: else: log.warning(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") - def init_network(self, config: HeraldConfig): + def init_herald(self, config: HeraldConfig, events: List[Type[Event]]): """Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`.""" - log.debug(f"Initializing herald...") self.herald: Link = Link(config, self.network_handler) + log.debug(f"Binding events...") + for SelectedEvent in events: + log.debug(f"Binding event: {SelectedEvent.name}.") + self.events[SelectedEvent.name] = SelectedEvent(self) async def network_handler(self, message: Union[Request, Broadcast]) -> Response: try: - herald_handler = self.herald_handlers[message.handler] + event: Event = self.events[message.handler] except KeyError: - log.warning(f"Missing network_handler for {message.handler}") - return ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.") - else: - log.debug(f"Using {herald_handler} as handler for {message.handler}") + log.warning(f"No event for '{message.handler}'") + return 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): try: - response_data = await herald_handler(self, **message.data) + response_data = await event.run(**message.data) return ResponseSuccess(data=response_data) except Exception as e: sentry_sdk.capture_exception(e) - log.error(f"Exception {e} in {herald_handler}") - return ResponseFailure("exception_in_handler", - f"An exception was raised in {herald_handler} for {message.handler}.", + log.error(f"Event error: {e.__class__.__qualname__} in {event.name}") + return ResponseFailure("exception_in_event", + f"An exception was raised in the event for '{message.handler}'.", extra_info={ - "type": e.__class__.__name__, + "type": e.__class__.__qualname__, "message": str(e) }) elif isinstance(message, Broadcast): - await herald_handler(self, **message.data) + await event.run(**message.data) @staticmethod def init_sentry(dsn): diff --git a/royalnet/serf/telegram/telegramserf.py b/royalnet/serf/telegram/telegramserf.py index b57cfac1..4ebee45a 100644 --- a/royalnet/serf/telegram/telegramserf.py +++ b/royalnet/serf/telegram/telegramserf.py @@ -38,14 +38,14 @@ class TelegramSerf(Serf): def __init__(self, *, alchemy_config: Optional[AlchemyConfig] = None, commands: List[Type[Command]] = None, - network_config: Optional[HeraldConfig] = None, + herald_config: Optional[HeraldConfig] = None, secrets_name: str = "__default__"): if telegram is None: raise ImportError("'telegram' extra is not installed") super().__init__(alchemy_config=alchemy_config, commands=commands, - network_config=network_config, + herald_config=herald_config, secrets_name=secrets_name) self.client = telegram.Bot(self.get_secret("telegram"), request=TRequest(5, read_timeout=30))