diff --git a/src/lib/req-parser.test.ts b/src/lib/req-parser.test.ts index 670924e..3b80ad5 100644 --- a/src/lib/req-parser.test.ts +++ b/src/lib/req-parser.test.ts @@ -363,6 +363,93 @@ describe("parseReqCommand", () => { }); }); + describe("raw coordinate support (kind:pubkey:d)", () => { + it("should parse raw coordinate and populate filter['#a']", () => { + const pubkey = "a".repeat(64); + const coordinate = `30023:${pubkey}:my-article`; + const result = parseReqCommand(["-e", coordinate]); + + expect(result.filter["#a"]).toBeDefined(); + expect(result.filter["#a"]).toHaveLength(1); + expect(result.filter["#a"]).toEqual([coordinate]); + expect(result.filter["#e"]).toBeUndefined(); + }); + + it("should normalize pubkey to lowercase", () => { + const pubkey = "A".repeat(64); + const coordinate = `30023:${pubkey}:my-article`; + const result = parseReqCommand(["-e", coordinate]); + + expect(result.filter["#a"]).toEqual([ + `30023:${"a".repeat(64)}:my-article`, + ]); + }); + + it("should handle empty d-tag identifier", () => { + const pubkey = "a".repeat(64); + const coordinate = `30023:${pubkey}:`; + const result = parseReqCommand(["-e", coordinate]); + + expect(result.filter["#a"]).toEqual([coordinate]); + }); + + it("should handle d-tag with special characters", () => { + const pubkey = "a".repeat(64); + const coordinate = `30023:${pubkey}:my-article/with:special-chars`; + const result = parseReqCommand(["-e", coordinate]); + + expect(result.filter["#a"]).toEqual([coordinate]); + }); + + it("should handle different kind numbers", () => { + const pubkey = "a".repeat(64); + const result = parseReqCommand([ + "-e", + `0:${pubkey}:,30000:${pubkey}:list,30023:${pubkey}:article`, + ]); + + expect(result.filter["#a"]).toHaveLength(3); + expect(result.filter["#a"]).toContain(`0:${pubkey}:`); + expect(result.filter["#a"]).toContain(`30000:${pubkey}:list`); + expect(result.filter["#a"]).toContain(`30023:${pubkey}:article`); + }); + + it("should combine with naddr coordinates", () => { + const pubkey = "a".repeat(64); + const rawCoord = `30023:${pubkey}:raw-article`; + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: pubkey, + identifier: "encoded-article", + }); + + const result = parseReqCommand(["-e", `${rawCoord},${naddr}`]); + + expect(result.filter["#a"]).toHaveLength(2); + expect(result.filter["#a"]).toContain(rawCoord); + expect(result.filter["#a"]).toContain( + `30023:${pubkey}:encoded-article`, + ); + }); + + it("should ignore invalid coordinate formats", () => { + // Missing parts + const result1 = parseReqCommand(["-e", "30023:abc"]); + expect(result1.filter["#a"]).toBeUndefined(); + + // Invalid pubkey (not 64 hex chars) + const result2 = parseReqCommand(["-e", "30023:abc123:article"]); + expect(result2.filter["#a"]).toBeUndefined(); + + // Invalid kind (not a number) + const result3 = parseReqCommand([ + "-e", + `abc:${"a".repeat(64)}:article`, + ]); + expect(result3.filter["#a"]).toBeUndefined(); + }); + }); + describe("mixed format support", () => { it("should handle comma-separated mix of all formats (all to tags)", () => { const hex = "a".repeat(64); diff --git a/src/lib/req-parser.ts b/src/lib/req-parser.ts index 1cc6e14..d233572 100644 --- a/src/lib/req-parser.ts +++ b/src/lib/req-parser.ts @@ -679,6 +679,20 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null { }; } + // Raw coordinate: kind:pubkey:identifier → #a tag + // Format: :: (e.g., 30023:abc123...:article-name) + const coordinateMatch = value.match(/^(\d+):([a-fA-F0-9]{64}):(.*)$/); + if (coordinateMatch) { + const [, kindStr, pubkey, identifier] = coordinateMatch; + const kind = parseInt(kindStr, 10); + if (!isNaN(kind)) { + return { + type: "tag-address", + value: `${kind}:${pubkey.toLowerCase()}:${identifier}`, + }; + } + } + return null; } diff --git a/src/types/man.ts b/src/types/man.ts index fb03990..7e30314 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -181,9 +181,9 @@ export const manPages: Record = { "Direct event lookup by ID (filter.ids). Fetch specific events by their ID. Supports note1, nevent1 (with relay hints), or raw hex. Comma-separated values supported: -i note1...,nevent1...,abc123...", }, { - flag: "-e ", + flag: "-e ", description: - "Tag-based filtering (#e/#a tags). Find events that reference the specified events or addresses. Supports note1, nevent1, naddr1, or raw hex. Comma-separated values supported: -e note1...,naddr1...", + "Tag-based filtering (#e/#a tags). Find events that reference the specified events or addresses. Supports note1, nevent1, naddr1, raw coordinates (kind:pubkey:d-tag), or hex. Comma-separated values supported: -e note1...,30023:pubkey:article", }, { flag: "-p ", @@ -264,6 +264,7 @@ export const manPages: Record = { "req -i note1abc123... Direct lookup: fetch event by ID", "req -i nevent1... Direct lookup: fetch event by nevent (uses relay hints)", "req -e note1abc123... -k 1 Tag filtering: find notes that reply to or reference event", + "req -e 30023:pubkey...:article-name -k 1,7 Tag filtering: find events referencing addressable event", "req -t nostr,grimoire,bitcoin -l 50 Get 50 events tagged #nostr, #grimoire, or #bitcoin", "req --tag a 30023:7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194:grimoire Get events referencing addressable event (#a tag)", "req -T r grimoire.rocks Get events referencing URL (#r tag)",