mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 11:34:18 +00:00
Event API complete, other improvements
This commit is contained in:
parent
c2a04301ef
commit
ae522c5e2a
16 changed files with 233 additions and 186 deletions
|
@ -47,7 +47,7 @@ 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{name}\t{levelname}\t{message}", style="{")
|
||||
stream_handler.formatter = Formatter("{asctime}\t| {processName}\t| {levelname}\t| {message}", style="{")
|
||||
royalnet_log.addHandler(stream_handler)
|
||||
|
||||
def get_secret(username: str):
|
||||
|
@ -85,7 +85,7 @@ def run(telegram: typing.Optional[bool],
|
|||
secret=get_secret("herald"),
|
||||
secure=False,
|
||||
path="/")
|
||||
herald_process = multiprocessing.Process(name="Herald",
|
||||
herald_process = multiprocessing.Process(name="Herald Server",
|
||||
target=r.herald.Server(config=herald_config).run_blocking,
|
||||
daemon=True)
|
||||
herald_process.start()
|
||||
|
@ -103,6 +103,7 @@ def run(telegram: typing.Optional[bool],
|
|||
enabled_commands = []
|
||||
enabled_page_stars = []
|
||||
enabled_exception_stars = []
|
||||
enabled_events = []
|
||||
for pack in packs:
|
||||
imported = importlib.import_module(pack)
|
||||
try:
|
||||
|
@ -117,20 +118,29 @@ def run(telegram: typing.Optional[bool],
|
|||
imported_exception_stars = imported.available_exception_stars
|
||||
except AttributeError:
|
||||
raise click.ClickException(f"{pack} isn't a Royalnet Pack as it is missing available_exception_stars.")
|
||||
try:
|
||||
imported_events = imported.available_events
|
||||
except AttributeError:
|
||||
raise click.ClickException(f"{pack} isn't a Royalnet Pack as it is missing available_events.")
|
||||
enabled_commands = [*enabled_commands, *imported_commands]
|
||||
enabled_page_stars = [*enabled_page_stars, *imported_page_stars]
|
||||
enabled_exception_stars = [*enabled_exception_stars, *imported_exception_stars]
|
||||
enabled_events = [*enabled_events, *imported_events]
|
||||
|
||||
telegram_process: typing.Optional[multiprocessing.Process] = None
|
||||
if interfaces["telegram"]:
|
||||
telegram_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
|
||||
master_table=r.backpack.tables.User,
|
||||
identity_table=r.backpack.tables.Telegram,
|
||||
identity_column="tg_id")
|
||||
if alchemy_url is not None:
|
||||
telegram_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
|
||||
master_table=r.backpack.tables.User,
|
||||
identity_table=r.backpack.tables.Telegram,
|
||||
identity_column="tg_id")
|
||||
else:
|
||||
telegram_db_config = None
|
||||
telegram_serf_kwargs = {
|
||||
'alchemy_config': telegram_db_config,
|
||||
'commands': enabled_commands,
|
||||
'network_config': herald_config.copy(name="telegram"),
|
||||
'events': enabled_events,
|
||||
'herald_config': herald_config.copy(name="telegram"),
|
||||
'secrets_name': secrets_name
|
||||
}
|
||||
telegram_process = multiprocessing.Process(name="Telegram Serf",
|
||||
|
@ -141,14 +151,18 @@ def run(telegram: typing.Optional[bool],
|
|||
|
||||
discord_process: typing.Optional[multiprocessing.Process] = None
|
||||
if interfaces["discord"]:
|
||||
discord_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
|
||||
master_table=r.backpack.tables.User,
|
||||
identity_table=r.backpack.tables.Discord,
|
||||
identity_column="discord_id")
|
||||
if alchemy_url is not None:
|
||||
discord_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
|
||||
master_table=r.backpack.tables.User,
|
||||
identity_table=r.backpack.tables.Discord,
|
||||
identity_column="discord_id")
|
||||
else:
|
||||
discord_db_config = None
|
||||
discord_serf_kwargs = {
|
||||
'alchemy_config': discord_db_config,
|
||||
'commands': enabled_commands,
|
||||
'network_config': herald_config.copy(name="discord"),
|
||||
'events': enabled_events,
|
||||
'herald_config': herald_config.copy(name="discord"),
|
||||
'secrets_name': secrets_name
|
||||
}
|
||||
discord_process = multiprocessing.Process(name="Discord Serf",
|
||||
|
|
|
@ -29,14 +29,8 @@ class SummonCommand(Command):
|
|||
member = None
|
||||
guild = None
|
||||
try:
|
||||
await self.interface.call_herald_event("discord", "discordvoice", {
|
||||
"operation": "summon",
|
||||
"data": {
|
||||
"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,
|
||||
}
|
||||
})
|
||||
# TODO: do something!
|
||||
pass
|
||||
except Exception as e:
|
||||
breakpoint()
|
||||
await data.reply(f"✅ Connesso alla chat vocale.")
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
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 *
|
||||
import weakref
|
||||
|
||||
try:
|
||||
import discord
|
||||
except ImportError:
|
||||
discord = None
|
||||
|
||||
|
||||
class DiscordvoiceEvent(Event):
|
||||
name: str = "discordvoice"
|
||||
|
||||
def __init__(self, serf: Serf):
|
||||
super().__init__(serf)
|
||||
self.bards: weakref.WeakValueDictionary = weakref.WeakValueDictionary()
|
||||
|
||||
async def run(self,
|
||||
operation: str,
|
||||
data: dict):
|
||||
if not isinstance(self.serf, DiscordSerf):
|
||||
raise ValueError("`discordvoice` event cannot run on other serfs.")
|
||||
|
||||
if operation == "summon":
|
||||
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
|
||||
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!")
|
||||
|
||||
# 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}")
|
|
@ -3,8 +3,6 @@ from .ytdlfile import YtdlFile
|
|||
from .ytdlmp3 import YtdlMp3
|
||||
from .ytdldiscord import YtdlDiscord
|
||||
from .errors import *
|
||||
from .discordbard import DiscordBard
|
||||
from . import implementations
|
||||
|
||||
try:
|
||||
from .fileaudiosource import FileAudioSource
|
||||
|
@ -21,6 +19,4 @@ __all__ = [
|
|||
"NotFoundError",
|
||||
"MultipleFilesError",
|
||||
"FileAudioSource",
|
||||
"implementations",
|
||||
"DiscordBard",
|
||||
]
|
||||
|
|
|
@ -25,6 +25,14 @@ except ImportError:
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
UVICORN_LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": True,
|
||||
"formatters": {},
|
||||
"handlers": {},
|
||||
"loggers": {},
|
||||
}
|
||||
|
||||
|
||||
class Constellation:
|
||||
"""The class that represents the webserver.
|
||||
|
@ -66,13 +74,16 @@ class Constellation:
|
|||
tables = tables.union(SelectedExcStar.tables)
|
||||
log.debug(f"Found Tables: {' '.join([table.__name__ for table in tables])}")
|
||||
|
||||
log.info(f"Creating Alchemy...")
|
||||
self.alchemy: royalnet.alchemy.Alchemy = royalnet.alchemy.Alchemy(database_uri=database_uri, tables=tables)
|
||||
self.alchemy = None
|
||||
"""The :class:`Alchemy` of this Constellation."""
|
||||
|
||||
if database_uri is not None:
|
||||
log.info(f"Creating Alchemy...")
|
||||
self.alchemy: royalnet.alchemy.Alchemy = royalnet.alchemy.Alchemy(database_uri=database_uri, tables=tables)
|
||||
|
||||
log.info("Registering PageStars...")
|
||||
for SelectedPageStar in page_stars:
|
||||
log.info(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__class__.__name__}")
|
||||
log.info(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
|
||||
try:
|
||||
page_star_instance = SelectedPageStar(constellation=self)
|
||||
except Exception as e:
|
||||
|
@ -82,7 +93,7 @@ class Constellation:
|
|||
|
||||
log.info("Registering ExceptionStars...")
|
||||
for SelectedExcStar in exc_stars:
|
||||
log.info(f"Registering: {SelectedExcStar.error} -> {SelectedExcStar.__class__.__name__}")
|
||||
log.info(f"Registering: {SelectedExcStar.error} -> {SelectedExcStar.__name__}")
|
||||
try:
|
||||
exc_star_instance = SelectedExcStar(constellation=self)
|
||||
except Exception as e:
|
||||
|
@ -141,10 +152,10 @@ class Constellation:
|
|||
release=release)
|
||||
log.info(f"Sentry: enabled (Royalnet {release})")
|
||||
# Run the server
|
||||
log.info(f"Running Constellation on {address}:{port}...")
|
||||
log.info(f"Running Constellation on https://{address}:{port}/ ...")
|
||||
constellation.running = True
|
||||
try:
|
||||
uvicorn.run(constellation.starlette, host=address, port=port)
|
||||
uvicorn.run(constellation.starlette, host=address, port=port, log_config=UVICORN_LOGGING_CONFIG)
|
||||
finally:
|
||||
constellation.running = False
|
||||
|
||||
|
|
|
@ -85,10 +85,10 @@ class Link:
|
|||
|
||||
async def connect(self):
|
||||
"""Connect to the :class:`Server` at :attr:`.master_uri`."""
|
||||
log.info(f"Connecting to {self.config.url}...")
|
||||
log.debug(f"Connecting to Herald Server at {self.config.url}...")
|
||||
self.websocket = await websockets.connect(self.config.url, loop=self._loop)
|
||||
self.connect_event.set()
|
||||
log.info(f"Connected!")
|
||||
log.debug(f"Connected!")
|
||||
|
||||
@requires_connection
|
||||
async def receive(self) -> Package:
|
||||
|
@ -103,7 +103,7 @@ class Link:
|
|||
self.error_event.set()
|
||||
self.connect_event.clear()
|
||||
self.identify_event.clear()
|
||||
log.info(f"Connection to {self.config.url} was closed.")
|
||||
log.warning(f"Herald Server connection closed: {self.config.url}")
|
||||
# What to do now? Let's just reraise.
|
||||
raise ConnectionClosedError()
|
||||
if self.identify_event.is_set() and package.destination != self.nid:
|
||||
|
@ -113,7 +113,7 @@ class Link:
|
|||
|
||||
@requires_connection
|
||||
async def identify(self) -> None:
|
||||
log.info(f"Identifying...")
|
||||
log.debug(f"Identifying...")
|
||||
await self.websocket.send(f"Identify {self.nid}:{self.config.name}:{self.config.secret}")
|
||||
response: Package = await self.receive()
|
||||
if not response.source == "<server>":
|
||||
|
@ -124,7 +124,7 @@ class Link:
|
|||
raise ConnectionClosedError(f"Identification error: {response.data['type']}")
|
||||
assert response.data["type"] == "success"
|
||||
self.identify_event.set()
|
||||
log.info(f"Identified successfully!")
|
||||
log.debug(f"Identified successfully!")
|
||||
|
||||
@requires_identification
|
||||
async def send(self, package: Package):
|
||||
|
|
|
@ -62,28 +62,30 @@ class Server:
|
|||
return matching or []
|
||||
|
||||
async def listener(self, websocket: "websockets.server.WebSocketServerProtocol", path):
|
||||
log.info(f"{websocket.remote_address} connected to the server.")
|
||||
connected_client = ConnectedClient(websocket)
|
||||
# Wait for identification
|
||||
identify_msg = await websocket.recv()
|
||||
log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
|
||||
if not isinstance(identify_msg, str):
|
||||
log.warning(f"Failed Herald identification: {websocket.remote_address[0]}:{websocket.remote_address[1]}")
|
||||
await connected_client.send_service("error", "Invalid identification message (not a str)")
|
||||
return
|
||||
identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
|
||||
if identification is None:
|
||||
log.warning(f"Failed Herald identification: {websocket.remote_address[0]}:{websocket.remote_address[1]}")
|
||||
await connected_client.send_service("error", "Invalid identification message (regex failed)")
|
||||
return
|
||||
secret = identification.group(3)
|
||||
if secret != self.config.secret:
|
||||
log.warning(f"Invalid Herald secret: {websocket.remote_address[0]}:{websocket.remote_address[1]}")
|
||||
await connected_client.send_service("error", "Invalid secret")
|
||||
return
|
||||
# Identification successful
|
||||
connected_client.nid = identification.group(1)
|
||||
connected_client.link_type = identification.group(2)
|
||||
log.info(f"Joined the Herald: {websocket.remote_address[0]}:{websocket.remote_address[1]}"
|
||||
f" ({connected_client.link_type})")
|
||||
self.identified_clients.append(connected_client)
|
||||
log.debug(f"{websocket.remote_address} identified successfully as {connected_client.nid}"
|
||||
f" ({connected_client.link_type}).")
|
||||
await connected_client.send_service("success", "Identification successful!")
|
||||
log.debug(f"{connected_client.nid}'s identification confirmed.")
|
||||
# Main loop
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from .createrichembed import create_rich_embed
|
||||
from .escape import escape
|
||||
from .discordserf import DiscordSerf
|
||||
from . import discordbard
|
||||
|
||||
__all__ = [
|
||||
"create_rich_embed",
|
||||
"escape",
|
||||
"DiscordSerf",
|
||||
"discordbard",
|
||||
]
|
||||
|
|
33
royalnet/serf/discord/barddict.py
Normal file
33
royalnet/serf/discord/barddict.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from typing import Dict, Any
|
||||
from .discordbard import DiscordBard
|
||||
|
||||
try:
|
||||
import discord
|
||||
except ImportError:
|
||||
discord = None
|
||||
|
||||
|
||||
class BardsDict:
|
||||
def __init__(self, client: "discord.Client"):
|
||||
if discord is None:
|
||||
raise ImportError("'discord' extra is not installed.")
|
||||
self.client: "discord.Client" = client
|
||||
self._dict: Dict["discord.Guild", DiscordBard] = dict()
|
||||
|
||||
def __getitem__(self, item: "discord.Guild") -> DiscordBard:
|
||||
bard = self._dict[item]
|
||||
if bard.voice_client not in self.client.voice_clients:
|
||||
del self._dict[item]
|
||||
raise KeyError("Requested bard is disconnected and was removed from the dict.")
|
||||
return bard
|
||||
|
||||
def __setitem__(self, key: "discord.Guild", value):
|
||||
if not isinstance(value, DiscordBard):
|
||||
raise TypeError(f"Cannot __setitem__ with {value.__class__.__name__}.")
|
||||
self._dict[key] = value
|
||||
|
||||
def get(self, item: "discord.Guild", default: Any = None) -> Any:
|
||||
try:
|
||||
return self[item]
|
||||
except KeyError:
|
||||
return default
|
|
@ -1,7 +1,9 @@
|
|||
from .discordbard import DiscordBard
|
||||
from .dbstack import DBStack
|
||||
from .dbqueue import DBQueue
|
||||
|
||||
__all__ = [
|
||||
"DBStack",
|
||||
"DBQueue",
|
||||
"DiscordBard",
|
||||
]
|
|
@ -1,15 +1,19 @@
|
|||
from ..fileaudiosource import FileAudioSource
|
||||
from ..discordbard import DiscordBard
|
||||
from ..ytdldiscord import YtdlDiscord
|
||||
from royalnet.bard import FileAudioSource, YtdlDiscord
|
||||
from typing import List, AsyncGenerator, Tuple, Any, Dict, Optional
|
||||
from .discordbard import DiscordBard
|
||||
|
||||
try:
|
||||
import discord
|
||||
except ImportError:
|
||||
discord = None
|
||||
|
||||
|
||||
class DBQueue(DiscordBard):
|
||||
"""A First-In-First-Out music queue.
|
||||
|
||||
It is what was once called a ``playlist``."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, voice_client: "discord.VoiceClient"):
|
||||
super().__init__(voice_client)
|
||||
self.list: List[YtdlDiscord] = []
|
||||
|
||||
async def _generator(self) -> AsyncGenerator[Optional[FileAudioSource], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
|
|
@ -1,15 +1,19 @@
|
|||
from ..fileaudiosource import FileAudioSource
|
||||
from ..discordbard import DiscordBard
|
||||
from ..ytdldiscord import YtdlDiscord
|
||||
from typing import List, AsyncGenerator, Tuple, Any, Dict, Optional
|
||||
from royalnet.bard import FileAudioSource, YtdlDiscord
|
||||
from .discordbard import DiscordBard
|
||||
|
||||
try:
|
||||
import discord
|
||||
except ImportError:
|
||||
discord = None
|
||||
|
||||
|
||||
class DBStack(DiscordBard):
|
||||
"""A First-In-Last-Out music queue.
|
||||
|
||||
Not really sure if it is going to be useful..."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, voice_client: "discord.VoiceClient"):
|
||||
super().__init__(voice_client)
|
||||
self.list: List[YtdlDiscord] = []
|
||||
|
||||
async def _generator(self) -> AsyncGenerator[Optional[FileAudioSource], Tuple[Tuple[Any, ...], Dict[str, Any]]]:
|
|
@ -1,7 +1,10 @@
|
|||
from typing import Optional, AsyncGenerator, List, Tuple, Any, Dict
|
||||
from .ytdldiscord import YtdlDiscord
|
||||
from .fileaudiosource import FileAudioSource
|
||||
from .errors import UnsupportedError
|
||||
from royalnet.bard import YtdlDiscord, FileAudioSource, UnsupportedError
|
||||
|
||||
try:
|
||||
import discord
|
||||
except ImportError:
|
||||
discord = None
|
||||
|
||||
|
||||
class DiscordBard:
|
||||
|
@ -9,11 +12,14 @@ class DiscordBard:
|
|||
|
||||
Possible implementation may be playlist, song pools, multilayered tracks, and so on."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, voice_client: "discord.VoiceClient"):
|
||||
"""Create manually a :class:`DiscordBard`.
|
||||
|
||||
Warning:
|
||||
Avoid calling this method, please use :meth:`create` instead!"""
|
||||
self.voice_client: "discord.VoiceClient" = voice_client
|
||||
"""The voice client that this :class:`DiscordBard` refers to."""
|
||||
|
||||
self.now_playing: Optional[YtdlDiscord] = None
|
||||
"""The :class:`YtdlDiscord` that's currently being played."""
|
||||
|
||||
|
@ -26,13 +32,13 @@ class DiscordBard:
|
|||
it can take a args+kwargs tuple in input to optionally select a different source.
|
||||
|
||||
The generator should ``yield`` once before doing anything else."""
|
||||
args, kwargs = yield
|
||||
yield
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
async def create(cls) -> "DiscordBard":
|
||||
async def create(cls, voice_client: "discord.VoiceClient") -> "DiscordBard":
|
||||
"""Create an instance of the :class:`DiscordBard`, and initialize its async generator."""
|
||||
bard = cls()
|
||||
bard = cls(voice_client=voice_client)
|
||||
# noinspection PyTypeChecker
|
||||
none = bard.generator.asend(None)
|
||||
assert none is None
|
|
@ -3,8 +3,11 @@ import logging
|
|||
from typing import Type, Optional, List, Union
|
||||
from royalnet.commands import *
|
||||
from royalnet.utils import asyncify
|
||||
from royalnet.serf import Serf
|
||||
from .escape import escape
|
||||
from ..serf import Serf
|
||||
from .discordbard import *
|
||||
from .barddict import BardsDict
|
||||
|
||||
|
||||
try:
|
||||
import discord
|
||||
|
@ -33,6 +36,7 @@ class DiscordSerf(Serf):
|
|||
def __init__(self, *,
|
||||
alchemy_config: Optional[AlchemyConfig] = None,
|
||||
commands: List[Type[Command]] = None,
|
||||
events: List[Type[Event]] = None,
|
||||
herald_config: Optional[HeraldConfig] = None,
|
||||
secrets_name: str = "__default__"):
|
||||
if discord is None:
|
||||
|
@ -40,6 +44,7 @@ class DiscordSerf(Serf):
|
|||
|
||||
super().__init__(alchemy_config=alchemy_config,
|
||||
commands=commands,
|
||||
events=events,
|
||||
herald_config=herald_config,
|
||||
secrets_name=secrets_name)
|
||||
|
||||
|
@ -47,7 +52,10 @@ class DiscordSerf(Serf):
|
|||
"""The custom :class:`discord.Client` class that will be instantiated later."""
|
||||
|
||||
self.client = self.Client()
|
||||
"""The custo :class:`discord.Client` instance."""
|
||||
"""The custom :class:`discord.Client` instance."""
|
||||
|
||||
self.bards: BardsDict = BardsDict(self.client)
|
||||
"""A dictionary containing all bards spawned by this :class:`DiscordSerf`."""
|
||||
|
||||
def interface_factory(self) -> Type[CommandInterface]:
|
||||
# noinspection PyPep8Naming
|
||||
|
@ -144,15 +152,99 @@ class DiscordSerf(Serf):
|
|||
|
||||
return DiscordClient
|
||||
|
||||
def get_voice_client(self, guild: "discord.Guild") -> Optional["discord.VoiceClient"]:
|
||||
voice_clients: List["discord.VoiceClient"] = self.client.voice_clients
|
||||
for voice_client in voice_clients:
|
||||
if voice_client.guild == guild:
|
||||
return voice_client
|
||||
return None
|
||||
|
||||
async def run(self):
|
||||
await super().run()
|
||||
token = self.get_secret("discord")
|
||||
if token is None:
|
||||
raise ValueError("Missing discord token")
|
||||
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"]:
|
||||
"""Find the best channel matching all requests.
|
||||
|
||||
In case multiple channels match all requests, return the one with the most members connected.
|
||||
|
||||
Args:
|
||||
channel_type: Filter channels by type (select only :class:`discord.VoiceChannel`,
|
||||
:class:`discord.TextChannel`, ...).
|
||||
name: Filter channels by name starting with ``name`` (using :meth:`str.startswith`).
|
||||
Note that some channel types don't have names; this check will be skipped for them.
|
||||
guild: Filter channels by guild, keep only channels inside this one.
|
||||
accessible_to: Filter channels by permissions, keeping only channels where *all* these users have
|
||||
the required permissions.
|
||||
required_permissions: Filter channels by permissions, keeping only channels where the users have *all* these
|
||||
:class:`discord.Permissions`.
|
||||
|
||||
Returns:
|
||||
Either a :class:`~discord.abc.GuildChannel`, or :const:`None` if no channels were found."""
|
||||
if accessible_to is None:
|
||||
accessible_to = []
|
||||
if required_permissions is None:
|
||||
required_permissions = []
|
||||
channels: List[discord.abc.GuildChannel] = []
|
||||
for ch in self.client.get_all_channels():
|
||||
if channel_type is not None and not isinstance(ch, channel_type):
|
||||
continue
|
||||
|
||||
if name is not None:
|
||||
try:
|
||||
ch_name: str = ch.name
|
||||
if not ch_name.startswith(name):
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
ch_guild: "discord.Guild" = ch.guild
|
||||
if ch.guild == ch_guild:
|
||||
continue
|
||||
|
||||
for user in accessible_to:
|
||||
member: "discord.Member" = guild.get_member(user.id)
|
||||
if member is None:
|
||||
continue
|
||||
permissions: "discord.Permissions" = ch.permissions_for(member)
|
||||
missing_perms = False
|
||||
for permission in required_permissions:
|
||||
if not permissions.__getattribute__(permission):
|
||||
missing_perms = True
|
||||
break
|
||||
if missing_perms:
|
||||
continue
|
||||
|
||||
channels.append(ch)
|
||||
|
||||
if len(channels) == 0:
|
||||
return None
|
||||
else:
|
||||
# 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)
|
||||
|
||||
return channels[0]
|
||||
|
||||
async def voice_connect(self, channel: "discord.VoiceChannel"):
|
||||
"""Try to connect to a :class:`discord.VoiceChannel` and to create the corresponing :class:`DiscordBard`.
|
||||
|
||||
Info:
|
||||
Command-compatible! This method will raise :exc:`CommandError`s for all its errors, so it can be called
|
||||
inside a command!"""
|
||||
try:
|
||||
voice_client = 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!")
|
||||
self.bards[channel.guild] = DBQueue(voice_client=voice_client)
|
||||
|
|
|
@ -103,7 +103,7 @@ class Serf:
|
|||
log.info("Herald: disabled")
|
||||
else:
|
||||
self.init_herald(herald_config, events)
|
||||
log.info(f"Herald: {self.herald}")
|
||||
log.info(f"Herald: {len(self.events)} events bound")
|
||||
|
||||
self.loop: Optional[AbstractEventLoop] = None
|
||||
"""The event loop this Serf is running on."""
|
||||
|
@ -316,4 +316,8 @@ class Serf:
|
|||
serf.init_sentry(sentry_dsn)
|
||||
|
||||
serf.loop = get_event_loop()
|
||||
serf.loop.run_until_complete(serf.run())
|
||||
try:
|
||||
serf.loop.run_until_complete(serf.run())
|
||||
except Exception as e:
|
||||
log.error(f"Uncaught exception: {e}")
|
||||
serf.sentry_exc(e)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import logging
|
||||
import asyncio
|
||||
from typing import Type, Optional, List, Callable
|
||||
from royalnet.commands import Command, CommandInterface, CommandData, CommandArgs, CommandError, InvalidInputError, \
|
||||
UnsupportedError
|
||||
from royalnet.commands import *
|
||||
from royalnet.utils import asyncify
|
||||
from .escape import escape
|
||||
from ..serf import Serf
|
||||
|
@ -38,6 +37,7 @@ class TelegramSerf(Serf):
|
|||
def __init__(self, *,
|
||||
alchemy_config: Optional[AlchemyConfig] = None,
|
||||
commands: List[Type[Command]] = None,
|
||||
events: List[Type[Event]] = None,
|
||||
herald_config: Optional[HeraldConfig] = None,
|
||||
secrets_name: str = "__default__"):
|
||||
if telegram is None:
|
||||
|
@ -45,6 +45,7 @@ class TelegramSerf(Serf):
|
|||
|
||||
super().__init__(alchemy_config=alchemy_config,
|
||||
commands=commands,
|
||||
events=events,
|
||||
herald_config=herald_config,
|
||||
secrets_name=secrets_name)
|
||||
|
||||
|
|
Loading…
Reference in a new issue