From 57a7037ce97f76c6096e08f5b9fc0e0b684ea42d Mon Sep 17 00:00:00 2001 From: Siphalor Date: Tue, 6 Jul 2021 16:24:24 +0200 Subject: [PATCH] Migrate a lot of things to slash commands --- bot.py | 920 +++++++++++++++++++++++++++++++++-------------- lib/utils.py | 7 + requirements.txt | 1 + 3 files changed, 649 insertions(+), 279 deletions(-) diff --git a/bot.py b/bot.py index 5322a1c..c0eb4e8 100644 --- a/bot.py +++ b/bot.py @@ -3,17 +3,19 @@ import os import random import re import time -from types import coroutine -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Iterable import discord import gtts from discord.ext import commands +from discord.ext.commands import MemberConverter +from discord_slash import SlashCommand, SlashContext, SlashCommandOptionType +from discord_slash.utils.manage_commands import create_option, create_choice from dotenv import load_dotenv from lib.config import config, config_load, config_save, config_get, config_set, config_get_descriptions, \ - config_set_raw -from lib.utils import async_filter, find_category, find_role_case_insensitive + config_set_raw, config_meta +from lib.utils import async_filter, find_category, find_role_case_insensitive, link_channel load_dotenv() TOKEN = os.getenv('DISCORD_TOKEN') @@ -51,6 +53,7 @@ intents = discord.Intents.default() intents.members = True intents.voice_states = True bot = commands.Bot(command_prefix=config.get('prefix'), intents=intents) +slash = SlashCommand(bot, sync_commands=True) if 'LIBOPUS' in os.environ and not len(os.environ['LIBOPUS']) == 0: discord.opus.load_opus(os.environ['LIBOPUS']) @@ -262,281 +265,6 @@ async def flip_pipelin_command(ctx: commands.Context): await ctx.channel.send("Pipelin " + random.choice(['schnel', 'langsam'])) -@bot.command(name='quote', brief='Get a random quote or add new ones') -async def quote_command(ctx: commands.Context, author: Optional[str], *, quote: Optional[str]): - quotes: Optional[Dict[str, List[str]]] = config_get("guild_quotes", guild=ctx.guild.id) - if quotes is None: - quotes = {} - config_set_raw("guild_quotes", quotes, ctx.guild.id) - - if author is None: - authors = quotes.keys() - if authors: - author = random.choice([*authors]) - author_quotes = quotes[author] - quote = '\n'.join(map(lambda line: '> ' + line, random.choice(author_quotes).splitlines())) - await ctx.channel.send(quote + '\n *~' + author + '*') - else: - await ctx.channel.send("No quotes present!") - elif author == 'list': - def create_list_embed(quote_author: str, _quotes: List[str]) -> discord.Embed: - embed: discord.Embed = discord.Embed( - title=quote_author, - description='\n'.join(_quotes) - ) - return embed - - if quote is None: - coroutines = [] - for author, author_quotes in quotes.items(): - coroutines.append(ctx.send(embed=create_list_embed(author, author_quotes))) - await asyncio.gather(*coroutines) - else: - author = quote.strip().title() - if author in quotes: - await ctx.send(embed=create_list_embed(author, quotes[author])) - else: - await ctx.send("No such author!") - else: - author = author.strip().title() - if quote: - quote = quote.strip() - quote = quote[0].upper() + quote[1::] - if author in quotes: - author_quotes: List[str] = quotes[author] - regex = re.compile(r'\W') - stripped_quote = regex.sub('', quote).lower() - for aq in author_quotes: - if regex.sub('', aq).lower() == stripped_quote: - await ctx.channel.send("Quote already exists!") - return - - author_quotes.append(quote) - config_save() - await ctx.channel.send("Quote added") - - else: - quotes[author] = [quote] - config_save() - await ctx.channel.send("Quote added") - else: - if author in quotes: - quote = random.choice(quotes[author]) - quote = '\n'.join(map(lambda line: '> ' + line, quote.splitlines())) - await ctx.channel.send(quote + '\n *~ ' + author + '*') - else: - await ctx.channel.send("Unknown author!") - - -@bot.command(name='quote_remove', brief='Remove a quote') -async def quote_remove_command(ctx: commands.Context, author: str, quote: str): - if not ctx.author.guild_permissions.administrator: - await ctx.channel.send("Only admins are allowed to remove quotes!") - quotes: Optional[Dict[str, List[str]]] = config_get("guild_quotes", guild=ctx.guild.id) - if quotes is None: - await ctx.channel.send("No quotes available!") - - else: - quote = quote.strip().lower() - author = author.strip().title() - if author in quotes: - for q in quotes[author]: - if q.lower() == quote: - quotes[author].remove(q) - if not quotes[author]: - quotes.pop(author) - await ctx.channel.send("Successfully removed quote!") - config_save() - return - # No matching quote found - try: - quote = int(quote) - quotes[author].pop(quote) - if not quotes[author]: - quotes.pop(author) - await ctx.channel.send("Successfully removed quote!") - config_save() - return - except ValueError: - pass - - await ctx.channel.send("No such quote found!") - - -@bot.command(name='groups', brief="Manage groups") -async def group_command(ctx: commands.Context, subcommand: Optional[str], arg: Optional[str], - members: commands.Greedy[discord.Member]): - if not ctx.author.guild_permissions.manage_roles: - await ctx.send("Access denied!") - return - if subcommand is None: - await ctx.send("Available commands: `list`, `create `, " - "`archive `, `unarchive `, `delete `, `clear_vcs`") - return - - guild: discord.Guild = ctx.guild - role_prefix = config_get("groups-role-prefix", guild.id) - - def collect_group_channels(cat: discord.CategoryChannel) -> Dict[str, discord.abc.GuildChannel]: - return {channel.name: channel for channel in cat.text_channels} - - async def collect_group_roles() -> List[discord.Role]: - return list(filter(lambda role: role.name.startswith(role_prefix), await guild.fetch_roles())) - - async def fail_category(type: str, expected: str): - await ctx.send("Unable to find channel category \"" + expected + "\" for " + type + ". Change in configs.") - - def link_channel(channel: discord.abc.GuildChannel, italic: bool = False) -> str: - if italic: - return '[*' + channel.name + '*](https://discord.com/channels/' + str(guild.id) + '/' + str( - channel.id) + ')' - return '[' + channel.name + '](https://discord.com/channels/' + str(guild.id) + '/' + str(channel.id) + ')' - - groups_cat = find_category(guild, config_get("groups-category", guild.id)) - if groups_cat is None: - await fail_category("groups", config_get("groups-category", guild.id)) - return - - if subcommand == 'list': - archive_cat = find_category(guild, config_get("groups-archive-category", guild.id)) - if archive_cat is None: - await fail_category("archive", config_get("groups-archive-category", guild.id)) - return - - active_groups = collect_group_channels(groups_cat) - archived_groups = collect_group_channels(archive_cat) - - msg = "" - for role in await collect_group_roles(): - name = role.name[len(role_prefix):].lower() - if name in active_groups: - msg += link_channel(active_groups[name]) + "\n" - elif name in archived_groups: - msg += link_channel(archived_groups[name], True) + "\n" - else: - msg += "*" + name + "*\n" - - embed = discord.Embed(title="Groups", description=msg) - embed.set_footer(text="Italic groups are archived or unavailable.") - await ctx.send(embed=embed) - elif subcommand == 'archive': - if arg is None: - await ctx.send("Group name required!") - return - - channel_name = arg.lower() - groups = collect_group_channels(groups_cat) - if channel_name in groups: - archive_cat = find_category(guild, config_get("groups-archive-category", guild.id)) - if archive_cat is None: - await fail_category("archive", config_get("groups-archive-category", guild.id)) - return - await groups[channel_name].edit(reason="Archive group " + arg, category=archive_cat) - if channel_name + "_vc" in groups: - await groups[channel_name + "_vc"].delete(reason="Archive group" + arg) - - await ctx.send("Group " + arg + " archived.") - - else: - await ctx.send("Can't find that group!") - elif subcommand == 'unarchive': - if arg is None: - await ctx.send("Group name required!") - return - - channel_name = arg.lower() - archive_cat = find_category(guild, config_get("groups-archive-category", guild.id)) - if archive_cat is None: - await fail_category("archive", config_get("groups-archive-category", guild.id)) - return - archived_groups = collect_group_channels(archive_cat) - - if channel_name in archived_groups: - await archived_groups[channel_name].edit(reason="Archive group " + arg, category=groups_cat) - await ctx.send("Group " + arg + " unarchived.") - - else: - await ctx.send("Can't find that group!") - elif subcommand == 'delete': - if arg is None: - await ctx.send("Group name required!") - return - - archive_cat = find_category(guild, config_get("groups-archive-category", guild.id)) - if archive_cat is None: - await fail_category("archive", config_get("groups-archive-category", guild.id)) - return - - role = await find_role_case_insensitive(guild, arg, role_prefix) - if role is not None: - await role.delete(reason="Delete group " + arg) - - groups = collect_group_channels(groups_cat) - groups.update(collect_group_channels(archive_cat)) - - channel_name = arg.lower() - if channel_name in groups: - await groups[channel_name].delete(reason="Delete group " + arg) - if channel_name + "_vc" in groups: - await groups[channel_name + "_vc"].delete(reason="Delete group " + arg) - - await ctx.send("Group " + arg + " deleted.") - elif subcommand == 'create': - if arg is None: - await ctx.send("Group name required!") - return - - arg.strip() - - cor = groups_cat.create_text_channel(arg.lower(), reason="Create group " + arg) - cor_role = guild.create_role(name=config_get("groups-role-prefix", guild.id) + arg, - mentionable=True, reason="Create group " + arg) - channel: discord.TextChannel = await cor - cor = channel.edit(sync_permissions=True) - role: discord.Role = await cor_role - await cor - await channel.set_permissions(role, reason="Create group " + arg, read_messages=True) - if members: - for member in members: - await member.add_roles(role, reason="Create group " + arg) - await asyncio.sleep(5) - await channel.send("Hi, " + role.mention) - await ctx.send("Group " + arg + " created.") - elif subcommand == 'clear_vcs': - cors: List[coroutine] = [] - for vc in groups_cat.voice_channels: - cors.append(vc.delete(reason="Clear temporary vcs, as requested by " + ctx.author.mention)) - for cor in cors: - await cor - - -@bot.command(name='groupvc', brief='Creates a temporary vc for the current group text channel') -async def groupvc_command(ctx: commands.Context): - if ctx.channel.category is not None: - guild: discord.Guild = ctx.guild - channel: discord.TextChannel = ctx.channel - if channel.category.name.lower() == config_get("groups-category", guild.id).lower(): - role = await find_role_case_insensitive(guild, channel.name, config_get("groups-role-prefix", guild.id)) - if role is None: - await ctx.send("Couldn't resolve group role!") - return - - category: discord.CategoryChannel = channel.category - for vc in category.voice_channels: - if vc.name.lower() == channel.name.lower() + "_vc": - await ctx.send("Temporary group channel already exists.") - return - - reason = "Create temporary vc for group " + channel.name - vc = await category.create_voice_channel(channel.name + "_vc", reason=reason) - await vc.edit(sync_permissions=True, reason=reason) - await vc.set_permissions(role, view_channel=True, connect=True, reason=reason) - await ctx.send("Created temporary vc for this group") - return - - await ctx.send("Not an active group channel!") - - def _is_message_valid_for_selection(message: discord.Message, reaction_filter: Optional[str] = None) -> bool: if message.clean_content.strip() == '': return False @@ -599,4 +327,638 @@ async def config_prefix_command(ctx: commands.Context, cmd: str = '', key: str = await ctx.send('Unknown command!') +# ------------------- The slashy way of life ------------------- # + +slash_guild_ids = None +config_choices = [create_choice(name=entry[1][1], value=entry[0]) for entry in config_meta.items() if entry[1][0]] +config_option = create_option("config_key", "A key identifying the config entry to target", str, True, config_choices) + + +async def check_slash_context(ctx: SlashContext, require_op: bool = False, require_manage_roles: bool = False) -> bool: + if ctx.guild is None: + await ctx.send('You can\'t run config commands in private messages!') + return False + + if require_op and not ctx.author.guild_permissions.administrator: + await ctx.send('You\'re not allowed to run this command on this server!', hidden=True) + return False + + if require_manage_roles and not ctx.author.guild_permissions.manage_roles: + await ctx.send('You\'re not allowed to run this command on this server!', hidden=True) + return False + + return True + + +async def check_text_channel(ctx: SlashContext, channel) -> bool: + if not isinstance(channel, discord.TextChannel): + await ctx.send('The given channel is not a text channel!', hidden=True) + return False + return True + + +# ____ ____ ___ _ _ ____ ____ +# / ___| _ \ / _ \| | | | _ \/ ___| +# | | _| |_) | | | | | | | |_) \___ \ +# | |_| | _ <| |_| | |_| | __/ ___) | +# \____|_| \_\\___/ \___/|_| |____/ + +def get_groups_role_prefix(guild_id: int) -> str: + return config_get("groups-role-prefix", guild_id) + + +async def get_groups_category(ctx: SlashContext) -> Optional[discord.CategoryChannel]: + category = find_category(ctx.guild, config_get("groups-category", ctx.guild_id)) + if category is None: + await ctx.send("Failed to locate group category!") + return None + return category + + +async def get_groups_archive_category(ctx: SlashContext) -> Optional[discord.CategoryChannel]: + category = find_category(ctx.guild, config_get("groups-archive-category", ctx.guild_id)) + if category is None: + await ctx.send("Failed to locate group category!") + return None + return category + + +async def get_groups_categories(ctx: SlashContext) -> (discord.CategoryChannel, discord.CategoryChannel): + groups_cat = await get_groups_category(ctx) + if groups_cat is None: + return None + archive_cat = await get_groups_archive_category(ctx) + if archive_cat is None: + return None + return groups_cat, archive_cat + + +def collect_group_channels(cat: discord.CategoryChannel) -> Dict[str, discord.abc.GuildChannel]: + return {channel.name: channel for channel in cat.text_channels} + + +async def collect_group_roles(guild: discord.Guild) -> List[discord.Role]: + role_prefix = get_groups_role_prefix(guild.id) + return list(filter(lambda role: role.name.startswith(role_prefix), await guild.fetch_roles())) + + +@slash.subcommand( + base="groups", + name="list", + description="Lists all groups on this server", + guild_ids=slash_guild_ids +) +async def groups_list_slash(ctx: SlashContext): + if not await check_slash_context(ctx, require_manage_roles=True): + return + + categories = await get_groups_categories(ctx) + if categories is None: + return + groups_cat, archive_cat = categories + + active_groups = collect_group_channels(groups_cat) + archived_groups = collect_group_channels(archive_cat) + + role_prefix = get_groups_role_prefix(ctx.guild_id) + msg = "" + for role in await collect_group_roles(ctx.guild): + name = role.name[len(role_prefix):].lower() + if name in active_groups: + msg += link_channel(active_groups[name]) + "\n" + elif name in archived_groups: + msg += link_channel(archived_groups[name], True) + "\n" + else: + msg += "*" + name + "*\n" + + embed = discord.Embed(title="Groups", description=msg) + embed.set_footer(text="Italic groups are archived or unavailable.") + await ctx.send(embed=embed) + + +@slash.subcommand( + base="groups", + name="archive", + description="Move a group to the archive", + options=[ + create_option( + name="group_channel", + description="The group identified by it's channel", + option_type=discord.abc.GuildChannel, + required=True + ) + ], + guild_ids=slash_guild_ids +) +async def groups_archive_slash(ctx: SlashContext, group_channel: discord.abc.GuildChannel): + if not await check_slash_context(ctx, require_manage_roles=True): + return + if not await check_text_channel(ctx, group_channel): + return + + categories = await get_groups_categories(ctx) + if categories is None: + return + groups_cat, archive_cat = categories + + group_name = group_channel.name + groups = collect_group_channels(groups_cat) + if group_channel.category == groups_cat: + await group_channel.edit(reason="Archive group " + group_name, category=archive_cat) + if group_name + "_vc" in groups: + await groups[group_name + "_vc"].delete(reason="Archive group " + group_name) + + await ctx.send("Group " + group_name + " archived.") + elif group_channel.category == archive_cat: + await ctx.send("Group " + group_name + " is already archived.", hidden=True) + else: + await ctx.send(group_name + " is not a group") + + +@slash.subcommand( + base="groups", + name="unarchive", + description="Move a group from the archive to the main category", + options=[ + create_option( + name="group_channel", + description="The group identified by it's channel", + option_type=discord.abc.GuildChannel, + required=True + ) + ], + guild_ids=slash_guild_ids +) +async def groups_archive_slash(ctx: SlashContext, group_channel: discord.abc.GuildChannel): + if not await check_slash_context(ctx, require_manage_roles=True): + return + if not await check_text_channel(ctx, group_channel): + return + + categories = await get_groups_categories(ctx) + if categories is None: + return + groups_cat, archive_cat = categories + + group_name = group_channel.name + if group_channel.category == archive_cat: + await ctx.defer() + await group_channel.edit(reason="Archive group " + group_name, category=archive_cat) + await ctx.send("Group " + group_name + " archived.") + elif group_channel.category == groups_cat: + await ctx.send("Group " + group_name + " is not archived.", hidden=True) + else: + await ctx.send(group_name + " is not a group") + + +@slash.subcommand( + base="groups", + name="delete", + description="Move a group from the archive to the main category", + options=[ + create_option( + name="group_channel", + description="The group identified by it's channel", + option_type=discord.abc.GuildChannel, + required=True + ) + ], + guild_ids=slash_guild_ids +) +async def groups_delete_slash(ctx: SlashContext, group_channel: discord.abc.GuildChannel): + if not await check_slash_context(ctx, require_manage_roles=True): + return + if not await check_text_channel(ctx, group_channel): + return + group_channel: discord.TextChannel = group_channel + group_name = group_channel.name + + await ctx.defer() + + role = await find_role_case_insensitive(ctx.guild, group_channel.name, get_groups_role_prefix(ctx.guild_id)) + if role is not None: + await role.delete(reason="Delete group " + group_name) + + if group_channel.category: + vcs: List[discord.VoiceChannel] = group_channel.category.voice_channels + for vc in vcs: + if vc.name == group_name + "_vc": + await vc.delete(reason="Delete group " + group_name) + break + + await group_channel.delete(reason="Delete group " + group_name) + await ctx.send("Group " + group_name + " deleted.") + + +@slash.subcommand( + base="groups", + name="create", + description="Creates a new group channel and role", + options=[ + create_option( + name="name", + description="The name of the group", + option_type=str, + required=True + ), + create_option( + name="members", + description="A comma-separated list of members", + option_type=str, + required=False + ) + ], + guild_ids=slash_guild_ids +) +async def groups_create_slash(ctx: SlashContext, name: str, members: Optional[str] = None): + if not await check_slash_context(ctx, require_manage_roles=True): + return + await ctx.defer() + name = name.strip() + + groups_cat = await get_groups_category(ctx) + if not groups_cat: + return + + cor = groups_cat.create_text_channel(name.lower(), reason="Create grooup " + name) + cor_role = ctx.guild.create_role(name=config_get("groups-role-prefix", ctx.guild_id) + name.lower(), + mentionable=True, reason="Create group " + name) + channel: discord.TextChannel = await cor + cor = channel.edit(sync_permissions=True) + role: discord.Role = await cor_role + await cor + await channel.set_permissions(role, reason="Create group " + name, read_messages=True) + + extra = "" + if members is not None: + members = members.split(',') + converter = MemberConverter() + for member_str in members: + member_str = member_str.strip() + try: + member = await converter.convert(ctx, member_str) + await member.add_roles(role, reason="Create group " + name) + except discord.ext.commands.MemberNotFound: + extra += '\n*' + member_str + '*' + if extra: + extra = '\nCouldn\'t resolve users:' + extra + + await asyncio.sleep(3) + await channel.send("Hi, " + role.mention) + await ctx.send("Group " + name + " created." + extra) + + +@slash.subcommand( + base="groups", + name="remove-vc", + description="Removes a temporary voice channel associated with the current group", + guild_ids=slash_guild_ids +) +async def groups_remove_vc_slash(ctx: SlashContext): + if not await check_slash_context(ctx): + return + + groups_cat = await get_groups_category(ctx) + if not groups_cat: + return + + if ctx.channel.category == groups_cat: + vc: Optional[discord.VoiceChannel] = discord.utils.find( + lambda _vc: _vc.name == ctx.channel.name + '_vc', + ctx.channel.category.voice_channels + ) + if vc is None: + await ctx.send("Voice chat is not open!", hidden=True) + else: + await ctx.defer(hidden=True) + await vc.delete(reason="Delete temporary voice chat") + await ctx.send("Removed temporary voice chat.", hidden=True) + + else: + await ctx.send("This channel is not an active group!", hidden=True) + + +@slash.subcommand( + base="groups", + name="vc", + description="Create a temporary voice chat. Available to anyone.", + guild_ids=slash_guild_ids +) +async def groups_vc_slash(ctx: SlashContext): + if ctx.channel.category is not None: + guild: discord.Guild = ctx.guild + channel: discord.TextChannel = ctx.channel + if channel.category.name.lower() == config_get("groups-category", guild.id).lower(): + role = await find_role_case_insensitive(guild, channel.name, config_get("groups-role-prefix", guild.id)) + if role is None: + await ctx.send("Couldn't resolve group role!", hidden=True) + return + + category: discord.CategoryChannel = channel.category + for vc in category.voice_channels: + if vc.name.lower() == channel.name.lower() + "_vc": + await ctx.send("Temporary group channel already exists.", hidden=True) + return + + await ctx.defer() + reason = "Create temporary vc for group " + channel.name + vc = await category.create_voice_channel(channel.name + "_vc", reason=reason) + await vc.edit(sync_permissions=True, reason=reason) + await vc.set_permissions(role, view_channel=True, connect=True, reason=reason) + await ctx.send("Created temporary vc for this group") + return + + await ctx.send("Not an active group channel!", hidden=True) + + +# ___ _ _ ___ _____ _____ ____ +# / _ \| | | |/ _ \_ _| ____/ ___| +# | | | | | | | | | || | | _| \___ \ +# | |_| | |_| | |_| || | | |___ ___) | +# \__\_\\___/ \___/ |_| |_____|____/ + +def get_quotes(guild_id: int) -> Dict[str, List[str]]: + quotes: Optional[Dict[str, List[str]]] = config_get("guild_quotes", guild=guild_id) + if quotes is None: + quotes = {} + config_set_raw("guild_quotes", quotes, guild_id) + return quotes + + +@slash.subcommand( + base="quotes", + name="random", + description="Gets a random quote, optionally for the given author", + options=[ + create_option( + name="author", + description="The author to filter by", + option_type=str, + required=False + ) + ], + guild_ids=slash_guild_ids +) +async def quote_random_slash(ctx: SlashContext, author: Optional[str] = None): + if not await check_slash_context(ctx, False): + return + + quotes = get_quotes(ctx.guild_id) + + if author is None: + authors = quotes.keys() + if authors: + author = random.choice([*authors]) + else: + await ctx.send('No quotes available') + return + + if author not in quotes: + await ctx.send('No such author!', hidden=True) + return + + author_quotes = quotes[author] + quote = '\n'.join(map(lambda line: '> ' + line, random.choice(author_quotes).splitlines())) + await ctx.send(quote + '\n *~' + author + '*') + + +@slash.subcommand( + base="quotes", + name="list", + description="Lists all authors or all quotes by a given author", + options=[ + create_option( + name="author", + description="The author to filter by, use * for all authors", + option_type=str, + required=False + ) + ], + guild_ids=slash_guild_ids +) +async def quote_list_slash(ctx: SlashContext, author: Optional[str] = None): + if not await check_slash_context(ctx, False): + return + + def create_list_embed(title: str, items: Iterable[str]) -> discord.Embed: + embed: discord.Embed = discord.Embed( + title=title, + description='\n'.join(items) + ) + return embed + + quotes = get_quotes(ctx.guild_id) + + if author is None: + await ctx.send(embed=create_list_embed("Available authors", quotes.keys())) + elif author == '*': + coroutines = [] + await ctx.defer() + for author, author_quotes in quotes.items(): + coroutines.append(ctx.send(embed=create_list_embed(author, author_quotes))) + await asyncio.gather(*coroutines) + else: + author = author.strip().title() + if author in quotes: + await ctx.send(embed=create_list_embed(author, quotes[author])) + else: + await ctx.send("No such author: \"" + author + "\"", hidden=True) + + +@slash.subcommand( + base="quotes", + name="add", + description="Add a new quote", + options=[ + create_option( + name="author", + description="The author of the new quote", + option_type=str, + required=True + ), + create_option( + name="quote", + description="The quote to add", + option_type=str, + required=True + ) + ], + guild_ids=slash_guild_ids +) +async def quote_add_slash(ctx: SlashContext, author: str, quote: str): + if not await check_slash_context(ctx, False): + return + + ignored_character_regex = re.compile(r'\W') + + def prepare_quote_for_compare(_quote: str) -> str: + return ignored_character_regex.sub('', _quote).lower() + + quotes = get_quotes(ctx.guild_id) + + author = author.strip().title() + quote = quote.strip() + quote = quote[0].upper() + quote[1::] + if author in quotes: + author_quotes: List[str] = quotes[author] + stripped_quote = prepare_quote_for_compare(quote) + for aq in author_quotes: + if prepare_quote_for_compare(aq) == stripped_quote: + await ctx.send("Quote already exists!", hidden=True) + return + + author_quotes.append(quote) + config_save() + await ctx.send("Quote added.") + + else: + quotes[author] = [quote] + config_save() + await ctx.send("Author and quote added") + + +@slash.subcommand( + base="quotes", + name="remove", + description="Removes a quote. Requires administrator permissions.", + options=[ + create_option( + name="author", + description="The author to delete from", + option_type=str, + required=True + ), + create_option( + name="quote", + description="The exact quote to remove", + option_type=str, + required=False + ), + create_option( + name="quote_position", + description="The quote position to delete", + option_type=SlashCommandOptionType.INTEGER, + required=False + ) + ], + guild_ids=slash_guild_ids +) +async def quote_remove_slash(ctx: SlashContext, author: str, quote: Optional[str] = None, quote_position: Optional[int] = None): + if not await check_slash_context(ctx, True): + return + + quotes = get_quotes(ctx.guild_id) + + if quotes is None: + await ctx.send("No quotes available!", hidden=True) + else: + author = author.strip().title() + if author not in quotes: + await ctx.send("No quotes from author \"" + author + "\"!", hidden=True) + return + + author_quotes = quotes[author] + + if quote is not None: + quote = quote.strip().lower() + for aq in author_quotes: + if aq.lower() == quote: + author_quotes.remove(aq) + if not author_quotes: + quotes.pop(author) + + await ctx.send("Removed quote from author \"" + author + "\"") + config_save() + return + + await ctx.send("No such quote found!", hidden=True) + + elif quote_position is not None: + if 0 <= quote_position < len(author_quotes): + author_quotes.pop(quote_position) + if not author_quotes: + quotes.pop(author) + + await ctx.send("Removed quote from author \"" + author + "\"") + config_save() + return + + await ctx.send("Invalid quote position - must be within the range of 0 and " + str(len(author_quotes)) + "!", hidden=True) + + else: + await ctx.send("Either quote or quote_position must be used!", hidden=True) + + +# ____ ___ _ _ _____ ___ ____ +# / ___/ _ \| \ | | ___|_ _/ ___| +# | | | | | | \| | |_ | | | _ +# | |__| |_| | |\ | _| | | |_| | +# \____\___/|_| \_|_| |___\____| + + +@slash.slash( + name="config", + description="Change or query the configuration of this bot", + guild_ids=slash_guild_ids +) +async def config_slash(ctx: SlashContext): + await ctx.send("missingno", hidden=True) + + +@slash.subcommand( + base="config", + name="list", + description="List available config options", + guild_ids=slash_guild_ids +) +async def config_list_slash(ctx: SlashContext): + msg = 'Available config options:' + for (key, value) in config_get_descriptions(): + msg += '\n - `' + key + '`: ' + value[1] + await ctx.send(msg, hidden=True) + + +@slash.subcommand( + base="config", + name="get", + description="Queries the configuration for a value", + options=[ + config_option + ], + guild_ids=slash_guild_ids +) +async def config_get_slash(ctx: SlashContext, config_key: str): + if not await check_slash_context(ctx, False): + return + if config_key in config_meta and config_meta[config_key][0]: + await ctx.send('`' + config_key + '` is set to `' + str(config_get(config_key, ctx.guild.id)) + '`', + hidden=True) + else: + await ctx.send('Unknown config option `' + config_key + '`', hidden=True) + + +@slash.subcommand( + base="config", + name="set", + description="Changes a configuration entry", + options=[ + config_option, + create_option( + name="value", + description="The value to set", + option_type=str, + required=True + ) + ], + guild_ids=slash_guild_ids +) +async def config_set_slash(ctx: SlashContext, config_key: str, value: str): + if not await check_slash_context(ctx, True): + return + if ctx.author.guild_permissions.administrator: + await ctx.send(config_set(config_key, value, ctx.guild.id), hidden=True) + else: + await ctx.send('You\'re not allowed to change the configuration on this server!', hidden=True) + + bot.run(TOKEN) diff --git a/lib/utils.py b/lib/utils.py index 1f18fa5..58e3b4d 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -22,3 +22,10 @@ async def find_role_case_insensitive(guild: discord.Guild, name: str, prefix: st if name.strip().lower() == role.name[len(prefix):].lower(): return role return None + + +def link_channel(channel: discord.abc.GuildChannel, italic: bool = False) -> str: + if italic: + return '[*' + channel.name + '*](https://discord.com/channels/' + str(channel.guild.id) + '/' + str( + channel.id) + ')' + return '[' + channel.name + '](https://discord.com/channels/' + str(channel.guild.id) + '/' + str(channel.id) + ')' diff --git a/requirements.txt b/requirements.txt index fc6b552..193e091 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ discord +discord-py-slash-command>=2 python-dotenv PyNaCl ffmpeg