diff --git a/extensions/buzz/src/mentions.test.ts b/extensions/buzz/src/mentions.test.ts index f074f2109e95..4cdb6f12261f 100644 --- a/extensions/buzz/src/mentions.test.ts +++ b/extensions/buzz/src/mentions.test.ts @@ -9,6 +9,7 @@ import { const BOT_PUBLIC_KEY = "a".repeat(64); const ALICE_PUBLIC_KEY = "b".repeat(64); const SECOND_ALICE_PUBLIC_KEY = "c".repeat(64); +const BOB_PUBLIC_KEY = "d".repeat(64); function members(...values: BuzzMentionMember[]): BuzzMentionMember[] { return [{ publicKey: BOT_PUBLIC_KEY, displayName: "OpenClaw" }, ...values]; @@ -82,6 +83,51 @@ describe("Buzz outbound mentions", () => { ).toEqual([ALICE_PUBLIC_KEY]); }); + it("requires explicit identities to resolve the named member they accompany", () => { + const explicitBob = nip19.npubEncode(BOB_PUBLIC_KEY); + const roomMembers = members( + { publicKey: ALICE_PUBLIC_KEY, displayName: "Alice" }, + { publicKey: SECOND_ALICE_PUBLIC_KEY, displayName: "Alice" }, + { publicKey: BOB_PUBLIC_KEY, displayName: "Bob" }, + ); + + expect(() => + resolveBuzzMessageMentions({ + text: `Hello @Missing (nostr:${explicitBob})`, + members: roomMembers, + senderPublicKey: BOT_PUBLIC_KEY, + }), + ).toThrow('Buzz mention "@missing" does not match a current room member'); + + expect(() => + resolveBuzzMessageMentions({ + text: `Hello @Alice (nostr:${explicitBob})`, + members: roomMembers, + senderPublicKey: BOT_PUBLIC_KEY, + }), + ).toThrow('Buzz mention "@alice" is ambiguous'); + }); + + it("bounds ambiguous-member guidance", () => { + const duplicateMembers = Array.from({ length: 8 }, (_, index) => ({ + publicKey: (index + 1).toString(16).repeat(64), + displayName: "Alice", + })); + + expect(() => + resolveBuzzMessageMentions({ + text: "Hello @Alice", + members: members(...duplicateMembers), + senderPublicKey: BOT_PUBLIC_KEY, + }), + ).toThrow( + `candidates: ${duplicateMembers + .slice(0, 5) + .map((member) => nip19.npubEncode(member.publicKey)) + .join(", ")}, and 3 more.`, + ); + }); + it("rejects explicit identities outside the room and excludes the bot identity", () => { const outsider = "d".repeat(64); expect(() => diff --git a/extensions/buzz/src/mentions.ts b/extensions/buzz/src/mentions.ts index b3f58f0471e9..d72eb7c4a605 100644 --- a/extensions/buzz/src/mentions.ts +++ b/extensions/buzz/src/mentions.ts @@ -1,6 +1,7 @@ import { nip19 } from "nostr-tools"; export const BUZZ_MENTION_MAX_COUNT = 50; +const BUZZ_AMBIGUITY_CANDIDATE_LIMIT = 5; export type BuzzMentionMember = { publicKey: string; @@ -243,26 +244,30 @@ export function resolveBuzzMessageMentions(params: { .map((member) => member.displayName) .filter((name): name is string => Boolean(name)), ); - const hasExplicitMentions = explicitPublicKeys.length > 0; - if (hasAtMentionCandidate(stripped) && names.length === 0 && !hasExplicitMentions) { + if (hasAtMentionCandidate(stripped) && names.length === 0) { throw new Error( "Buzz mention does not match a current room member; use nostr:npub... for an explicit identity", ); } for (const name of names) { const matches = namesToPublicKeys.get(name) ?? []; - if (matches.length !== 1) { - if (hasExplicitMentions) { + if (matches.length === 0) { + throw new Error( + `Buzz mention "@${name}" does not match a current room member; use nostr:npub... for an explicit identity`, + ); + } + if (matches.length > 1) { + if (matches.some((publicKey) => explicitPublicKeys.includes(publicKey))) { continue; } - if (matches.length === 0) { - throw new Error( - `Buzz mention "@${name}" does not match a current room member; use nostr:npub... for an explicit identity`, - ); - } - const candidates = matches.map((publicKey) => nip19.npubEncode(publicKey)).join(", "); + const visibleCandidates = matches + .slice(0, BUZZ_AMBIGUITY_CANDIDATE_LIMIT) + .map((publicKey) => nip19.npubEncode(publicKey)) + .join(", "); + const hiddenCandidateCount = matches.length - BUZZ_AMBIGUITY_CANDIDATE_LIMIT; + const candidateSuffix = hiddenCandidateCount > 0 ? `, and ${hiddenCandidateCount} more` : ""; throw new Error( - `Buzz mention "@${name}" is ambiguous; candidates: ${candidates}. Use nostr:npub... for an explicit identity`, + `Buzz mention "@${name}" is ambiguous; candidates: ${visibleCandidates}${candidateSuffix}. Use nostr:npub... for an explicit identity`, ); } const publicKey = matches[0];