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:
parent
7bbf7f334b
commit
b2d5039c76
9 changed files with 178 additions and 94 deletions
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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}>!")
|
|
10
royalnet/backpack/events/__init__.py
Normal file
10
royalnet/backpack/events/__init__.py
Normal 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]
|
113
royalnet/backpack/events/discordvoice.py
Normal file
113
royalnet/backpack/events/discordvoice.py
Normal 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}")
|
0
royalnet/backpack/utils/__init__.py
Normal file
0
royalnet/backpack/utils/__init__.py
Normal 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"
|
||||||
]
|
]
|
||||||
|
|
30
royalnet/commands/event.py
Normal file
30
royalnet/commands/event.py
Normal 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()
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue