Skip to content
4 changes: 4 additions & 0 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
198 changes: 197 additions & 1 deletion cogs/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from dateutil import parser

from core import checks
from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, getLogger
from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, UnseenFormatter, getLogger
from core.paginator import EmbedPaginatorSession
from core.thread import Thread
from core.time import UserFriendlyTime, human_timedelta
Expand Down Expand Up @@ -535,6 +535,195 @@ 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}`.
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states "You can use your arg in a reply with {arg-name}" but doesn't mention that args only work in certain reply commands (reply, freply, fareply, fpreply, fpareply) and not in others (areply, preply, pareply). Consider updating the documentation to clarify which commands support args to avoid user confusion.

Suggested change
You can use your arg in a reply with `{arg-name}`.
You can use your arg in supported reply commands (`
reply`, `freply`, `fareply`, `fpreply`, `fpareply`) with `{arg-name}`. Args are
not available in `areply`, `preply`, or `pareply`.

Copilot uses AI. Check for mistakes.
"""

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")
Comment on lines +568 to +569
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The args command uses direct name lookup without resolving through aliases like the snippet command does with _resolve_snippet. This creates an inconsistency where snippets can be accessed via their aliases but args cannot. Consider adding a _resolve_arg method similar to _resolve_snippet to provide consistent behavior.

Copilot uses AI. Check for mistakes.
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.",
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: "dont" should be "don't".

Suggested change
description="You dont have any args at the moment.",
description="You don't have any args at the moment.",

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent contraction usage: "dont" should be "don't" with an apostrophe.

Suggested change
description="You dont have any args at the moment.",
description="You don't have any args at the moment.",

Copilot uses AI. Check for mistakes.
)
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also check that there is no conflict with the keywords from formatreply (channel, recipient and author). Currently this causes a silent fail without error. See screenshots.

Image Image

embed = discord.Embed(
title="Error",
color=self.bot.error_color,
description=f"Arg `{name}` already exists.",
)
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 in self.bot.args:
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 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}".',
)
else:
embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readability, this might be better fit as a guard clause at the start of the function. So

if name not in self.bot.args:
    return await ctx.send(embed=create_not_found_embed(name, self.bot.args.keys(), "Arg"))

or alternatively

if name not in self.bot.args:
    embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
    return await ctx.send(embed=embed)

await ctx.send(embed=embed)

@commands.command(usage="<category> [options]")
@checks.has_permissions(PermissionLevel.MODERATOR)
@checks.thread_only()
Expand Down Expand Up @@ -1510,6 +1699,9 @@ async def reply(self, ctx, *, msg: str = ""):
automatically embedding image URLs.
"""

if self.bot.args:
msg = UnseenFormatter().format(msg, **self.bot.args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no check for total message length after applying args. If I make an arg that's very long (I tested with a long part of lorem ipsum) then make a long reply plus the arg going over the limit, I just get an "unknown error". The bot logs show it goes over the limit.
As per the docs (screenshot below), the total length of an embed cannot exceed 6000 characters, and the descritpion may only be 4096 characters long. Please add a check to give a human error.

Image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the result of a long message
Image

Image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this formatter to be consistent with how formatreply works (using self.bot.formater.format())
This is done correctly in the formatreply command.


# Ensure logs record only the reply text, not the command.
ctx.message.content = msg
async with safe_typing(ctx):
Expand All @@ -1532,6 +1724,7 @@ 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,
Comment on lines +1727 to 1730
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the freply, fareply, fpreply, and fpareply commands, args are added before the built-in variables (channel, recipient, author). This means that if an arg is named "channel", "recipient", or "author", it will be overridden by the built-in variables, potentially causing unexpected behavior. Consider either adding args after the built-in variables (so built-in variables take precedence) or documenting this behavior and warning users not to use reserved names.

Copilot uses AI. Check for mistakes.
Expand All @@ -1558,6 +1751,7 @@ 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,
Expand All @@ -1584,6 +1778,7 @@ 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,
Expand All @@ -1610,6 +1805,7 @@ 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,
Expand Down
2 changes: 1 addition & 1 deletion cogs/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def format_cog_help(self, cog, *, no_cog=False):
return embeds

def process_help_msg(self, help_: str):
return help_.format(prefix=self.context.clean_prefix) if help_ else "No help message."
return help_.replace("{prefix}", self.context.clean_prefix) if help_ else "No help message."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being changed out? As far as I'm aware, the format() call was working just fine. If really needed, please elaborate why this is changed out.


async def send_bot_help(self, mapping):
embeds = []
Expand Down
1 change: 1 addition & 0 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class ConfigManager:
"override_command_level": {},
# threads
"snippets": {},
"args": {},
"notification_squad": {},
"subscriptions": {},
"closures": {},
Expand Down
Loading