From c54e7af00a7ac1cf88a61e2ab31ad5e0cc77b0b1 Mon Sep 17 00:00:00 2001 From: Elnath Date: Mon, 4 Jan 2021 22:31:06 +0100 Subject: [PATCH] Make users able to set a default name for their channels --- ChannelsConfigFile/ChannelsConfigFile.py | 71 +++++++++++++++++++++++- ChannelsConfigFile/v1_0Tov1_1.py | 21 +++++++ VocalMaisBot.py | 44 ++++++++++++++- utils/discord_utils.py | 23 ++++++++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 ChannelsConfigFile/v1_0Tov1_1.py diff --git a/ChannelsConfigFile/ChannelsConfigFile.py b/ChannelsConfigFile/ChannelsConfigFile.py index 7266092..1334811 100644 --- a/ChannelsConfigFile/ChannelsConfigFile.py +++ b/ChannelsConfigFile/ChannelsConfigFile.py @@ -2,19 +2,21 @@ import atexit import json import logging from pathlib import Path -from typing import Union, Dict, List, Callable, Tuple +from typing import Union, Dict, List, Callable, Tuple, Any, Optional import discord import utils from . import vNoneTov1_0 +from . import v1_0Tov1_1 logger = logging.getLogger(__name__) # Functions used to convert between configuration versions, in a dictionary of: old_version -> converter # Each converter takes the old configuration and returns the converted configuration and the new version config_version_converters: Dict[Union[None, str], Callable[[Dict], Tuple[Dict, str]]] = { - None: vNoneTov1_0.convert + None: vNoneTov1_0.convert, + "1.0": v1_0Tov1_1.convert, } @@ -22,7 +24,7 @@ class ChannelsConfigFile: """ Wrapper for the channels configuration file, telling which channels to watch and which channels were created by the bot. """ - version = "1.0" + version = "1.1" @staticmethod def empty_config() -> Dict: @@ -121,6 +123,7 @@ class ChannelsConfigFile: "_name": guild.name, "watched_channels": [], "created_channels": [], + "user_config": {}, } return self.config[str_guild_id] @@ -202,3 +205,65 @@ class ChannelsConfigFile: :return: Whether the given channel is one of the channels that have been created by the bot """ return channel.id in self._get_created_channels(guild) + + def _get_user_configs(self, guild: discord.Guild) -> Dict[str, Dict]: + """ + :return: The configuration for all users in a guild + """ + guild_configuration = self._get_guild_configuration(guild) + if "user_config" not in guild_configuration: + guild_configuration["user_config"] = {} + return guild_configuration["user_config"] + + def _get_user_config(self, guild: discord.Guild, user: discord.Member) -> Dict[str, Any]: + """ + :return: The configuration for a user in a guild + """ + all_users_configs = self._get_user_configs(guild) + user_id_str = str(user.id) + if user_id_str not in all_users_configs: + all_users_configs[user_id_str] = { + "__username": f"{user.name}#{user.discriminator}", + "channel_name": None, + } + return all_users_configs[user_id_str] + + def _clean_user_config_if_useless(self, guild: discord.Guild, user: discord.Member): + """ + If some of the information for a user in the user config of a guild is useless, remove it. + If the whole entry for a user in the user config of a guild is useless (i.e. it contains only its username), delete it. + """ + all_users_config = self._get_user_configs(guild) + user_id_str = str(user.id) + user_config = all_users_config[user_id_str] if user_id_str in all_users_config else {} + # If the default channel name for this user is set to None, it is the same as not being set + if "channel_name" in user_config and user_config["channel_name"] is None: + del user_config["channel_name"] + # If the only information remaining is the username (or nothing), delete the entry + if list(user_config.keys()) in ([], ["__username"]): + del all_users_config[user_id_str] + logger.debug(f"[{guild.name}({guild.id})] Deleted user {user.display_name}({user.id}) configuration because it was useless") + + def set_user_channel_name(self, guild: discord.Guild, user: discord.Member, channel_name: str) -> None: + """ + Set the initial name for the channels created for a specific user + """ + user_config = self._get_user_config(guild, user) + user_config["channel_name"] = channel_name + + def get_user_channel_name(self, guild: discord.Guild, user: discord.Member) -> Optional[str]: + """ + :return: The initial name for the channels created for a specific user, if the user set one + """ + user_config = self._get_user_config(guild, user) + channel_name = user_config["channel_name"] if "channel_name" in user_config else None + self._clean_user_config_if_useless(guild, user) + return channel_name + + def clear_user_channel_name(self, guild: discord.Guild, user: discord.Member): + """ + Remove the user preferences about the names for their channels + """ + user_config = self._get_user_config(guild, user) + user_config["channel_name"] = None + self._clean_user_config_if_useless(guild, user) diff --git a/ChannelsConfigFile/v1_0Tov1_1.py b/ChannelsConfigFile/v1_0Tov1_1.py new file mode 100644 index 0000000..d640cd6 --- /dev/null +++ b/ChannelsConfigFile/v1_0Tov1_1.py @@ -0,0 +1,21 @@ +""" +Converter for converting ChannelsConfigFile from version 1.0 to version 1.1 +""" + +from typing import Dict, Tuple + + +def convert(config: Dict) -> Tuple[Dict, str]: + assert config["__version__"] == "1.0" + + for config_key, config_value in config.items(): + # We skip the version information + if config_key == "__version__": + continue + # All the other entries are guild configuration + guild_config = config_value + if "user_config" not in guild_config: + guild_config["user_config"] = {} + + config["__version__"] = "1.1" + return config, "1.1" diff --git a/VocalMaisBot.py b/VocalMaisBot.py index c11a61a..25934d5 100755 --- a/VocalMaisBot.py +++ b/VocalMaisBot.py @@ -3,6 +3,7 @@ import argparse import logging import sys from pathlib import Path +from typing import List import discord import discord.utils @@ -150,6 +151,44 @@ class VocalMaisBot(commands.Cog): self.channels_config.clear_watched_channels(ctx.guild) await ctx.send(":white_check_mark: I am no longer watching any channel") + @commands.command( + "set_name", + brief = "Set the default name for your voice channels", + help = "Set the default name for your voice channels. If you put {user} in the name, it will be replaced by your nickname", + usage = "" + ) + async def set_user_default_channel_name(self, ctx: commands.Context, *name_elements: str): + # Verify that the given name does not mention anyone by checking the message mentions + if len(set(ctx.message.mentions) - {self.bot.user}) > 0: + await ctx.send(":no_entry: User mentions are not allowed in channel names. Use {user} if you want to include your name.") + return + + name = " ".join(name_elements) + if len(name) == 0: + await ctx.send(f":x: Missing channel name! {self.get_command_usage_message(ctx.command)}") + raise discord_utils.CheckFailDoNotNotify + + self.channels_config.set_user_channel_name(ctx.guild, ctx.author, discord_utils.voice_channel_safe_name(name)) + self.channels_config.save_to_file() + # We escape the markdown from the channel name when sending the message because for now discord does not support markdown in channel names, + # so we do not want the users to get their hopes up by seeing markdown working in the message + await ctx.send(f":white_check_mark: The default name for your channels has been set! If you create one now, it will appear as {discord_utils.voice_channel_safe_name(name, user_name_replacement = ctx.author.display_name, escape_markdown = True)}") + + @commands.command("get_name", help = "Get the default name for your voice channels") + async def get_user_default_channel_name(self, ctx: commands.Context): + channel_name = self.channels_config.get_user_channel_name(ctx.guild, ctx.author) + if channel_name is None: + await ctx.send(":person_shrugging: You have not set a special name for your voice channels") + else: + channel_name = discord_utils.voice_channel_safe_name(channel_name, user_name_replacement = ctx.author.display_name, escape_markdown = True) + await ctx.send(f":memo: If you create a voice channel right now, it will be named like this: {channel_name}") + + @commands.command("clear_name", help = "Do not use a special name for your voice channels") + async def clear_user_default_channel_name(self, ctx: commands.Context): + self.channels_config.clear_user_channel_name(ctx.guild, ctx.author) + self.channels_config.save_to_file() + await ctx.send(":white_check_mark: Your voice channels will use the default naming convention") + @commands.Cog.listener() async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): if before.channel is not None: # They left a channel @@ -173,7 +212,10 @@ class VocalMaisBot(commands.Cog): channel_permissions[user].manage_channels = True # We allow the user for which we created the channel to change the channel's name # Computing the channel name - channel_name = f"{user.display_name}'s channel" + user_default_channel_name = self.channels_config.get_user_channel_name(channel.guild, user) + if user_default_channel_name is None: + user_default_channel_name = "{user}'s channel" + channel_name = discord_utils.voice_channel_safe_name(user_default_channel_name, user_name_replacement = user.display_name) # Creating the channel and moving the user into it user_channel = await category.create_voice_channel(channel_name, overwrites = channel_permissions) diff --git a/utils/discord_utils.py b/utils/discord_utils.py index 78c2e47..afd8945 100644 --- a/utils/discord_utils.py +++ b/utils/discord_utils.py @@ -1,3 +1,4 @@ +import discord import discord.ext.commands as commands @@ -7,3 +8,25 @@ class CheckFailDoNotNotify(commands.CheckFailure): For example it can be used if the check function already sent the user a customised error message. """ pass + + +def voice_channel_safe_name(template: str, max_chars: int = 100, escape_mentions: bool = True, escape_markdown: bool = False, user_name_replacement: str = None) -> str: + """ + Make a name that is suitable for naming voice channels from a template (e.g. found in the config file or given by a user) + :param template: The template used as basis for the name + :param max_chars: The name is ellipsed if it exceeds this number of characters. For a discord voice channel, it is currently 100 chars + :param escape_mentions: If true, escape role and user mentions + :param escape_markdown: If true, escape discord's markdown. For now, discord does not support markdown in channel names anyways + :param user_name_replacement: If not None, replace occurrences of '{user}' in the message with this value + """ + message = template + if user_name_replacement is not None: + message = message.replace("{user}", user_name_replacement) + if escape_mentions: + message = discord.utils.escape_mentions(message) + if escape_markdown: + message = discord.utils.escape_markdown(message) + if len(message) > max_chars: + message = message[:max_chars - 3] + "..." + + return message