From c54e6099e3dfac34b91f575f454c8f88b1e7f98d Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:26:22 +0200 Subject: [PATCH 01/14] [Fix]: Escape markdown-relevant characters in nicknames. --- util.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/util.py b/util.py index 11ad552..507caf2 100644 --- a/util.py +++ b/util.py @@ -26,6 +26,26 @@ class RelayMember(discord.Member): """ +def escape_nickname(name: str) -> str: + """ + Escape all characters in a discord nickname so they don't convert to markdown. + """ + + name = name.replace("\\", "\\\\") # Need to escape \ first, so we don't undo any of our work later. + escapeable_characters = [ + "*", + "_", + "`", + ">", + "~", + "|", + ] + for char in escapeable_characters: + name = name.replace(char, rf"\{char}") + + return name + + def return_or_truncate(text, max_length): """Takes a string and truncates it to a maximum length, adding ellipsis if truncated. If the string is shorter than the maximum length, it returns the original string. From c54391faf6888b99a1f479177320cc0e25265c1a Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:30:57 +0200 Subject: [PATCH 02/14] [Feat]: Use `util.escape_nickname` at every location where the nickname is used in a reply or log message. --- cogs/cOREmands.py | 6 +++--- cogs/errorhandler.py | 4 ++-- cogs/notifications.py | 6 ++++-- cogs/randcommands.py | 9 +++++---- cogs/reminders.py | 4 ++-- cogs/timers.py | 4 ++-- patrick.py | 8 ++++---- util.py | 8 +++++--- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/cogs/cOREmands.py b/cogs/cOREmands.py index 8c40a92..0e5d4df 100644 --- a/cogs/cOREmands.py +++ b/cogs/cOREmands.py @@ -2,7 +2,7 @@ import discord from discord.ext import commands -from util import app_is_staff, is_staff, create_deletion_embed, reply +from util import app_is_staff, is_staff, create_deletion_embed, reply, escape_nickname class COREmands(commands.Cog): @@ -35,10 +35,10 @@ async def trust(self, ctx, member: discord.Member): role = ctx.guild.get_role(self.bot.config["roles"]["trusted"]) if role in member.roles: await member.remove_roles(role) - await reply(ctx, f"{member.display_name} is no longer Trusted.") + await reply(ctx, f"{escape_nickname(member.display_name)} is no longer Trusted.") else: await member.add_roles(role) - await reply(ctx, f"{member.display_name} is now Trusted.") + await reply(ctx, f"{escape_nickname(member.display_name)} is now Trusted.") async def setup(bot): await bot.add_cog(COREmands(bot)) diff --git a/cogs/errorhandler.py b/cogs/errorhandler.py index b7b8300..d338973 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -2,7 +2,7 @@ from discord.ext import commands -from util import NoRelayException, reply +from util import NoRelayException, reply, escape_nickname class ErrorHandler(commands.Cog): @@ -34,7 +34,7 @@ async def respond(message): ) elif isinstance(error, commands.CommandNotFound): self.bot.logger.info( - f"User '{ctx.author.display_name}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User '{escape_nickname(ctx.author.display_name)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) await respond("Unrecognized command :'(") elif isinstance(error, commands.CommandOnCooldown): diff --git a/cogs/notifications.py b/cogs/notifications.py index 50ba1a7..5b9bbac 100644 --- a/cogs/notifications.py +++ b/cogs/notifications.py @@ -1,6 +1,8 @@ import discord from discord.ext import commands +from util import escape_nickname + class RoleButton(discord.ui.Button): def __init__(self, role): @@ -13,13 +15,13 @@ async def callback(self, interaction: discord.Interaction): if self.role in interaction.user.roles: await interaction.user.remove_roles(self.role) await interaction.response.send_message( - f"{interaction.user.display_name}, you are no longer subscribed to {self.role.name} notifications.", + f"{escape_nickname(interaction.user.display_name)}, you are no longer subscribed to {self.role.name} notifications.", ephemeral=True, ) else: await interaction.user.add_roles(self.role) await interaction.response.send_message( - f"{interaction.user.display_name}, you are now subscribed to {self.role.name} notifications.", + f"{escape_nickname(interaction.user.display_name)}, you are now subscribed to {self.role.name} notifications.", ephemeral=True, ) diff --git a/cogs/randcommands.py b/cogs/randcommands.py index 102ad81..fa905ba 100644 --- a/cogs/randcommands.py +++ b/cogs/randcommands.py @@ -15,7 +15,7 @@ from fractal import fractal from spirograph import spirograph from brainfuck import process_brainfuck -from util import is_staff, BaseConversionError, baseconvert, reply +from util import is_staff, BaseConversionError, baseconvert, reply, escape_nickname class RandCommands(commands.Cog): @@ -42,7 +42,7 @@ async def convert_func(ctx, number: str): # otherwise keep the generic text. except BaseConversionError as err: raise err from None - except ValueError as e: + except ValueError: await reply(ctx, f"Invalid input number for base {from_base}") for from_base, from_value in bases.items(): @@ -63,7 +63,8 @@ async def ping(self, ctx): message = await reply(ctx, "Testing...") latency = (perf_counter() - start) * 1000 await message.edit( - content=f"{ctx.author.display_name}: Pong!\nLatency: {latency:.2f}ms\n" + content=f"{escape_nickname(ctx.author.display_name)}: Pong!\n" + f"Latency: {latency:.2f}ms\n" f"API Latency: {self.bot.latency * 1000:.2f}ms" ) @@ -347,7 +348,7 @@ async def spirograph(self, ctx, seed: str): @commands.command(help="Be mean to someone. >:D") async def insult(self, ctx, target: str = None): if target is None: - target = ctx.author.display_name + target = escape_nickname(ctx.author.display_name) message = choice(self.bot.config["insults"]) await reply(ctx, message.format(user=target)) diff --git a/cogs/reminders.py b/cogs/reminders.py index 7343957..eacfa8f 100644 --- a/cogs/reminders.py +++ b/cogs/reminders.py @@ -2,7 +2,7 @@ from discord.ext import commands, tasks from datetime import datetime, timedelta -from util import is_discord_member +from util import is_discord_member, escape_nickname from timeutil import UserFriendlyTime from paginator import EmbedPaginatorSession @@ -40,7 +40,7 @@ async def my_reminders(self, ctx): """List all reminders set by the user.""" reminders = await self.bot.database.get_reminders(ctx.author.id) if not reminders: - return await ctx.reply(f"{ctx.author.display_name}: You have no reminders set.") + return await ctx.reply(f"{escape_nickname(ctx.author.display_name)}: You have no reminders set.") if len(reminders) > 5: embeds = [discord.Embed(title=f"{ctx.author.display_name}'s Reminders", color=discord.Color.blue()) for _ in range((len(reminders) - 1) // 5 + 1)] diff --git a/cogs/timers.py b/cogs/timers.py index c984772..fc144eb 100644 --- a/cogs/timers.py +++ b/cogs/timers.py @@ -3,7 +3,7 @@ import discord from discord.ext import commands -from util import is_discord_member, reply +from util import is_discord_member, reply, escape_nickname def pretty_timedelta(delta): @@ -61,7 +61,7 @@ async def list_timers(self, ctx, member: discord.Member = None): if member == ctx.author: await reply(ctx, f"Your timers:\n{timers}") else: - await reply(ctx, f"{member.display_name}'s timers:\n{timers}") + await reply(ctx, f"{escape_nickname(member.display_name)}'s timers:\n{timers}") else: await reply(ctx, "No timers found.") diff --git a/patrick.py b/patrick.py index d0402e3..419762c 100644 --- a/patrick.py +++ b/patrick.py @@ -14,7 +14,7 @@ from logger import StreamLogFormatter, setup_logger from util import (find_automod_matches, is_admin, load_automod_regexes, process_custom_command, reformat_relay_chat, split_list, - reply, create_automod_embed, RelayMember) + reply, create_automod_embed, RelayMember, escape_nickname) load_dotenv(Path(__file__).parent / ".env") TOKEN: str = getenv("TOKEN") @@ -229,7 +229,7 @@ async def on_message(self, message: discord.Message) -> None: matches = find_automod_matches(self, part) if matches: logger.info( - f"Automod triggered for user {message.author.display_name} with message {message.content}" + f"Automod triggered for user {escape_nickname(message.author.display_name)} with message {message.content}" ) channel = message.guild.get_channel( self.config["channels"]["automod"] @@ -269,7 +269,7 @@ async def process_commands(self, message: discord.Message) -> None: else: # A prefix was found, but no (custom) command was found. This means the user is trying to run a command that does not exist. self.logger.info( - f"User '{ctx.author.display_name}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User '{escape_nickname(ctx.author.display_name)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) return await reply(ctx, "Unrecognized command :'(") @@ -277,7 +277,7 @@ async def process_commands(self, message: discord.Message) -> None: # The context is valid when a command and prefix was found. # This is provided by discord.py and ensures that the context is valid for regular command processing self.logger.info( - f"User '{message.author.display_name}' ran command '{ctx.command.name}'" + f"User '{escape_nickname(message.author.display_name)}' ran command '{ctx.command.name}'" ) await self.database.add_command_history( message.author.display_name, ctx.command.name diff --git a/util.py b/util.py index 507caf2..edf3c92 100644 --- a/util.py +++ b/util.py @@ -105,12 +105,14 @@ async def process_custom_command(bot, message) -> bool: for prefix in bot.command_prefix: if message.content.removeprefix(prefix) in commands: bot.logger.info( - f"User '{message.author.display_name}' ran custom command '{message.content[1:]}'" + f"User '{escape_nickname(message.author.display_name)}' ran custom command '{message.content[1:]}'" ) await message.channel.send( - f"{message.author.display_name}: {choice(commands[message.content.removeprefix(prefix)])}" + f"{escape_nickname(message.author.display_name)}: {choice(commands[message.content.removeprefix(prefix)])}" ) await bot.database.add_command_history( + # No need to escape name here, this is not sent immediately. Also, it might + # cause problems with the current state of the DB. message.author.display_name, message.content.removeprefix(prefix) ) return True @@ -428,4 +430,4 @@ async def reply(ctx, message=None, is_reply=False, is_silent=False, **kwargs): if message is None: message = "" target = ctx.reply if is_reply else ctx.send - return await target(f"{ctx.author.display_name}: {message}", silent=is_silent, **kwargs) + return await target(f"{escape_nickname(ctx.author.display_name)}: {message}", silent=is_silent, **kwargs) From 3e424aef22156fe629020dec9612d412bfd68883 Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:31:29 +0200 Subject: [PATCH 03/14] [Feat]: Use `util.escape_nickname` in embed positions, as that seems to break too. --- cogs/moderation.py | 4 ++-- cogs/reminders.py | 4 ++-- util.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/moderation.py b/cogs/moderation.py index 32f310d..cadb184 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -3,7 +3,7 @@ from discord.ext import commands, tasks import typing -from util import is_staff, app_is_staff, create_deletion_embed, reformat_relay_chat +from util import is_staff, app_is_staff, create_deletion_embed, reformat_relay_chat, escape_nickname from timeutil import UserFriendlyTime class Moderation(commands.Cog): @@ -97,7 +97,7 @@ async def tempban(self, ctx, user: typing.Union[discord.Member, discord.User], * embed.set_thumbnail(url="https://i.imgflip.com/44o9ir.png") embed.add_field(name="Staff Member", value=ctx.author.mention, inline=False) embed.add_field(name="User", value=user.mention, inline=True) - embed.add_field(name="Display Name", value=user.display_name, inline=True) + embed.add_field(name="Display Name", value=escape_nickname(user.display_name), inline=True) embed.add_field(name="Reason", value=reason if reason else "No reason provided", inline=False) embed.timestamp = ctx.message.created_at diff --git a/cogs/reminders.py b/cogs/reminders.py index eacfa8f..ff64b9c 100644 --- a/cogs/reminders.py +++ b/cogs/reminders.py @@ -43,7 +43,7 @@ async def my_reminders(self, ctx): return await ctx.reply(f"{escape_nickname(ctx.author.display_name)}: You have no reminders set.") if len(reminders) > 5: - embeds = [discord.Embed(title=f"{ctx.author.display_name}'s Reminders", color=discord.Color.blue()) for _ in range((len(reminders) - 1) // 5 + 1)] + embeds = [discord.Embed(title=f"{escape_nickname(ctx.author.display_name)}'s Reminders", color=discord.Color.blue()) for _ in range((len(reminders) - 1) // 5 + 1)] for i, reminder in enumerate(reminders): embeds[i // 5].add_field( name=f"Reminder at {reminder[2].strftime('%Y-%m-%d %H:%M:%S')}", @@ -53,7 +53,7 @@ async def my_reminders(self, ctx): paginator = EmbedPaginatorSession(ctx, *embeds) await paginator.run() else: - embed = discord.Embed(title=f"{ctx.author.display_name}'s Reminders", color=discord.Color.blue()) + embed = discord.Embed(title=f"{escape_nickname(ctx.author.display_name)}'s Reminders", color=discord.Color.blue()) for message, _, timestamp in reminders: embed.add_field( name=f"Reminder at {timestamp.strftime('%Y-%m-%d %H:%M:%S')}", diff --git a/util.py b/util.py index edf3c92..1f97141 100644 --- a/util.py +++ b/util.py @@ -349,7 +349,7 @@ async def create_deletion_embed( embed.set_thumbnail(url="https://i.imgflip.com/44o9ir.png") embed.add_field(name="Staff Member", value=staff.mention, inline=False) embed.add_field(name="User", value=message.author.mention, inline=True) - embed.add_field(name="Display Name", value=message.author.display_name, inline=True) + embed.add_field(name="Display Name", value=escape_nickname(message.author.display_name), inline=True) embed.add_field(name="Reason", value=reason, inline=False) if len(message.message_snapshots) > 0: embed.add_field( From a224ca9d7ff7f12d87c10fe6490181440f4fe07a Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:36:13 +0200 Subject: [PATCH 04/14] [Feat]: Add link formats to escaped character-set. --- util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/util.py b/util.py index 1f97141..6387f90 100644 --- a/util.py +++ b/util.py @@ -39,6 +39,11 @@ def escape_nickname(name: str) -> str: ">", "~", "|", + # Part of links. + "[", + "]", + "(", + ")", ] for char in escapeable_characters: name = name.replace(char, rf"\{char}") From 4b08b9063abbe6d97d867b64d3af96d950a5e083 Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:11:42 -0600 Subject: [PATCH 05/14] Rewrite `escape_nickname` using regexes. --- util.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/util.py b/util.py index 6387f90..a245a5b 100644 --- a/util.py +++ b/util.py @@ -9,6 +9,9 @@ from discord.ext import commands +DISCORD_NICKNAME_ESCAPE_RE = re.compile(r'([\\*_`>~|\[\]()])') + + class NoRelayException(Exception): ... @@ -31,24 +34,7 @@ def escape_nickname(name: str) -> str: Escape all characters in a discord nickname so they don't convert to markdown. """ - name = name.replace("\\", "\\\\") # Need to escape \ first, so we don't undo any of our work later. - escapeable_characters = [ - "*", - "_", - "`", - ">", - "~", - "|", - # Part of links. - "[", - "]", - "(", - ")", - ] - for char in escapeable_characters: - name = name.replace(char, rf"\{char}") - - return name + return DISCORD_NICKNAME_ESCAPE_RE.sub(r"\\\1", name) def return_or_truncate(text, max_length): From bdd2b6243b0303e71d2151c968ec3101499955aa Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:39:11 -0600 Subject: [PATCH 06/14] [Feat]: Add `chattore_log_format` function to format usernames within logs. --- cogs/errorhandler.py | 4 ++-- patrick.py | 11 ++++++----- util.py | 11 ++++++++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/cogs/errorhandler.py b/cogs/errorhandler.py index d338973..11a04b7 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -2,7 +2,7 @@ from discord.ext import commands -from util import NoRelayException, reply, escape_nickname +from util import NoRelayException, reply, chattore_log_format class ErrorHandler(commands.Cog): @@ -34,7 +34,7 @@ async def respond(message): ) elif isinstance(error, commands.CommandNotFound): self.bot.logger.info( - f"User '{escape_nickname(ctx.author.display_name)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User '{chattore_log_format(ctx.author)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) await respond("Unrecognized command :'(") elif isinstance(error, commands.CommandOnCooldown): diff --git a/patrick.py b/patrick.py index 419762c..25f69de 100644 --- a/patrick.py +++ b/patrick.py @@ -14,7 +14,8 @@ from logger import StreamLogFormatter, setup_logger from util import (find_automod_matches, is_admin, load_automod_regexes, process_custom_command, reformat_relay_chat, split_list, - reply, create_automod_embed, RelayMember, escape_nickname) + reply, create_automod_embed, RelayMember, escape_nickname, + chattore_log_format,) load_dotenv(Path(__file__).parent / ".env") TOKEN: str = getenv("TOKEN") @@ -218,7 +219,7 @@ async def on_message(self, message: discord.Message) -> None: if message.guild is not None and message.content.startswith("/link"): # If the message starts with /link, it's probably someone trying to link their account but not selecting the command from the popup. await message.channel.send( - f"{message.author.display_name}: Please use the `/link` command from the command popup as you type. Do not type it out manually." + f"{escape_nickname(message.author.display_name)}: Please use the `/link` command from the command popup as you type. Do not type it out manually." ) await message.delete() return @@ -229,7 +230,7 @@ async def on_message(self, message: discord.Message) -> None: matches = find_automod_matches(self, part) if matches: logger.info( - f"Automod triggered for user {escape_nickname(message.author.display_name)} with message {message.content}" + f"Automod triggered for user {chattore_log_format(message.author)} with message {message.content}" ) channel = message.guild.get_channel( self.config["channels"]["automod"] @@ -269,7 +270,7 @@ async def process_commands(self, message: discord.Message) -> None: else: # A prefix was found, but no (custom) command was found. This means the user is trying to run a command that does not exist. self.logger.info( - f"User '{escape_nickname(ctx.author.display_name)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User {chattore_log_format(ctx.author)} attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) return await reply(ctx, "Unrecognized command :'(") @@ -277,7 +278,7 @@ async def process_commands(self, message: discord.Message) -> None: # The context is valid when a command and prefix was found. # This is provided by discord.py and ensures that the context is valid for regular command processing self.logger.info( - f"User '{escape_nickname(message.author.display_name)}' ran command '{ctx.command.name}'" + f"User {chattore_log_format(message.author)}' ran command '{ctx.command.name}'" ) await self.database.add_command_history( message.author.display_name, ctx.command.name diff --git a/util.py b/util.py index a245a5b..d65658f 100644 --- a/util.py +++ b/util.py @@ -29,6 +29,15 @@ class RelayMember(discord.Member): """ +def chattore_log_format(user: discord.User | discord.Member) -> str: + """ + Create a formatted string using the un-escaped nickname as well as the user + id, so log messages are the same as in chattore. + """ + + return f"{user.display_name} ({user.id})" + + def escape_nickname(name: str) -> str: """ Escape all characters in a discord nickname so they don't convert to markdown. @@ -96,7 +105,7 @@ async def process_custom_command(bot, message) -> bool: for prefix in bot.command_prefix: if message.content.removeprefix(prefix) in commands: bot.logger.info( - f"User '{escape_nickname(message.author.display_name)}' ran custom command '{message.content[1:]}'" + f"User {chattore_log_format(message.author)} ran custom command '{message.content[1:]}'" ) await message.channel.send( f"{escape_nickname(message.author.display_name)}: {choice(commands[message.content.removeprefix(prefix)])}" From e515adcd966b46d0bab0345788591f0e0ff67b3e Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:02:08 -0600 Subject: [PATCH 07/14] [Fix]: Allow `#` and `-` in Nickname too. --- util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.py b/util.py index d65658f..e4847a1 100644 --- a/util.py +++ b/util.py @@ -9,7 +9,7 @@ from discord.ext import commands -DISCORD_NICKNAME_ESCAPE_RE = re.compile(r'([\\*_`>~|\[\]()])') +DISCORD_NICKNAME_ESCAPE_RE = re.compile(r'([\\*#-_`>~|\[\]()])') class NoRelayException(Exception): From 4647ae9804bf45c511f1df073b901e46ce25c64c Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:06:21 -0600 Subject: [PATCH 08/14] Rename `chattore_log_format` to `user_log_repr`. --- cogs/errorhandler.py | 4 ++-- patrick.py | 8 ++++---- util.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cogs/errorhandler.py b/cogs/errorhandler.py index 11a04b7..c2bac52 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -2,7 +2,7 @@ from discord.ext import commands -from util import NoRelayException, reply, chattore_log_format +from util import NoRelayException, reply, user_log_repr class ErrorHandler(commands.Cog): @@ -34,7 +34,7 @@ async def respond(message): ) elif isinstance(error, commands.CommandNotFound): self.bot.logger.info( - f"User '{chattore_log_format(ctx.author)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User '{user_log_repr(ctx.author)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) await respond("Unrecognized command :'(") elif isinstance(error, commands.CommandOnCooldown): diff --git a/patrick.py b/patrick.py index 25f69de..c10cbeb 100644 --- a/patrick.py +++ b/patrick.py @@ -15,7 +15,7 @@ from util import (find_automod_matches, is_admin, load_automod_regexes, process_custom_command, reformat_relay_chat, split_list, reply, create_automod_embed, RelayMember, escape_nickname, - chattore_log_format,) + user_log_repr) load_dotenv(Path(__file__).parent / ".env") TOKEN: str = getenv("TOKEN") @@ -230,7 +230,7 @@ async def on_message(self, message: discord.Message) -> None: matches = find_automod_matches(self, part) if matches: logger.info( - f"Automod triggered for user {chattore_log_format(message.author)} with message {message.content}" + f"Automod triggered for user {user_log_repr(message.author)} with message {message.content}" ) channel = message.guild.get_channel( self.config["channels"]["automod"] @@ -270,7 +270,7 @@ async def process_commands(self, message: discord.Message) -> None: else: # A prefix was found, but no (custom) command was found. This means the user is trying to run a command that does not exist. self.logger.info( - f"User {chattore_log_format(ctx.author)} attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User {user_log_repr(ctx.author)} attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) return await reply(ctx, "Unrecognized command :'(") @@ -278,7 +278,7 @@ async def process_commands(self, message: discord.Message) -> None: # The context is valid when a command and prefix was found. # This is provided by discord.py and ensures that the context is valid for regular command processing self.logger.info( - f"User {chattore_log_format(message.author)}' ran command '{ctx.command.name}'" + f"User {user_log_repr(message.author)}' ran command '{ctx.command.name}'" ) await self.database.add_command_history( message.author.display_name, ctx.command.name diff --git a/util.py b/util.py index e4847a1..03ccf5a 100644 --- a/util.py +++ b/util.py @@ -29,7 +29,7 @@ class RelayMember(discord.Member): """ -def chattore_log_format(user: discord.User | discord.Member) -> str: +def user_log_repr(user: discord.User | discord.Member) -> str: """ Create a formatted string using the un-escaped nickname as well as the user id, so log messages are the same as in chattore. @@ -105,7 +105,7 @@ async def process_custom_command(bot, message) -> bool: for prefix in bot.command_prefix: if message.content.removeprefix(prefix) in commands: bot.logger.info( - f"User {chattore_log_format(message.author)} ran custom command '{message.content[1:]}'" + f"User {user_log_repr(message.author)} ran custom command '{message.content[1:]}'" ) await message.channel.send( f"{escape_nickname(message.author.display_name)}: {choice(commands[message.content.removeprefix(prefix)])}" From e8869fdca5acc8d4ab63b6d34de2a0ae5094a77d Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Sun, 20 Sep 2026 09:07:39 -0600 Subject: [PATCH 09/14] [Fix]: Very critical bug that would have crashed the entire world trust. --- patrick.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patrick.py b/patrick.py index c10cbeb..255e79e 100644 --- a/patrick.py +++ b/patrick.py @@ -270,7 +270,7 @@ async def process_commands(self, message: discord.Message) -> None: else: # A prefix was found, but no (custom) command was found. This means the user is trying to run a command that does not exist. self.logger.info( - f"User {user_log_repr(ctx.author)} attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User '{user_log_repr(ctx.author)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) return await reply(ctx, "Unrecognized command :'(") @@ -278,7 +278,7 @@ async def process_commands(self, message: discord.Message) -> None: # The context is valid when a command and prefix was found. # This is provided by discord.py and ensures that the context is valid for regular command processing self.logger.info( - f"User {user_log_repr(message.author)}' ran command '{ctx.command.name}'" + f"User '{user_log_repr(message.author)}' ran command '{ctx.command.name}'" ) await self.database.add_command_history( message.author.display_name, ctx.command.name From 9d66e41429376910c4b3881c287896aee5e64bbd Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:14:43 +0200 Subject: [PATCH 10/14] [Fix]: https://github.com/OpenRedstoneEngineers/Patrick/pull/67#discussion_r4057212728 --- util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.py b/util.py index 03ccf5a..792e702 100644 --- a/util.py +++ b/util.py @@ -9,7 +9,7 @@ from discord.ext import commands -DISCORD_NICKNAME_ESCAPE_RE = re.compile(r'([\\*#-_`>~|\[\]()])') +DISCORD_NICKNAME_ESCAPE_RE = re.compile(r'([\\*#_`>~|\[\]()-])') class NoRelayException(Exception): From 30ef2723aafe7b9c8b75c10b1829cb48c4eed576 Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:51:26 +0200 Subject: [PATCH 11/14] [Fix]: Undo markdown escaping for `RelayMember` instances so `escape_nickname` doesn't undo any work. --- util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.py b/util.py index 792e702..bf35931 100644 --- a/util.py +++ b/util.py @@ -83,7 +83,7 @@ def reformat_relay_chat(bot, message) -> typing.Optional[discord.Message]: author_name, content = match.groups() message.author = copy(message.author) message.author.__class__ = RelayMember - message.author.nick = author_name + message.author.nick = author_name.replace("\\", "") message.content = content return message return None From 96a8b931cc4eb22d536e40f6cf397cc6dfa2e4e4 Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:53:24 +0200 Subject: [PATCH 12/14] [Fix]: Make `user_log_repr` wrap only the `name` section in quotes. See https://github.com/OpenRedstoneEngineers/Patrick/pull/67#discussion_r4057845767. --- util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.py b/util.py index bf35931..9581f33 100644 --- a/util.py +++ b/util.py @@ -35,7 +35,7 @@ def user_log_repr(user: discord.User | discord.Member) -> str: id, so log messages are the same as in chattore. """ - return f"{user.display_name} ({user.id})" + return f"'{user.display_name}' ({user.id})" def escape_nickname(name: str) -> str: From dc1947f54d9c974e120c1bc66ffb889548df3a98 Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:55:35 +0200 Subject: [PATCH 13/14] [Fix]: Remove quotes around `user_log_repr` in `errorhandler.py` --- cogs/errorhandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/errorhandler.py b/cogs/errorhandler.py index c2bac52..e25b3b5 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -34,7 +34,7 @@ async def respond(message): ) elif isinstance(error, commands.CommandNotFound): self.bot.logger.info( - f"User '{user_log_repr(ctx.author)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User {user_log_repr(ctx.author)} attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) await respond("Unrecognized command :'(") elif isinstance(error, commands.CommandOnCooldown): From 37489c87f03d1b7da4126d237d8f2898e610de7a Mon Sep 17 00:00:00 2001 From: JoBe <165585785+JoBeGaming@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:58:09 +0200 Subject: [PATCH 14/14] [Fix]: Remove quotes around `user_log_repr` in `patrick.py` --- patrick.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patrick.py b/patrick.py index 255e79e..386cd86 100644 --- a/patrick.py +++ b/patrick.py @@ -270,7 +270,7 @@ async def process_commands(self, message: discord.Message) -> None: else: # A prefix was found, but no (custom) command was found. This means the user is trying to run a command that does not exist. self.logger.info( - f"User '{user_log_repr(ctx.author)}' attempted to run an unrecognized command: '{ctx.message.content[1:]}'" + f"User {user_log_repr(ctx.author)} attempted to run an unrecognized command: '{ctx.message.content[1:]}'" ) return await reply(ctx, "Unrecognized command :'(") @@ -278,7 +278,7 @@ async def process_commands(self, message: discord.Message) -> None: # The context is valid when a command and prefix was found. # This is provided by discord.py and ensures that the context is valid for regular command processing self.logger.info( - f"User '{user_log_repr(message.author)}' ran command '{ctx.command.name}'" + f"User {user_log_repr(message.author)} ran command '{ctx.command.name}'" ) await self.database.add_command_history( message.author.display_name, ctx.command.name