diff --git a/royalnet/__main__.py b/royalnet/__main__.py index e6660e4c..047370b1 100644 --- a/royalnet/__main__.py +++ b/royalnet/__main__.py @@ -47,8 +47,9 @@ def run(telegram: typing.Optional[bool], royalnet_log: Logger = getLogger("royalnet") royalnet_log.setLevel(log_level) stream_handler = StreamHandler() - stream_handler.formatter = Formatter("{asctime}\t| {processName}\t| {levelname}\t| {message}", style="{") + stream_handler.formatter = Formatter("{asctime}\t| {processName}\t| {levelname}\t| {name}\t| {message}", style="{") royalnet_log.addHandler(stream_handler) + royalnet_log.debug("Logging: ready") def get_secret(username: str): return keyring.get_password(f"Royalnet/{secrets_name}", username) @@ -141,7 +142,8 @@ def run(telegram: typing.Optional[bool], 'commands': enabled_commands, 'events': enabled_events, 'herald_config': herald_config.copy(name="telegram"), - 'secrets_name': secrets_name + 'secrets_name': secrets_name, + 'log_level': log_level, } telegram_process = multiprocessing.Process(name="Telegram Serf", target=r.serf.telegram.TelegramSerf.run_process, @@ -163,7 +165,8 @@ def run(telegram: typing.Optional[bool], 'commands': enabled_commands, 'events': enabled_events, 'herald_config': herald_config.copy(name="discord"), - 'secrets_name': secrets_name + 'secrets_name': secrets_name, + 'log_level': log_level, } discord_process = multiprocessing.Process(name="Discord Serf", target=r.serf.discord.DiscordSerf.run_process, @@ -181,6 +184,7 @@ def run(telegram: typing.Optional[bool], 'database_uri': alchemy_url, 'page_stars': enabled_page_stars, 'exc_stars': enabled_exception_stars, + 'log_level': log_level, } constellation_process = multiprocessing.Process(name="Constellation", target=r.constellation.Constellation.run_process, diff --git a/royalnet/backpack/commands/summon.py b/royalnet/backpack/commands/summon.py index a99b5228..4803b4c6 100644 --- a/royalnet/backpack/commands/summon.py +++ b/royalnet/backpack/commands/summon.py @@ -28,9 +28,11 @@ class SummonCommand(Command): else: member = None guild = None - try: - # TODO: do something! - pass - except Exception as e: - breakpoint() - await data.reply(f"✅ Connesso alla chat vocale.") + name = args.joined() + response: dict = await self.interface.call_herald_event("discord", "summon", { + "channel_name": name, + "guild_id": guild.id if guild is not None else None, + "user_id": member.id if member is not None else None, + }) + await data.reply(f"✅ Connected to [b]#{response['channel']['name']}[/b]" + f" in [i]{response['channel']['guild']['name']}[/i]!") diff --git a/royalnet/backpack/events/__init__.py b/royalnet/backpack/events/__init__.py index 0c59d789..dde03ebb 100644 --- a/royalnet/backpack/events/__init__.py +++ b/royalnet/backpack/events/__init__.py @@ -1,9 +1,9 @@ # Imports go here! - +from .summon import SummonEvent # Enter the commands of your Pack here! available_events = [ - + SummonEvent, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/events/summon.py b/royalnet/backpack/events/summon.py new file mode 100644 index 00000000..6b313968 --- /dev/null +++ b/royalnet/backpack/events/summon.py @@ -0,0 +1,59 @@ +from typing import Optional +from royalnet.commands import * +from royalnet.serf.discord import DiscordSerf + +try: + import discord +except ImportError: + discord = None + + +class SummonEvent(Event): + name = "summon" + + async def run(self, *, channel_name: str, guild_id: Optional[int], user_id: Optional[int], **kwargs): + if not isinstance(self.serf, DiscordSerf): + raise UnsupportedError("Summon can't be called on interfaces other than Discord.") + if discord is None: + raise UnsupportedError("'discord' extra is not installed.") + # Find the guild + if guild_id is not None: + guild: Optional["discord.Guild"] = self.serf.client.get_guild(guild_id) + else: + guild = None + # Find the member + if user_id is not None and guild is not None: + member = guild.get_member(user_id=user_id) + else: + member = None + # Find accessible_to + accessible_to = [self.serf.client.user] + if member is not None: + accessible_to.append(member) + # Find the channel + channel: Optional["discord.VoiceChannel"] = self.serf.find_channel(channel_type=discord.VoiceChannel, + name=channel_name, + guild=guild, + accessible_to=accessible_to, + required_permissions=["connect", "speak"]) + if channel is None: + raise InvalidInputError("No channels found with the specified name.") + # Connect to the channel + await self.serf.voice_connect(channel) + # Find the created bard + bard = self.serf.bards[channel.guild] + bard_peek = await bard.peek() + # Reply to the request + return { + "channel": { + "id": channel.id, + "name": channel.name, + "guild": { + "name": channel.guild.name, + }, + }, + "bard": { + "type": bard.__class__.__qualname__, + "peek": bard_peek, + } + } diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py index a3cff73a..8e47a1a1 100644 --- a/royalnet/constellation/constellation.py +++ b/royalnet/constellation/constellation.py @@ -116,6 +116,7 @@ class Constellation: database_uri: str, page_stars: typing.List[typing.Type[PageStar]] = None, exc_stars: typing.List[typing.Type[ExceptionStar]] = None, + log_level: str = "WARNING", *, debug: bool = __debug__,): """Blockingly create and run the Constellation. @@ -131,6 +132,16 @@ class Constellation: exc_stars=exc_stars, debug=debug) + # Initialize logging, as Windows doesn't have fork + royalnet_log: logging.Logger = logging.getLogger("royalnet") + royalnet_log.setLevel(log_level) + stream_handler = logging.StreamHandler() + stream_handler.formatter = logging.Formatter("{asctime}\t| {processName}\t| {levelname}\t| {name}\t| {message}", + style="{") + if len(royalnet_log.handlers) < 1: + royalnet_log.addHandler(stream_handler) + royalnet_log.debug("Logging: ready") + # Initialize Sentry on the process if sentry_sdk is None: log.info("Sentry: not installed") diff --git a/royalnet/serf/discord/discordserf.py b/royalnet/serf/discord/discordserf.py index 51c840c6..2de5344c 100644 --- a/royalnet/serf/discord/discordserf.py +++ b/royalnet/serf/discord/discordserf.py @@ -160,12 +160,12 @@ class DiscordSerf(Serf): await self.client.login(token) await self.client.connect() - async def find_channel(self, - channel_type: Optional[Type["discord.abc.GuildChannel"]] = None, - name: Optional[str] = None, - guild: Optional["discord.Guild"] = None, - accessible_to: List["discord.User"] = None, - required_permissions: List[str] = None) -> Optional["discord.abc.GuildChannel"]: + def find_channel(self, + channel_type: Optional[Type["discord.abc.GuildChannel"]] = None, + name: Optional[str] = None, + guild: Optional["discord.Guild"] = None, + accessible_to: List["discord.User"] = None, + required_permissions: List[str] = None) -> Optional["discord.abc.GuildChannel"]: """Find the best channel matching all requests. In case multiple channels match all requests, return the one with the most members connected. @@ -201,11 +201,11 @@ class DiscordSerf(Serf): pass ch_guild: "discord.Guild" = ch.guild - if ch.guild == ch_guild: + if ch.guild != ch_guild: continue for user in accessible_to: - member: "discord.Member" = guild.get_member(user.id) + member: "discord.Member" = ch.guild.get_member(user.id) if member is None: continue permissions: "discord.Permissions" = ch.permissions_for(member) diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index b7f7c392..1b93e64c 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -156,11 +156,22 @@ class Serf: request: Request = Request(handler=event_name, data=args) response: Response = await self.herald.request(destination=destination, request=request) if isinstance(response, ResponseFailure): + # TODO: pretty sure there's a better way to do this if response.extra_info["type"] == "CommandError": raise CommandError(response.extra_info["message"]) - # TODO: change exception type - raise Exception(f"Herald action call failed:\n" - f"[p]{response}[/p]") + elif response.extra_info["type"] == "UserError": + raise UserError(response.extra_info["message"]) + elif response.extra_info["type"] == "InvalidInputError": + raise InvalidInputError(response.extra_info["message"]) + elif response.extra_info["type"] == "UnsupportedError": + raise UnsupportedError(response.extra_info["message"]) + elif response.extra_info["type"] == "ConfigurationError": + raise ConfigurationError(response.extra_info["message"]) + elif response.extra_info["type"] == "ExternalError": + raise ExternalError(response.extra_info["message"]) + else: + raise TypeError(f"Herald action call returned invalid error:\n" + f"[p]{response}[/p]") elif isinstance(response, ResponseSuccess): return response.data else: @@ -233,8 +244,6 @@ class Serf: response_data = await event.run(**message.data) return ResponseSuccess(data=response_data) except Exception as e: - sentry_sdk.capture_exception(e) - 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={ @@ -290,8 +299,7 @@ class Serf: await data.reply(f"⚠️ {e.message}") except Exception as e: self.sentry_exc(e) - error_message = f"⛔ [b]{e.__class__.__name__}[/b]\n" \ - '\n'.join(e.args) + error_message = f"⛔️ [b]{e.__class__.__name__}[/b]\n" + '\n'.join(e.args) await data.reply(error_message) async def run(self): @@ -300,12 +308,21 @@ class Serf: # OVERRIDE THIS METHOD! @classmethod - def run_process(cls, *args, **kwargs): + def run_process(cls, *args, log_level: str = "WARNING", **kwargs): """Blockingly create and run the Serf. This should be used as the target of a :class:`multiprocessing.Process`.""" serf = cls(*args, **kwargs) + royalnet_log: logging.Logger = logging.getLogger("royalnet") + royalnet_log.setLevel(log_level) + stream_handler = logging.StreamHandler() + stream_handler.formatter = logging.Formatter("{asctime}\t| {processName}\t| {levelname}\t| {name}\t| {message}", + style="{") + if len(royalnet_log.handlers) < 1: + royalnet_log.addHandler(stream_handler) + royalnet_log.debug("Logging: ready") + if sentry_sdk is None: log.info("Sentry: not installed") else: diff --git a/royalnet/serf/telegram/telegramserf.py b/royalnet/serf/telegram/telegramserf.py index 4a053ab6..44b42a48 100644 --- a/royalnet/serf/telegram/telegramserf.py +++ b/royalnet/serf/telegram/telegramserf.py @@ -196,7 +196,7 @@ class TelegramSerf(Serf): else: session = None # Prepare data - data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message) + data = self.Data(interface=command.interface, session=session, loop=self.loop, update=update) # Call the command await self.call(command, data, parameters) # Close the alchemy session