diff --git a/src/lib/nostr-utils.test.ts b/src/lib/nostr-utils.test.ts index 73eb3aa..4226408 100644 --- a/src/lib/nostr-utils.test.ts +++ b/src/lib/nostr-utils.test.ts @@ -237,16 +237,86 @@ describe("resolveFilterAliases", () => { expect(result.authors?.length).toBe(5000); }); - it("should handle mixed case aliases (should not match)", () => { - // Aliases are case-sensitive and should be lowercase + it("should handle mixed case aliases (case-insensitive)", () => { + // Aliases are case-insensitive for user convenience + const accountPubkey = "a".repeat(64); + const contacts = ["b".repeat(64), "c".repeat(64)]; const filter: NostrFilter = { authors: ["$Me", "$CONTACTS"] }; - const result = resolveFilterAliases(filter, "a".repeat(64), [ - "b".repeat(64), - ]); + const result = resolveFilterAliases(filter, accountPubkey, contacts); - // These should NOT be resolved (case mismatch) - expect(result.authors).toContain("$Me"); - expect(result.authors).toContain("$CONTACTS"); + // These should be resolved despite case differences + expect(result.authors).toContain(accountPubkey); + expect(result.authors).toContain(contacts[0]); + expect(result.authors).toContain(contacts[1]); + expect(result.authors).not.toContain("$Me"); + expect(result.authors).not.toContain("$CONTACTS"); + }); + }); + + describe("case-insensitive alias resolution", () => { + it("should resolve $ME (uppercase) in authors", () => { + const filter: NostrFilter = { authors: ["$ME"] }; + const accountPubkey = "a".repeat(64); + const result = resolveFilterAliases(filter, accountPubkey, []); + + expect(result.authors).toEqual([accountPubkey]); + expect(result.authors).not.toContain("$ME"); + }); + + it("should resolve $Me (mixed case) in #p tags", () => { + const filter: NostrFilter = { "#p": ["$Me"] }; + const accountPubkey = "a".repeat(64); + const result = resolveFilterAliases(filter, accountPubkey, []); + + expect(result["#p"]).toEqual([accountPubkey]); + expect(result["#p"]).not.toContain("$Me"); + }); + + it("should resolve $CONTACTS (uppercase) in authors", () => { + const filter: NostrFilter = { authors: ["$CONTACTS"] }; + const contacts = ["a".repeat(64), "b".repeat(64)]; + const result = resolveFilterAliases(filter, undefined, contacts); + + expect(result.authors).toEqual(contacts); + expect(result.authors).not.toContain("$CONTACTS"); + }); + + it("should resolve $Contacts (mixed case) in #P tags", () => { + const filter: NostrFilter = { "#P": ["$Contacts"] }; + const contacts = ["a".repeat(64), "b".repeat(64)]; + const result = resolveFilterAliases(filter, undefined, contacts); + + expect(result["#P"]).toEqual(contacts); + expect(result["#P"]).not.toContain("$Contacts"); + }); + + it("should handle multiple case variations in same filter", () => { + const filter: NostrFilter = { + authors: ["$me", "$ME", "$Me"], + "#p": ["$contacts", "$CONTACTS", "$Contacts"], + }; + const accountPubkey = "a".repeat(64); + const contacts = ["b".repeat(64), "c".repeat(64)]; + const result = resolveFilterAliases(filter, accountPubkey, contacts); + + // Should deduplicate all variants of $me to single pubkey + expect(result.authors).toEqual([accountPubkey]); + // Should deduplicate all variants of $contacts + expect(result["#p"]).toEqual(contacts); + }); + + it("should handle sloppy typing with whitespace-like patterns", () => { + const filter: NostrFilter = { + authors: ["$ME", "$me", "$Me"], + "#P": ["$CONTACTS", "$contacts", "$Contacts"], + }; + const accountPubkey = "a".repeat(64); + const contacts = ["b".repeat(64), "c".repeat(64)]; + const result = resolveFilterAliases(filter, accountPubkey, contacts); + + expect(result.authors?.length).toBe(1); + expect(result.authors).toContain(accountPubkey); + expect(result["#P"]).toEqual(contacts); }); }); diff --git a/src/lib/nostr-utils.ts b/src/lib/nostr-utils.ts index 83a4648..a3fc69f 100644 --- a/src/lib/nostr-utils.ts +++ b/src/lib/nostr-utils.ts @@ -26,7 +26,7 @@ export function getDisplayName( } /** - * Resolve $me and $contacts aliases in a Nostr filter + * Resolve $me and $contacts aliases in a Nostr filter (case-insensitive) * @param filter - Filter that may contain $me or $contacts aliases * @param accountPubkey - Current user's pubkey (for $me resolution) * @param contacts - Array of contact pubkeys (for $contacts resolution) @@ -44,11 +44,12 @@ export function resolveFilterAliases( const resolvedAuthors: string[] = []; for (const author of resolved.authors) { - if (author === "$me") { + const normalized = author.toLowerCase(); + if (normalized === "$me") { if (accountPubkey) { resolvedAuthors.push(accountPubkey); } - } else if (author === "$contacts") { + } else if (normalized === "$contacts") { resolvedAuthors.push(...contacts); } else { resolvedAuthors.push(author); @@ -64,11 +65,12 @@ export function resolveFilterAliases( const resolvedPTags: string[] = []; for (const pTag of resolved["#p"]) { - if (pTag === "$me") { + const normalized = pTag.toLowerCase(); + if (normalized === "$me") { if (accountPubkey) { resolvedPTags.push(accountPubkey); } - } else if (pTag === "$contacts") { + } else if (normalized === "$contacts") { resolvedPTags.push(...contacts); } else { resolvedPTags.push(pTag); @@ -84,11 +86,12 @@ export function resolveFilterAliases( const resolvedPTagsUppercase: string[] = []; for (const pTag of resolved["#P"]) { - if (pTag === "$me") { + const normalized = pTag.toLowerCase(); + if (normalized === "$me") { if (accountPubkey) { resolvedPTagsUppercase.push(accountPubkey); } - } else if (pTag === "$contacts") { + } else if (normalized === "$contacts") { resolvedPTagsUppercase.push(...contacts); } else { resolvedPTagsUppercase.push(pTag); diff --git a/src/lib/req-parser.test.ts b/src/lib/req-parser.test.ts index 06a4a02..1628410 100644 --- a/src/lib/req-parser.test.ts +++ b/src/lib/req-parser.test.ts @@ -725,6 +725,41 @@ describe("parseReqCommand", () => { }); }); + describe("case-insensitive aliases", () => { + it("should normalize $ME to $me in authors", () => { + const result = parseReqCommand(["-a", "$ME"]); + expect(result.filter.authors).toContain("$me"); + expect(result.needsAccount).toBe(true); + }); + + it("should normalize $CONTACTS to $contacts in authors", () => { + const result = parseReqCommand(["-a", "$CONTACTS"]); + expect(result.filter.authors).toContain("$contacts"); + expect(result.needsAccount).toBe(true); + }); + + it("should normalize mixed case $Me to $me in #p tags", () => { + const result = parseReqCommand(["-p", "$Me"]); + expect(result.filter["#p"]).toContain("$me"); + expect(result.needsAccount).toBe(true); + }); + + it("should normalize $CONTACTS to $contacts in #P tags", () => { + const result = parseReqCommand(["-P", "$CONTACTS"]); + expect(result.filter["#P"]).toContain("$contacts"); + expect(result.needsAccount).toBe(true); + }); + + it("should handle mixed case aliases with other values", () => { + const hex = "a".repeat(64); + const result = parseReqCommand(["-a", `$ME,${hex},$Contacts`]); + expect(result.filter.authors).toContain("$me"); + expect(result.filter.authors).toContain("$contacts"); + expect(result.filter.authors).toContain(hex); + expect(result.needsAccount).toBe(true); + }); + }); + describe("$me alias in #p tags (-p)", () => { it("should detect $me in #p tags", () => { const result = parseReqCommand(["-p", "$me"]); diff --git a/src/lib/req-parser.ts b/src/lib/req-parser.ts index 33c6cfe..dadf5bc 100644 --- a/src/lib/req-parser.ts +++ b/src/lib/req-parser.ts @@ -127,9 +127,10 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const values = nextArg.split(",").map((a) => a.trim()); for (const authorStr of values) { if (!authorStr) continue; - // Check for $me and $contacts aliases - if (authorStr === "$me" || authorStr === "$contacts") { - authors.add(authorStr); + // Check for $me and $contacts aliases (case-insensitive) + const normalized = authorStr.toLowerCase(); + if (normalized === "$me" || normalized === "$contacts") { + authors.add(normalized); addedAny = true; } else if (isNip05(authorStr)) { // Check if it's a NIP-05 identifier @@ -188,9 +189,10 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const values = nextArg.split(",").map((p) => p.trim()); for (const pubkeyStr of values) { if (!pubkeyStr) continue; - // Check for $me and $contacts aliases - if (pubkeyStr === "$me" || pubkeyStr === "$contacts") { - pTags.add(pubkeyStr); + // Check for $me and $contacts aliases (case-insensitive) + const normalized = pubkeyStr.toLowerCase(); + if (normalized === "$me" || normalized === "$contacts") { + pTags.add(normalized); addedAny = true; } else if (isNip05(pubkeyStr)) { // Check if it's a NIP-05 identifier @@ -223,9 +225,10 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const values = nextArg.split(",").map((p) => p.trim()); for (const pubkeyStr of values) { if (!pubkeyStr) continue; - // Check for $me and $contacts aliases - if (pubkeyStr === "$me" || pubkeyStr === "$contacts") { - pTagsUppercase.add(pubkeyStr); + // Check for $me and $contacts aliases (case-insensitive) + const normalized = pubkeyStr.toLowerCase(); + if (normalized === "$me" || normalized === "$contacts") { + pTagsUppercase.add(normalized); addedAny = true; } else if (isNip05(pubkeyStr)) { // Check if it's a NIP-05 identifier