1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

what a mess

This commit is contained in:
Steffo 2019-11-18 19:44:30 +01:00
parent 7bbf7f334b
commit b2d5039c76
9 changed files with 178 additions and 94 deletions

View file

@ -1,16 +1,19 @@
"""A Pack that is imported by default by all :mod:`royalnet` instances.""" """A Pack that is imported by default by all :mod:`royalnet` instances."""
from . import commands, tables, stars from . import commands, tables, stars, events
from .commands import available_commands from .commands import available_commands
from .tables import available_tables from .tables import available_tables
from .stars import available_page_stars, available_exception_stars from .stars import available_page_stars, available_exception_stars
from .events import available_events
__all__ = [ __all__ = [
"commands", "commands",
"tables", "tables",
"stars", "stars",
"events",
"available_commands", "available_commands",
"available_tables", "available_tables",
"available_page_stars", "available_page_stars",
"available_exception_stars", "available_exception_stars",
"available_events",
] ]

View file

@ -12,6 +12,8 @@ if TYPE_CHECKING:
class SummonCommand(Command): class SummonCommand(Command):
# TODO: possibly move this in another pack
name: str = "summon" name: str = "summon"
description = "Connect the bot to a Discord voice channel." description = "Connect the bot to a Discord voice channel."
@ -19,89 +21,13 @@ class SummonCommand(Command):
syntax = "[channelname]" syntax = "[channelname]"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
# This command only runs on Discord!
if self.interface.name != "discord":
# TODO: use a Herald Event to remotely connect the bot
raise UnsupportedError()
if discord is None:
raise ConfigurationError("'discord' extra is not installed.")
# noinspection PyUnresolvedReferences
message: discord.Message = data.message
member: Union[discord.User, discord.Member] = message.author
serf: DiscordSerf = self.interface.serf
client: discord.Client = serf.client
channel_name: Optional[str] = args.joined()
# If the channel name was passed as an argument...
if channel_name != "":
# Try to find all possible channels
channels: List[discord.VoiceChannel] = []
for ch in client.get_all_channels():
guild: discord.Guild = ch.guild
# Ensure the channel is a voice channel
if not isinstance(ch, discord.VoiceChannel):
continue
# Ensure the channel starts with the requested name
ch_name: str = ch.name
if not ch_name.startswith(channel_name):
continue
# Ensure that the command author can access the channel
if guild.get_member(member.id) is None:
continue
member_permissions: discord.Permissions = ch.permissions_for(member)
if not (member_permissions.connect and member_permissions.speak):
continue
# Ensure that the bot can access the channel
bot_member = guild.get_member(client.user.id)
bot_permissions: discord.Permissions = ch.permissions_for(bot_member)
if not (bot_permissions.connect and bot_permissions.speak):
continue
# Found one!
channels.append(ch)
# Ensure at least a single channel is returned
if len(channels) == 0:
raise InvalidInputError("Could not find any channel to connect to.")
elif len(channels) == 1:
channel = channels[0]
else:
# Give priority to channels in the current guild
filter_by_guild = False
for ch in channels:
if ch.guild == message.guild:
filter_by_guild = True
break
if filter_by_guild:
new_channels = []
for ch in channels:
if ch.guild == message.guild:
new_channels.append(ch)
channels = new_channels
# Give priority to channels with the most people
def people_count(c: discord.VoiceChannel):
return len(c.members)
channels.sort(key=people_count, reverse=True)
channel = channels[0]
else:
# Try to use the channel in which the command author is in
voice: Optional[discord.VoiceState] = message.author.voice
if voice is None:
raise UserError("You must be connected to a voice channel to summon the bot without any arguments.")
channel: discord.VoiceChannel = voice.channel
# Try to connect to the voice channel
try: try:
await channel.connect() await self.interface.call_herald_action("discord", "discordvoice", {
except asyncio.TimeoutError: "operation": "summon",
raise ExternalError("Timed out while trying to connect to the channel") "data": {
except discord.opus.OpusNotLoaded: "channel_name": args.joined()
raise ConfigurationError("[c]libopus[/c] is not loaded in the serf") }
except discord.ClientException as e: })
# The bot is already connected to a voice channel except Exception as e:
# TODO: safely move the bot somewhere else breakpoint()
raise CommandError("The bot is already connected in another channel.") await data.reply(f"✅ Connesso alla chat vocale.")
await data.reply(f"✅ Connected to <#{channel.id}>!")

View file

@ -0,0 +1,10 @@
# Imports go here!
# Enter the commands of your Pack here!
available_events = [
]
# Don't change this, it should automatically generate __all__
__all__ = [command.__name__ for command in available_events]

View file

@ -0,0 +1,113 @@
import asyncio
from typing import Dict, List, Optional
from royalnet.commands import *
from royalnet.serf import Serf
from royalnet.serf.discord import DiscordSerf
from royalnet.bard import DiscordBard
from royalnet.bard.implementations import *
try:
import discord
except ImportError:
discord = None
class DiscordvoiceEvent(Event):
name: str = "discordvoice"
def __init__(self, serf: Serf):
super().__init__(serf)
self.bards: Dict["discord.Guild", DiscordBard] = {}
async def run(self, 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")
client: discord.Client = self.serf.client
# Get the guild, if it exists
if guild_id is not None:
guild: Optional[discord.Guild] = client.get_guild(guild_id)
else:
guild = None
# Get the member, if it exists
if member_id is not None and guild is not None:
member: Optional[discord.Member] = guild.get_member(member_id)
else:
member = None
# Try to find all possible channels
channels: List[discord.VoiceChannel] = []
for ch in client.get_all_channels():
guild: discord.Guild = ch.guild
# Ensure the channel is a voice channel
if not isinstance(ch, discord.VoiceChannel):
continue
# Ensure the channel starts with the requested name
ch_name: str = ch.name
if not ch_name.startswith(channel_name):
continue
# Ensure that the command author can access the channel
if member is not None:
member_permissions: discord.Permissions = ch.permissions_for(member)
if not (member_permissions.connect and member_permissions.speak):
continue
# Ensure that the bot can access the channel
bot_member = guild.get_member(client.user.id)
bot_permissions: discord.Permissions = ch.permissions_for(bot_member)
if not (bot_permissions.connect and bot_permissions.speak):
continue
# Found one!
channels.append(ch)
# Ensure at least a single channel is returned
if len(channels) == 0:
raise InvalidInputError("Could not find any channel to connect to.")
else:
# Give priority to channels in the current guild
filter_by_guild = False
for ch in channels:
if ch.guild == guild:
filter_by_guild = True
break
if filter_by_guild:
new_channels = []
for ch in channels:
if ch.guild == guild:
new_channels.append(ch)
channels = new_channels
# Give priority to channels with the most people
def people_count(c: discord.VoiceChannel):
return len(c.members)
channels.sort(key=people_count, reverse=True)
channel = channels[0]
# Try to connect to the voice channel
try:
await channel.connect()
except asyncio.TimeoutError:
raise ExternalError("Timed out while trying to connect to the channel")
except discord.opus.OpusNotLoaded:
raise ConfigurationError("[c]libopus[/c] is not loaded in the serf")
except discord.ClientException:
# The bot is already connected to a voice channel
# TODO: safely move the bot somewhere else
raise CommandError("The bot is already connected in another channel.\n"
" Please disconnect it before resummoning!")
return {
"connected": True
}
else:
raise ValueError(f"Invalid operation received: {operation}")

View file

View file

@ -2,6 +2,7 @@ from .commandinterface import CommandInterface
from .command import Command from .command import Command
from .commanddata import CommandData from .commanddata import CommandData
from .commandargs import CommandArgs from .commandargs import CommandArgs
from .event import Event
from .errors import CommandError, \ from .errors import CommandError, \
InvalidInputError, \ InvalidInputError, \
UnsupportedError, \ UnsupportedError, \
@ -20,4 +21,5 @@ __all__ = [
"ConfigurationError", "ConfigurationError",
"ExternalError", "ExternalError",
"UserError", "UserError",
"Event"
] ]

View file

@ -0,0 +1,30 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from serf import Serf
class Event:
"""A remote procedure call triggered by a :mod:`royalnet.herald` request."""
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):
"""Bind the event to a :class:`~royalnet.serf.Serf`."""
self.serf: Serf = serf
@property
def alchemy(self):
"""A shortcut for :attr:`.serf.alchemy`."""
return self.serf.alchemy
@property
def loop(self):
"""A shortcut for :attr:`.serf.loop`"""
return self.serf.loop
async def run(self, data: dict):
raise NotImplementedError()

View file

@ -43,7 +43,7 @@ class DiscordSerf(Serf):
network_config=network_config, network_config=network_config,
secrets_name=secrets_name) secrets_name=secrets_name)
self.Client = self.bot_factory() self.Client = self.client_factory()
"""The custom :class:`discord.Client` class that will be instantiated later.""" """The custom :class:`discord.Client` class that will be instantiated later."""
self.client = self.Client() self.client = self.Client()
@ -129,7 +129,7 @@ class DiscordSerf(Serf):
if session is not None: if session is not None:
await asyncify(session.close) await asyncify(session.close)
def bot_factory(self) -> Type["discord.Client"]: def client_factory(self) -> Type["discord.Client"]:
"""Create a custom class inheriting from :py:class:`discord.Client`.""" """Create a custom class inheriting from :py:class:`discord.Client`."""
# noinspection PyMethodParameters # noinspection PyMethodParameters
class DiscordClient(discord.Client): class DiscordClient(discord.Client):

View file

@ -235,27 +235,27 @@ class Serf:
async def network_handler(self, message: Union[Request, Broadcast]) -> Response: async def network_handler(self, message: Union[Request, Broadcast]) -> Response:
try: try:
network_handler = self.herald_handlers[message.handler] herald_handler = self.herald_handlers[message.handler]
except KeyError: except KeyError:
log.warning(f"Missing network_handler for {message.handler}") log.warning(f"Missing network_handler for {message.handler}")
return ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.") return ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.")
else: else:
log.debug(f"Using {network_handler} as handler for {message.handler}") log.debug(f"Using {herald_handler} as handler for {message.handler}")
if isinstance(message, Request): if isinstance(message, Request):
try: try:
response_data = await network_handler(self, **message.data) response_data = await herald_handler(self, **message.data)
return ResponseSuccess(data=response_data) return ResponseSuccess(data=response_data)
except Exception as e: except Exception as e:
sentry_sdk.capture_exception(e) sentry_sdk.capture_exception(e)
log.error(f"Exception {e} in {network_handler}") log.error(f"Exception {e} in {herald_handler}")
return ResponseFailure("exception_in_handler", return ResponseFailure("exception_in_handler",
f"An exception was raised in {network_handler} for {message.handler}.", f"An exception was raised in {herald_handler} for {message.handler}.",
extra_info={ extra_info={
"type": e.__class__.__name__, "type": e.__class__.__name__,
"message": str(e) "message": str(e)
}) })
elif isinstance(message, Broadcast): elif isinstance(message, Broadcast):
await network_handler(self, **message.data) await herald_handler(self, **message.data)
@staticmethod @staticmethod
def init_sentry(dsn): def init_sentry(dsn):