diff --git a/royalnet/audio/royalpcmfile.py b/royalnet/audio/royalpcmfile.py index b62332ae..36969d5a 100644 --- a/royalnet/audio/royalpcmfile.py +++ b/royalnet/audio/royalpcmfile.py @@ -4,7 +4,7 @@ import os import typing import time from .youtubedl import YtdlFile, YtdlInfo -from ..utils import safefilename +from ..utils import fileformat log = logging.getLogger(__name__) @@ -82,14 +82,14 @@ class RoyalPCMFile(YtdlFile): Warning: It's going to be deleted as soon as the :py:func:`royalnet.audio.RoyalPCMFile.__init__` function has completed, so it's probably not going to be very useful... """ - return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.ytdl" + return f"./downloads/{fileformat(self.info.title)}-{fileformat(str(int(self._time)))}.ytdl" @property def audio_filename(self) -> str: """ Returns: The name of the downloaded and PCM-converted audio file.""" - return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.pcm" + return f"./downloads/{fileformat(self.info.title)}-{fileformat(str(int(self._time)))}.pcm" def delete_audio_file(self): """Delete the PCM-converted audio file.""" diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index 1373a61e..0bc2b2d2 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -6,7 +6,7 @@ from .generic import GenericBot from ..commands import NullCommand from ..utils import asyncify, Call, Command from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError -from ..network import RoyalnetConfig, Request, Response, ResponseSuccess, ResponseError +from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError from ..database import DatabaseConfig from ..audio import PlayMode, Playlist, RoyalPCMAudio diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index f2d44955..cb119a14 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -4,7 +4,7 @@ import asyncio import logging from ..utils import Command, NetworkHandler, Call from ..commands import NullCommand -from ..network import RoyalnetLink, Request, Response, ResponseSuccess, ResponseError, RoyalnetConfig +from ..network import RoyalnetLink, Request, Response, ResponseError, RoyalnetConfig from ..database import Alchemy, DatabaseConfig, relationshiplinkchain diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index 10a6d444..e034cfdd 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -22,9 +22,10 @@ from .skip import SkipCommand from .playmode import PlaymodeCommand from .videochannel import VideochannelCommand from .missing import MissingCommand +from .cv import CvCommand __all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand", "SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand", "KvactiveCommand", "KvCommand", "KvrollCommand", "VideoinfoCommand", "SummonCommand", "PlayCommand", - "SkipCommand", "PlaymodeCommand", "VideochannelCommand", "MissingCommand"] + "SkipCommand", "PlaymodeCommand", "VideochannelCommand", "MissingCommand", "CvCommand"] diff --git a/royalnet/commands/cv.py b/royalnet/commands/cv.py new file mode 100644 index 00000000..338e5bf8 --- /dev/null +++ b/royalnet/commands/cv.py @@ -0,0 +1,122 @@ +import typing +import discord +import asyncio +from ..utils import Command, Call, NetworkHandler, andformat +from ..network import Request, ResponseSuccess +from ..error import NoneFoundError, TooManyFoundError +if typing.TYPE_CHECKING: + from ..bots import DiscordBot + + +loop = asyncio.get_event_loop() + + +class CvNH(NetworkHandler): + message_type = "discord_cv" + + @classmethod + async def discord(cls, bot: "DiscordBot", data: dict): + # Find the matching guild + if data["guild_name"]: + guild: discord.Guild = bot.client.find_guild_by_name(data["guild_name"]) + else: + if len(bot.client.guilds) == 0: + raise NoneFoundError("No guilds found") + if len(bot.client.guilds) > 1: + raise TooManyFoundError("Multiple guilds found") + guild = list(bot.client.guilds)[0] + # Edit the message, sorted by channel + discord_members = list(guild.members) + channels = {0: None} + members_in_channels = {0: []} + message = "" + # Find all the channels + for member in discord_members: + if member.voice is not None: + channel = members_in_channels.get(member.voice.channel.id) + if channel is None: + members_in_channels[member.voice.channel.id] = list() + channel = members_in_channels[member.voice.channel.id] + channels[member.voice.channel.id] = member.voice.channel + channel.append(member) + else: + members_in_channels[0].append(member) + # Edit the message, sorted by channel + for channel in sorted(channels, key=lambda c: -c): + members_in_channels[channel].sort(key=lambda x: x.nick if x.nick is not None else x.name) + if channel == 0: + message += "[b]Non in chat vocale:[/b]\n" + else: + message += f"[b]In #{channels[channel].name}:[/b]\n" + for member in members_in_channels[channel]: + member: typing.Union[discord.User, discord.Member] + # Ignore not-connected non-notable members + if not data["full"] and channel == 0 and len(member.roles) < 2: + continue + # Ignore offline members + if member.status == discord.Status.offline and member.voice is None: + continue + # Online status emoji + if member.bot: + message += "🤖 " + elif member.status == discord.Status.online: + message += "🔵 " + elif member.status == discord.Status.idle: + message += "⚫️ " + elif member.status == discord.Status.dnd: + message += "🔴 " + elif member.status == discord.Status.offline: + message += "⚪️ " + # Voice + if channel != 0: + # Voice status + if member.voice.self_mute: + message += f"🔇 " + else: + message += f"🔊 " + # Nickname + if member.nick is not None: + message += f"[i]{member.nick}[/i]" + else: + message += member.name + # Game or stream + if member.activity is not None: + if member.activity.type == discord.ActivityType.playing: + message += f" | 🎮 {member.activity.name}" + # Rich presence + try: + if member.activity.state is not None: + message += f" ({member.activity.state}" \ + f" | {member.activity.details})" + except AttributeError: + pass + elif member.activity.type == discord.ActivityType.streaming: + message += f" | 📡 {member.activity.url})" + elif member.activity.type == discord.ActivityType.listening: + if isinstance(member.activity, discord.Spotify): + if member.activity.title == member.activity.album: + message += f" | 🎧 {member.activity.title} ({andformat(member.activity.artists, final=' e ')})" + else: + message += f" | 🎧 {member.activity.title} ({member.activity.album} | {andformat(member.activity.artists, final=' e ')})" + else: + message += f" | 🎧 {member.activity.name}" + elif member.activity.type == discord.ActivityType.watching: + message += f" | 📺 {member.activity.name}" + message += "\n" + message += "\n" + return ResponseSuccess({"response": message}) + + +class CvCommand(Command): + + command_name = "cv" + command_description = "Elenca le persone attualmente connesse alla chat vocale." + command_syntax = "[guildname]" + + network_handlers = [CvNH] + + @classmethod + async def common(cls, call: Call): + guild_name = call.args.optional(0) + response = await call.net_request(Request("discord_cv", {"guild_name": guild_name, "full": False}), "discord") + await call.reply(response["response"]) diff --git a/royalnet/commands/error_handler.py b/royalnet/commands/error_handler.py index 3012e648..f82c833b 100644 --- a/royalnet/commands/error_handler.py +++ b/royalnet/commands/error_handler.py @@ -34,7 +34,9 @@ class ErrorHandlerCommand(Command): await call.reply(f"⚠️ Il bot non è stato configurato correttamente, quindi questo comando non può essere eseguito.\n[p]{exception}[/p]") return if isinstance(exception, RoyalnetRequestError): - await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: [p]{exception.error}[/p]") + await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore:\n" + f"[p]{exception.error.extra_info['type']}\n" + f"{exception.error.extra_info['str']}[/p]") return if isinstance(exception, ExternalError): await call.reply(f"⚠️ Una risorsa esterna necessaria per l'esecuzione del comando non ha funzionato correttamente, quindi il comando è stato annullato.\n[p]{exception}[/p]") diff --git a/royalnet/royalgames.py b/royalnet/royalgames.py index f6175bf9..867a66e7 100644 --- a/royalnet/royalgames.py +++ b/royalnet/royalgames.py @@ -20,7 +20,7 @@ log.setLevel(logging.WARNING) commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand, AuthorCommand, DiarioCommand, RageCommand, DateparserCommand, ReminderCommand, KvactiveCommand, KvCommand, KvrollCommand, VideoinfoCommand, SummonCommand, PlayCommand, SkipCommand, PlaymodeCommand, - VideochannelCommand] + VideochannelCommand, CvCommand] address, port = "127.0.0.1", 1234 diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index 958bd2c7..01bebb7d 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -7,9 +7,8 @@ from .commandargs import CommandArgs from .safeformat import safeformat from .classdictjanitor import cdj from .sleepuntil import sleep_until -from .plusformat import plusformat from .networkhandler import NetworkHandler -from .safefilename import safefilename +from .formatters import andformat, plusformat, fileformat __all__ = ["asyncify", "Call", "Command", "safeformat", "cdj", "sleep_until", "plusformat", "CommandArgs", - "NetworkHandler", "safefilename"] + "NetworkHandler", "andformat", "plusformat", "fileformat"] diff --git a/royalnet/utils/formatters.py b/royalnet/utils/formatters.py new file mode 100644 index 00000000..644c92d3 --- /dev/null +++ b/royalnet/utils/formatters.py @@ -0,0 +1,40 @@ +import typing +import re + + +def andformat(l: typing.List[str], middle=", ", final=" and ") -> str: + """Convert a :py:class:`list` to a :py:class:`str` by adding ``final`` between the last two elements and ``middle`` between the others. + + Parameters: + l: the input :py:class:`list`. + middle: the :py:class:`str` to be added between the middle elements. + final: the :py:class:`str` to be added between the last two elements. + + Returns: + The resulting :py:class:`str`.""" + result = "" + for index, item in enumerate(l): + result += item + if index == len(l) - 2: + result += final + elif index != len(l) - 1: + result += middle + return result + + +def plusformat(i: int) -> str: + """Convert an :py:class:`int` to a :py:class:`str`, adding a ``+`` if they are greater than 0. + + Parameters: + i: the :py:class:`int` to convert. + + Returns: + The resulting :py:class:`str`.""" + if i >= 0: + return f"+{i}" + return str(i) + + +def fileformat(string: str) -> str: + """Ensure a string can be used as a filename by replacing all non-word characters with underscores.""" + return re.sub(r"\W", "_", string) diff --git a/royalnet/utils/plusformat.py b/royalnet/utils/plusformat.py deleted file mode 100644 index 7da0bd83..00000000 --- a/royalnet/utils/plusformat.py +++ /dev/null @@ -1,11 +0,0 @@ -def plusformat(i: int) -> str: - """Convert an :py:class:`int` to a string, adding a plus if they are greater than 0. - - Parameters: - i: the :py:class:`int` to convert. - - Returns: - The resulting :py:class:`str`.""" - if i >= 0: - return f"+{i}" - return str(i) diff --git a/royalnet/utils/safefilename.py b/royalnet/utils/safefilename.py deleted file mode 100644 index 1baa9c0c..00000000 --- a/royalnet/utils/safefilename.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - - -def safefilename(string: str) -> str: - """Ensure a string can be used as a filename by replacing all non-word characters with underscores.""" - return re.sub(r"\W", "_", string)