From c45e248ccb66fcc7e3c9133e10d71d2e4dc7a6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Wed, 15 Apr 2026 17:54:34 +0000 Subject: [PATCH 1/3] fix: resolve mentions instead of stripping all (#363) Replace strip_mention with resolve_mentions so only the bot's own trigger mention is removed. All other <@ID> tokens are resolved to @DisplayName using msg.mentions, preserving bot-to-bot @mention visibility. --- src/discord.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/discord.rs b/src/discord.rs index 749297be..2788e993 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -11,7 +11,8 @@ use std::sync::LazyLock; use serenity::async_trait; use serenity::model::channel::{Message, ReactionType}; use serenity::model::gateway::Ready; -use serenity::model::id::{ChannelId, MessageId}; +use serenity::model::id::{ChannelId, MessageId, UserId}; +use serenity::model::user::User; use serenity::prelude::*; use std::collections::HashSet; use std::sync::Arc; @@ -167,7 +168,7 @@ impl EventHandler for Handler { } let prompt = if is_mentioned { - strip_mention(&msg.content) + resolve_mentions(&msg.content, bot_id, &msg.mentions) } else { msg.content.trim().to_string() }; @@ -790,12 +791,18 @@ fn compose_display(tool_lines: &[ToolEntry], text: &str, streaming: bool) -> Str out } -static MENTION_RE: LazyLock = LazyLock::new(|| { - regex::Regex::new(r"<@[!&]?\d+>").unwrap() -}); - -fn strip_mention(content: &str) -> String { - MENTION_RE.replace_all(content, "").trim().to_string() +fn resolve_mentions(content: &str, bot_id: UserId, mentions: &[User]) -> String { + let bot_re = regex::Regex::new(&format!(r"<@!?{}>", bot_id)).unwrap(); + let mut out = bot_re.replace_all(content, "").to_string(); + for user in mentions { + if user.id == bot_id { + continue; + } + let label = user.global_name.as_deref().unwrap_or(&user.name); + let re = regex::Regex::new(&format!(r"<@!?{}>", user.id)).unwrap(); + out = re.replace_all(&out, format!("@{}", label)).to_string(); + } + out.trim().to_string() } fn shorten_thread_name(prompt: &str) -> String { From 11edd651663ff13efdc951d834c8b014de8cf76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Wed, 15 Apr 2026 18:00:25 +0000 Subject: [PATCH 2/3] fix: avoid regex in loop, restore role mention handling, add fallback - Use str::replace instead of Regex::new in the resolve loop (no per-call regex compilation) - Restore handling of role mentions <@&ID> via the fallback - Unresolved <@ID> tokens (edge case: user not in msg.mentions) are replaced with @unknown instead of leaking raw tokens --- src/discord.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/discord.rs b/src/discord.rs index 2788e993..a0b34b28 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -791,17 +791,28 @@ fn compose_display(tool_lines: &[ToolEntry], text: &str, streaming: bool) -> Str out } +static MENTION_RE: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"<@[!&]?\d+>").unwrap() +}); + fn resolve_mentions(content: &str, bot_id: UserId, mentions: &[User]) -> String { - let bot_re = regex::Regex::new(&format!(r"<@!?{}>", bot_id)).unwrap(); - let mut out = bot_re.replace_all(content, "").to_string(); + // 1. Strip the bot's own trigger mention + let mut out = content + .replace(&format!("<@{}>", bot_id), "") + .replace(&format!("<@!{}>", bot_id), ""); + // 2. Resolve known user mentions to @DisplayName for user in mentions { if user.id == bot_id { continue; } let label = user.global_name.as_deref().unwrap_or(&user.name); - let re = regex::Regex::new(&format!(r"<@!?{}>", user.id)).unwrap(); - out = re.replace_all(&out, format!("@{}", label)).to_string(); + let display = format!("@{}", label); + out = out + .replace(&format!("<@{}>", user.id), &display) + .replace(&format!("<@!{}>", user.id), &display); } + // 3. Fallback: replace any remaining unresolved mentions (including role mentions) + let out = MENTION_RE.replace_all(&out, "@unknown").to_string(); out.trim().to_string() } From d5449dfedff5c8072456dc19cb360a920396ccea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Wed, 15 Apr 2026 18:01:48 +0000 Subject: [PATCH 3/3] nit: distinguish @(user) vs @(role) in fallback labels --- src/discord.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/discord.rs b/src/discord.rs index a0b34b28..0311b898 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -791,8 +791,11 @@ fn compose_display(tool_lines: &[ToolEntry], text: &str, streaming: bool) -> Str out } -static MENTION_RE: LazyLock = LazyLock::new(|| { - regex::Regex::new(r"<@[!&]?\d+>").unwrap() +static ROLE_MENTION_RE: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"<@&\d+>").unwrap() +}); +static USER_MENTION_RE: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"<@!?\d+>").unwrap() }); fn resolve_mentions(content: &str, bot_id: UserId, mentions: &[User]) -> String { @@ -811,8 +814,9 @@ fn resolve_mentions(content: &str, bot_id: UserId, mentions: &[User]) -> String .replace(&format!("<@{}>", user.id), &display) .replace(&format!("<@!{}>", user.id), &display); } - // 3. Fallback: replace any remaining unresolved mentions (including role mentions) - let out = MENTION_RE.replace_all(&out, "@unknown").to_string(); + // 3. Fallback: replace any remaining unresolved mentions + let out = ROLE_MENTION_RE.replace_all(&out, "@(role)"); + let out = USER_MENTION_RE.replace_all(&out, "@(user)").to_string(); out.trim().to_string() }