diff --git a/bot.py b/bot.py index 7aa120fdad..edc2e4b319 100644 --- a/bot.py +++ b/bot.py @@ -377,6 +377,10 @@ async def wait_for_connected(self) -> None: def snippets(self) -> typing.Dict[str, str]: return self.config["snippets"] + @property + def args(self) -> typing.Dict[str, str]: + return self.config["args"] + @property def aliases(self) -> typing.Dict[str, str]: return self.config["aliases"] diff --git a/cogs/modmail.py b/cogs/modmail.py index cbab46bcb0..f23ad93332 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -25,6 +25,10 @@ logger = getLogger(__name__) +# Arg names reserved by formatreply commands (channel, recipient, author). +RESERVED_ARG_NAMES = {"channel", "recipient", "author"} + + class Modmail(commands.Cog): """Commands directly related to Modmail functionality.""" @@ -209,7 +213,7 @@ async def snippet(self, ctx, *, name: str.lower = None): When `{prefix}snippet` is used by itself, this will retrieve a list of snippets that are currently set. `{prefix}snippet-name` will show what the - snippet point to. + snippet points to. To create a snippet: - `{prefix}snippet add snippet-name A pre-defined text.` @@ -535,6 +539,214 @@ async def snippet_rename(self, ctx, name: str.lower, *, value): embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def args(self, ctx, *, name: str.lower = None): + """ + Create dynamic args for use in replies. + + When `{prefix}args` is used by itself, this will retrieve + a list of args that are currently set. `{prefix}args name` will show what the + arg points to. + + To create an arg: + - `{prefix}args add arg-name A value.` + + You can use your arg in a reply with `{arg-name}`. + """ + + if name is not None: + if name == "compact": + embeds = [] + + for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.args)),) * 15)): + description = format_description(i, names) + embed = discord.Embed(color=self.bot.main_color, description=description) + embed.set_author(name="Args", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) + embeds.append(embed) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + return + + if name not in self.bot.args: + embed = create_not_found_embed(name, self.bot.args.keys(), "Arg") + else: + val = self.bot.args[name] + embed = discord.Embed( + title=f'Arg - "{name}":', + description=val, + color=self.bot.main_color, + ) + return await ctx.send(embed=embed) + + if not self.bot.args: + embed = discord.Embed( + color=self.bot.error_color, + description="You dont have any args at the moment.", + ) + embed.set_footer(text=f'Check "{self.bot.prefix}help args add" to add an arg.') + embed.set_author( + name="Args", + icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128), + ) + return await ctx.send(embed=embed) + + embeds = [discord.Embed(color=self.bot.main_color) for _ in range((len(self.bot.args) // 10) + 1)] + for embed in embeds: + embed.set_author(name="Args", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) + + for i, arg in enumerate(sorted(self.bot.args.items())): + embeds[i // 10].add_field(name=arg[0], value=return_or_truncate(arg[1], 350), inline=False) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + + @args.command(name="raw") + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def args_raw(self, ctx, *, name: str.lower): + """ + View the raw content of an arg. + """ + if name not in self.bot.args: + embed = create_not_found_embed(name, self.bot.args.keys(), "Arg") + else: + val = truncate(escape_code_block(self.bot.args[name]), 2048 - 7) + embed = discord.Embed( + title=f'Raw arg - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color, + ) + + return await ctx.send(embed=embed) + + @args.command(name="add", aliases=["create", "make"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def args_add(self, ctx, name: str.lower, *, value: commands.clean_content): + """ + Add an arg. + + Simply to add an arg, do: ``` + {prefix}args add name value + ``` + """ + if name in self.bot.args: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Arg `{name}` already exists.", + ) + return await ctx.send(embed=embed) + + if name in RESERVED_ARG_NAMES: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Arg name `{name}` is reserved (used by formatreply commands). " + f"Reserved names: {', '.join(f'`{n}`' for n in sorted(RESERVED_ARG_NAMES))}.", + ) + return await ctx.send(embed=embed) + + if len(name) > 120: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="Arg names cannot be longer than 120 characters.", + ) + return await ctx.send(embed=embed) + + self.bot.args[name] = value + await self.bot.config.update() + + embed = discord.Embed( + title="Added arg", + color=self.bot.main_color, + description="Successfully created arg.", + ) + return await ctx.send(embed=embed) + + @args.command(name="remove", aliases=["del", "delete"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def args_remove(self, ctx, *, name: str.lower): + """Remove an arg.""" + if name in self.bot.args: + self.bot.args.pop(name) + await self.bot.config.update() + embed = discord.Embed( + title="Removed arg", + color=self.bot.main_color, + description=f"Arg `{name}` is now deleted.", + ) + else: + embed = create_not_found_embed(name, self.bot.args.keys(), "Arg") + await ctx.send(embed=embed) + + @args.command(name="edit") + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def args_edit(self, ctx, name: str.lower, *, value): + """ + Edit an arg. + """ + if name in self.bot.args: + self.bot.args[name] = value + await self.bot.config.update() + + embed = discord.Embed( + title="Edited arg", + color=self.bot.main_color, + description=f'`{name}` will now be replaced with "{value}".', + ) + else: + embed = create_not_found_embed(name, self.bot.args.keys(), "Arg") + await ctx.send(embed=embed) + + @args.command(name="rename") + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def args_rename(self, ctx, name: str.lower, *, value): + """ + Rename an arg. + """ + if name not in self.bot.args: + embed = create_not_found_embed(name, self.bot.args.keys(), "Arg") + return await ctx.send(embed=embed) + + if value in self.bot.args: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Arg `{value}` already exists.", + ) + return await ctx.send(embed=embed) + + if value in RESERVED_ARG_NAMES: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Arg name `{value}` is reserved (used by formatreply commands). " + f"Reserved names: {', '.join(f'`{n}`' for n in sorted(RESERVED_ARG_NAMES))}.", + ) + return await ctx.send(embed=embed) + + if len(value) > 120: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="Arg names cannot be longer than 120 characters.", + ) + return await ctx.send(embed=embed) + + old_arg_value = self.bot.args[name] + self.bot.args.pop(name) + self.bot.args[value] = old_arg_value + await self.bot.config.update() + + embed = discord.Embed( + title="Renamed arg", + color=self.bot.main_color, + description=f'`{name}` has been renamed to "{value}".', + ) + await ctx.send(embed=embed) + @commands.command(usage=" [options]") @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() @@ -1510,6 +1722,19 @@ async def reply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ + if self.bot.args: + msg = self.bot.formatter.format(msg, **self.bot.args) + + if len(msg) > 4096: + return await ctx.send( + embed=discord.Embed( + title="Error", + color=self.bot.error_color, + description="The resulting message is too long to fit in an embed description " + f"({len(msg)}/4096 characters). Please shorten your message or args.", + ) + ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): @@ -1532,10 +1757,22 @@ async def freply(self, ctx, *, msg: str = ""): """ msg = self.bot.formatter.format( msg, + **self.bot.args, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author, ) + + if len(msg) > 4096: + return await ctx.send( + embed=discord.Embed( + title="Error", + color=self.bot.error_color, + description="The resulting message is too long to fit in an embed description " + f"({len(msg)}/4096 characters). Please shorten your message or args.", + ) + ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): @@ -1558,10 +1795,22 @@ async def fareply(self, ctx, *, msg: str = ""): """ msg = self.bot.formatter.format( msg, + **self.bot.args, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author, ) + + if len(msg) > 4096: + return await ctx.send( + embed=discord.Embed( + title="Error", + color=self.bot.error_color, + description="The resulting message is too long to fit in an embed description " + f"({len(msg)}/4096 characters). Please shorten your message or args.", + ) + ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): @@ -1584,10 +1833,22 @@ async def fpreply(self, ctx, *, msg: str = ""): """ msg = self.bot.formatter.format( msg, + **self.bot.args, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author, ) + + if len(msg) > 4096: + return await ctx.send( + embed=discord.Embed( + title="Error", + color=self.bot.error_color, + description="The resulting message is too long to fit in an embed description " + f"({len(msg)}/4096 characters). Please shorten your message or args.", + ) + ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): @@ -1610,10 +1871,22 @@ async def fpareply(self, ctx, *, msg: str = ""): """ msg = self.bot.formatter.format( msg, + **self.bot.args, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author, ) + + if len(msg) > 4096: + return await ctx.send( + embed=discord.Embed( + title="Error", + color=self.bot.error_color, + description="The resulting message is too long to fit in an embed description " + f"({len(msg)}/4096 characters). Please shorten your message or args.", + ) + ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): diff --git a/cogs/utility.py b/cogs/utility.py index bc863a6f45..283823d39c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1048,7 +1048,7 @@ async def alias(self, ctx, *, name: str.lower = None): When `{prefix}alias` is used by itself, this will retrieve a list of alias that are currently set. `{prefix}alias-name` will show what the - alias point to. + alias points to. To use alias: diff --git a/core/config.py b/core/config.py index 0e45b00175..113e7f17e6 100644 --- a/core/config.py +++ b/core/config.py @@ -182,6 +182,7 @@ class ConfigManager: "override_command_level": {}, # threads "snippets": {}, + "args": {}, "notification_squad": {}, "subscriptions": {}, "closures": {},