fix(plugin): strip leading @mention from Buzz channel messages

Channel messages address the agent with a leading @mention (e.g. '@Chip
/whoami'). The adapter passed the raw content through, so the gateway's
is_command() check (text.lstrip().startswith('/')) never matched and slash
commands were routed as plain chat. Strip a leading mention (name, npub, or
hex form) before dispatch in channels, mirroring the Discord adapter. Also
cleans normal prompts ('@Chip what's up?' -> 'what's up?'). DMs are untouched.

Verified live: '@Chip /whoami' -> '/whoami' after connect populates identity.

Refs #68871
This commit is contained in:
Rob Zehner 2026-07-25 21:15:36 +00:00 committed by Teknium
parent 01f8852ccc
commit 65f52d4913

View file

@ -687,8 +687,13 @@ class BuzzAdapter(BasePlatformAdapter):
logger.debug("Buzz: ignoring message from unauthorized pubkey %s", pubkey[:8])
return
# In channels the message addressed us with a leading @mention; strip
# it so slash commands (@Chip /whoami -> /whoami) and clean prompts are
# recognized. DMs are already clean.
dispatch_text = content if is_dm else self._strip_mention(content)
await self._dispatch_message(
text=content,
text=dispatch_text,
chat_id=channel_id,
chat_type="dm" if is_dm else "group",
user_id=pubkey,
@ -710,6 +715,33 @@ class BuzzAdapter(BasePlatformAdapter):
return True
return False
def _strip_mention(self, content: str) -> str:
"""Remove a leading @mention of this agent so the remaining text can be
recognized as a slash command or clean prompt.
Mirrors the Discord adapter, which strips its own ``<@id>`` mention
before dispatch. Without this a channel message like ``@Chip /whoami``
arrives with a leading ``@Chip``; the gateway's ``is_command()`` checks
``text.lstrip().startswith("/")`` and never fires the command. Only a
LEADING mention is stripped (case-insensitive); mentions mid-sentence
are left intact so normal prose is unaffected.
"""
text = content.strip()
candidates = []
if self._display_name:
candidates.append(re.escape(self._display_name))
if self._self_npub:
candidates.append(re.escape(self._self_npub))
if self._self_pubkey:
candidates.append(re.escape(self._self_pubkey))
if not candidates:
return text
# Optional leading '@', one of the identity forms, optional trailing
# ':' or ',' and surrounding whitespace.
pattern = rf"^@?(?:{'|'.join(candidates)})[\s:,]*"
stripped = re.sub(pattern, "", text, count=1, flags=re.IGNORECASE)
return stripped.strip()
async def _resolve_user_name(self, pubkey: str) -> str:
"""Resolve a pubkey to a display name (cached; falls back to npub prefix)."""
cached = self._user_names.get(pubkey)