mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-04 09:41:13 +02:00
Add ID filtering to REQ command (#202)
* feat: add -i/--id flag for direct event ID filtering in REQ command Add a new -i/--id flag for direct event lookup via filter.ids, and clarify -e flag behavior for tag-based filtering (#e/#a tags). Changes: - Add -i/--id flag: accepts note1, nevent, or hex event IDs for direct lookup - Clarify -e flag: now always routes to #e/#a tags (including nevent) - Update man page with new flag documentation and examples - Add comprehensive tests for the new -i/--id flag This aligns with nak's behavior where -i is for direct ID filtering and -e is for tag-based event references. * feat: support raw coordinate format (kind:pubkey:d) in -e flag The -e flag now accepts raw a-tag coordinates like `30023:pubkey:article` in addition to naddr bech32 encoding. Both route to #a tag filtering. Examples: - req -e 30023:abc123...:my-article # Raw coordinate - req -e naddr1... # Bech32 encoded (same effect) * feat: add event ID previews in REQ viewer query dropdown When using -i/--id flag for direct event lookup, the query dropdown now shows clickable event ID previews (truncated hex). Click any ID to open the event detail view. Works in both accordion (complex queries) and simple card views. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
Sparkles,
|
||||
Link as LinkIcon,
|
||||
Check,
|
||||
Target,
|
||||
} from "lucide-react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced";
|
||||
@@ -90,6 +91,27 @@ const MemoizedFeedEvent = memo(
|
||||
(prev, next) => prev.event.id === next.event.id,
|
||||
);
|
||||
|
||||
/**
|
||||
* Compact event ID display in query dropdown
|
||||
* Shows truncated ID with click to open
|
||||
*/
|
||||
function EventIdPreview({ eventId }: { eventId: string }) {
|
||||
const { addWindow } = useGrimoire();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addWindow("open", { pointer: { id: eventId } });
|
||||
}, [eventId, addWindow]);
|
||||
|
||||
return (
|
||||
<code
|
||||
className="text-xs font-mono cursor-crosshair hover:text-primary transition-colors"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{eventId.slice(0, 8)}...{eventId.slice(-4)}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
interface ReqViewerProps {
|
||||
filter: NostrFilter;
|
||||
relays?: string[];
|
||||
@@ -120,11 +142,15 @@ function QueryDropdown({
|
||||
const { copy: handleCopy, copied } = useCopy();
|
||||
|
||||
// Expandable lists state
|
||||
const [showAllIds, setShowAllIds] = useState(false);
|
||||
const [showAllAuthors, setShowAllAuthors] = useState(false);
|
||||
const [showAllPTags, setShowAllPTags] = useState(false);
|
||||
const [showAllETags, setShowAllETags] = useState(false);
|
||||
const [showAllTTags, setShowAllTTags] = useState(false);
|
||||
|
||||
// Get IDs for direct lookup (from -i flag)
|
||||
const eventIds = filter.ids || [];
|
||||
|
||||
// Get pubkeys for authors and #p tags
|
||||
const authorPubkeys = filter.authors || [];
|
||||
const pTagPubkeys = filter["#p"] || [];
|
||||
@@ -154,6 +180,7 @@ function QueryDropdown({
|
||||
// Determine if we should use accordion for complex queries
|
||||
const isComplexQuery =
|
||||
(filter.kinds?.length || 0) +
|
||||
eventIds.length +
|
||||
authorPubkeys.length +
|
||||
(filter.search ? 1 : 0) +
|
||||
tagCount >
|
||||
@@ -170,6 +197,7 @@ function QueryDropdown({
|
||||
type="multiple"
|
||||
defaultValue={[
|
||||
"kinds",
|
||||
"ids",
|
||||
"authors",
|
||||
"mentions",
|
||||
"time",
|
||||
@@ -203,6 +231,35 @@ function QueryDropdown({
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* IDs Section (direct event lookup) */}
|
||||
{eventIds.length > 0 && (
|
||||
<AccordionItem value="ids" className="border-0">
|
||||
<AccordionTrigger className="py-2 hover:no-underline">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold">
|
||||
<Target className="size-3.5 text-muted-foreground" />
|
||||
Event IDs ({eventIds.length})
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-1 ml-5">
|
||||
{eventIds
|
||||
.slice(0, showAllIds ? undefined : 3)
|
||||
.map((eventId) => (
|
||||
<EventIdPreview key={eventId} eventId={eventId} />
|
||||
))}
|
||||
{eventIds.length > 3 && (
|
||||
<button
|
||||
onClick={() => setShowAllIds(!showAllIds)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
{showAllIds ? "Show less" : `Show all ${eventIds.length}`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* Time Range Section */}
|
||||
{(filter.since || filter.until) && (
|
||||
<AccordionItem value="time" className="border-0">
|
||||
@@ -469,6 +526,31 @@ function QueryDropdown({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Event IDs (direct lookup) */}
|
||||
{eventIds.length > 0 && (
|
||||
<div className="">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold mb-1.5">
|
||||
<Target className="size-3.5 text-muted-foreground" />
|
||||
Event IDs ({eventIds.length})
|
||||
</div>
|
||||
<div className="ml-5 space-y-1">
|
||||
{eventIds
|
||||
.slice(0, showAllIds ? undefined : 3)
|
||||
.map((eventId) => (
|
||||
<EventIdPreview key={eventId} eventId={eventId} />
|
||||
))}
|
||||
{eventIds.length > 3 && (
|
||||
<button
|
||||
onClick={() => setShowAllIds(!showAllIds)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
{showAllIds ? "Show less" : `Show all ${eventIds.length}`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Time Range */}
|
||||
{(filter.since || filter.until) && (
|
||||
<div className="">
|
||||
|
||||
@@ -216,8 +216,8 @@ describe("parseReqCommand", () => {
|
||||
});
|
||||
|
||||
describe("event ID flag (-e) with nevent/naddr support", () => {
|
||||
describe("nevent support", () => {
|
||||
it("should parse nevent and populate filter.ids", () => {
|
||||
describe("nevent support (tag filtering)", () => {
|
||||
it("should parse nevent and populate filter['#e'] (tag filtering)", () => {
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const nevent = nip19.neventEncode({
|
||||
@@ -225,10 +225,10 @@ describe("parseReqCommand", () => {
|
||||
});
|
||||
const result = parseReqCommand(["-e", nevent]);
|
||||
|
||||
expect(result.filter.ids).toBeDefined();
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
expect(result.filter.ids).toEqual([eventId]);
|
||||
expect(result.filter["#e"]).toBeUndefined();
|
||||
expect(result.filter["#e"]).toBeDefined();
|
||||
expect(result.filter["#e"]).toHaveLength(1);
|
||||
expect(result.filter["#e"]).toEqual([eventId]);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should extract relay hints from nevent", () => {
|
||||
@@ -267,7 +267,7 @@ describe("parseReqCommand", () => {
|
||||
});
|
||||
const result = parseReqCommand(["-e", nevent]);
|
||||
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
expect(result.filter["#e"]).toHaveLength(1);
|
||||
expect(result.relays).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -363,8 +363,95 @@ 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", () => {
|
||||
it("should handle comma-separated mix of all formats (all to tags)", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
@@ -383,14 +470,13 @@ describe("parseReqCommand", () => {
|
||||
`${hex},${note},${nevent},${naddr}`,
|
||||
]);
|
||||
|
||||
// hex and note should go to filter["#e"]
|
||||
// hex, note, and nevent all go to filter["#e"] (deduplicated: eventId appears twice)
|
||||
expect(result.filter["#e"]).toHaveLength(2);
|
||||
expect(result.filter["#e"]).toContain(hex);
|
||||
expect(result.filter["#e"]).toContain(eventId);
|
||||
|
||||
// nevent should go to filter.ids
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
expect(result.filter.ids).toContain(eventId);
|
||||
// No direct ID lookup for -e flag
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
|
||||
// naddr should go to filter["#a"]
|
||||
expect(result.filter["#a"]).toHaveLength(1);
|
||||
@@ -408,8 +494,9 @@ describe("parseReqCommand", () => {
|
||||
|
||||
const result = parseReqCommand(["-e", `${nevent1},${nevent2}`]);
|
||||
|
||||
// Both nevent decode to same event ID, should deduplicate
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
// Both nevent decode to same event ID, should deduplicate in #e
|
||||
expect(result.filter["#e"]).toHaveLength(1);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should collect relay hints from mixed formats", () => {
|
||||
@@ -447,10 +534,12 @@ describe("parseReqCommand", () => {
|
||||
|
||||
const result = parseReqCommand(["-e", `${nevent1},${hex}`]);
|
||||
|
||||
// nevent goes to filter.ids
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
// hex goes to filter["#e"]
|
||||
// Both nevent and hex go to filter["#e"]
|
||||
expect(result.filter["#e"]).toHaveLength(2);
|
||||
expect(result.filter["#e"]).toContain(eventId);
|
||||
expect(result.filter["#e"]).toContain(hex);
|
||||
// No direct ID lookup for -e flag
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
// relays extracted from nevent
|
||||
expect(result.relays).toBeDefined();
|
||||
expect(result.relays).toContain("wss://relay.damus.io/");
|
||||
@@ -499,7 +588,8 @@ describe("parseReqCommand", () => {
|
||||
const result = parseReqCommand(["-k", "1", "-e", nevent]);
|
||||
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
expect(result.filter["#e"]).toHaveLength(1);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should work with explicit relays", () => {
|
||||
@@ -539,13 +629,199 @@ describe("parseReqCommand", () => {
|
||||
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
expect(result.filter.authors).toEqual([hex]);
|
||||
expect(result.filter.ids).toHaveLength(1);
|
||||
expect(result.filter["#e"]).toHaveLength(1);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
expect(result.filter.since).toBeDefined();
|
||||
expect(result.filter.limit).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("direct ID lookup flag (-i, --id)", () => {
|
||||
describe("basic parsing", () => {
|
||||
it("should parse hex event ID to filter.ids", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["-i", hex]);
|
||||
expect(result.filter.ids).toEqual([hex]);
|
||||
});
|
||||
|
||||
it("should parse note to filter.ids", () => {
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const note = nip19.noteEncode(eventId);
|
||||
const result = parseReqCommand(["-i", note]);
|
||||
expect(result.filter.ids).toEqual([eventId]);
|
||||
});
|
||||
|
||||
it("should parse nevent to filter.ids", () => {
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const nevent = nip19.neventEncode({ id: eventId });
|
||||
const result = parseReqCommand(["-i", nevent]);
|
||||
expect(result.filter.ids).toEqual([eventId]);
|
||||
});
|
||||
|
||||
it("should handle --id long form", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["--id", hex]);
|
||||
expect(result.filter.ids).toEqual([hex]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("comma-separated values", () => {
|
||||
it("should parse comma-separated hex IDs", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
const result = parseReqCommand(["-i", `${hex1},${hex2}`]);
|
||||
expect(result.filter.ids).toEqual([hex1, hex2]);
|
||||
});
|
||||
|
||||
it("should parse comma-separated mixed formats", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const note = nip19.noteEncode(eventId);
|
||||
const nevent = nip19.neventEncode({ id: eventId });
|
||||
|
||||
const result = parseReqCommand(["-i", `${hex},${note},${nevent}`]);
|
||||
|
||||
// hex is unique, note and nevent decode to same eventId (deduplicated)
|
||||
expect(result.filter.ids).toHaveLength(2);
|
||||
expect(result.filter.ids).toContain(hex);
|
||||
expect(result.filter.ids).toContain(eventId);
|
||||
});
|
||||
|
||||
it("should deduplicate IDs", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["-i", `${hex},${hex}`]);
|
||||
expect(result.filter.ids).toEqual([hex]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relay hints", () => {
|
||||
it("should extract relay hints from nevent", () => {
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const nevent = nip19.neventEncode({
|
||||
id: eventId,
|
||||
relays: ["wss://relay.damus.io"],
|
||||
});
|
||||
const result = parseReqCommand(["-i", nevent]);
|
||||
|
||||
expect(result.filter.ids).toEqual([eventId]);
|
||||
expect(result.relays).toBeDefined();
|
||||
expect(result.relays).toContain("wss://relay.damus.io/");
|
||||
});
|
||||
|
||||
it("should normalize relay URLs from nevent", () => {
|
||||
const eventId =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const nevent = nip19.neventEncode({
|
||||
id: eventId,
|
||||
relays: ["wss://relay.damus.io"],
|
||||
});
|
||||
const result = parseReqCommand(["-i", nevent]);
|
||||
|
||||
result.relays?.forEach((url) => {
|
||||
expect(url).toMatch(/^wss?:\/\//);
|
||||
expect(url).toMatch(/\/$/);
|
||||
});
|
||||
});
|
||||
|
||||
it("should collect relay hints from multiple nevents", () => {
|
||||
const eventId1 =
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||
const eventId2 = "b".repeat(64);
|
||||
const nevent1 = nip19.neventEncode({
|
||||
id: eventId1,
|
||||
relays: ["wss://relay.damus.io"],
|
||||
});
|
||||
const nevent2 = nip19.neventEncode({
|
||||
id: eventId2,
|
||||
relays: ["wss://nos.lol"],
|
||||
});
|
||||
|
||||
const result = parseReqCommand(["-i", `${nevent1},${nevent2}`]);
|
||||
|
||||
expect(result.relays).toBeDefined();
|
||||
expect(result.relays).toContain("wss://relay.damus.io/");
|
||||
expect(result.relays).toContain("wss://nos.lol/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should ignore invalid bech32", () => {
|
||||
const result = parseReqCommand(["-i", "note1invalid"]);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should ignore invalid nevent", () => {
|
||||
const result = parseReqCommand(["-i", "nevent1invalid"]);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should ignore naddr (not valid for direct ID lookup)", () => {
|
||||
const pubkey = "b".repeat(64);
|
||||
const naddr = nip19.naddrEncode({
|
||||
kind: 30023,
|
||||
pubkey: pubkey,
|
||||
identifier: "test-article",
|
||||
});
|
||||
const result = parseReqCommand(["-i", naddr]);
|
||||
expect(result.filter.ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should skip empty values", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["-i", `${hex},,`]);
|
||||
expect(result.filter.ids).toEqual([hex]);
|
||||
});
|
||||
|
||||
it("should continue parsing after invalid values", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
const result = parseReqCommand(["-i", `${hex1},invalid,${hex2}`]);
|
||||
expect(result.filter.ids).toEqual([hex1, hex2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration with other flags", () => {
|
||||
it("should work alongside -e flag (both IDs and tags)", () => {
|
||||
const directId = "a".repeat(64);
|
||||
const tagEventId = "b".repeat(64);
|
||||
|
||||
const result = parseReqCommand(["-i", directId, "-e", tagEventId]);
|
||||
|
||||
expect(result.filter.ids).toEqual([directId]);
|
||||
expect(result.filter["#e"]).toEqual([tagEventId]);
|
||||
});
|
||||
|
||||
it("should work with kind and limit", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["-i", hex, "-k", "1", "-l", "10"]);
|
||||
|
||||
expect(result.filter.ids).toEqual([hex]);
|
||||
expect(result.filter.kinds).toEqual([1]);
|
||||
expect(result.filter.limit).toBe(10);
|
||||
});
|
||||
|
||||
it("should work with relays", () => {
|
||||
const hex = "a".repeat(64);
|
||||
const result = parseReqCommand(["-i", hex, "wss://relay.example.com"]);
|
||||
|
||||
expect(result.filter.ids).toEqual([hex]);
|
||||
expect(result.relays).toContain("wss://relay.example.com/");
|
||||
});
|
||||
|
||||
it("should accumulate across multiple -i flags", () => {
|
||||
const hex1 = "a".repeat(64);
|
||||
const hex2 = "b".repeat(64);
|
||||
const result = parseReqCommand(["-i", hex1, "-i", hex2]);
|
||||
expect(result.filter.ids).toEqual([hex1, hex2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pubkey tag flag (-p)", () => {
|
||||
it("should parse hex pubkey for #p tag", () => {
|
||||
const hex = "a".repeat(64);
|
||||
|
||||
@@ -51,7 +51,7 @@ function parseCommaSeparated<T>(
|
||||
/**
|
||||
* Parse REQ command arguments into a Nostr filter
|
||||
* Supports:
|
||||
* - Filters: -k (kinds), -a (authors: hex/npub/nprofile/NIP-05), -l (limit), -e (note/nevent/naddr/hex), -p (#p: hex/npub/nprofile/NIP-05), -P (#P: hex/npub/nprofile/NIP-05), -t (#t), -d (#d), --tag/-T (any #tag)
|
||||
* - Filters: -k (kinds), -a (authors: hex/npub/nprofile/NIP-05), -l (limit), -i/--id (direct event lookup), -e (tag filtering: #e/#a), -p (#p: hex/npub/nprofile/NIP-05), -P (#P: hex/npub/nprofile/NIP-05), -t (#t), -d (#d), --tag/-T (any #tag)
|
||||
* - Time: --since, --until
|
||||
* - Search: --search
|
||||
* - Relays: wss://relay.com or relay.com (auto-adds wss://), relay hints from nprofile/nevent/naddr are automatically extracted
|
||||
@@ -183,8 +183,40 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
break;
|
||||
}
|
||||
|
||||
case "-i":
|
||||
case "--id": {
|
||||
// Direct event lookup via filter.ids
|
||||
// Support comma-separated: -i note1...,nevent1...,hex
|
||||
if (!nextArg) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
|
||||
let addedAny = false;
|
||||
const values = nextArg.split(",").map((v) => v.trim());
|
||||
|
||||
for (const val of values) {
|
||||
if (!val) continue;
|
||||
|
||||
const parsed = parseIdIdentifier(val);
|
||||
if (parsed) {
|
||||
ids.add(parsed.id);
|
||||
addedAny = true;
|
||||
|
||||
// Collect relay hints from nevent
|
||||
if (parsed.relays) {
|
||||
relays.push(...parsed.relays);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += addedAny ? 2 : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
case "-e": {
|
||||
// Support comma-separated event identifiers: -e note1...,nevent1...,naddr1...,hex
|
||||
// Tag-based filtering: -e note1...,nevent1...,naddr1...,hex
|
||||
// Events go to #e tag, addresses go to #a tag
|
||||
if (!nextArg) {
|
||||
i++;
|
||||
break;
|
||||
@@ -198,13 +230,11 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
|
||||
|
||||
const parsed = parseEventIdentifier(val);
|
||||
if (parsed) {
|
||||
// Route to appropriate filter field based on type
|
||||
if (parsed.type === "direct-event") {
|
||||
ids.add(parsed.value);
|
||||
} else if (parsed.type === "direct-address") {
|
||||
aTags.add(parsed.value);
|
||||
} else if (parsed.type === "tag-event") {
|
||||
// Route to appropriate tag filter based on type
|
||||
if (parsed.type === "tag-event") {
|
||||
eventIds.add(parsed.value);
|
||||
} else if (parsed.type === "tag-address") {
|
||||
aTags.add(parsed.value);
|
||||
}
|
||||
|
||||
// Collect relay hints
|
||||
@@ -564,24 +594,26 @@ function parseNpubOrHex(value: string): {
|
||||
}
|
||||
|
||||
interface ParsedEventIdentifier {
|
||||
type: "direct-event" | "direct-address" | "tag-event";
|
||||
type: "tag-event" | "tag-address";
|
||||
value: string;
|
||||
relays?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse event identifier - supports note, nevent, naddr, and hex event ID
|
||||
* Parse event identifier for -e flag (tag filtering)
|
||||
* All event IDs go to #e, addresses go to #a
|
||||
* Supports: note, nevent, naddr, and hex event ID
|
||||
*/
|
||||
function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
|
||||
if (!value) return null;
|
||||
|
||||
// nevent: direct event lookup with relay hints
|
||||
// nevent: decode and route to #e tag
|
||||
if (value.startsWith("nevent")) {
|
||||
try {
|
||||
const decoded = nip19.decode(value);
|
||||
if (decoded.type === "nevent") {
|
||||
return {
|
||||
type: "direct-event",
|
||||
type: "tag-event",
|
||||
value: decoded.data.id,
|
||||
relays: decoded.data.relays
|
||||
?.map((url) => {
|
||||
@@ -599,14 +631,14 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
|
||||
}
|
||||
}
|
||||
|
||||
// naddr: coordinate-based lookup with relay hints
|
||||
// naddr: coordinate-based lookup with relay hints → #a tag
|
||||
if (value.startsWith("naddr")) {
|
||||
try {
|
||||
const decoded = nip19.decode(value);
|
||||
if (decoded.type === "naddr") {
|
||||
const coordinate = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`;
|
||||
return {
|
||||
type: "direct-address",
|
||||
type: "tag-address",
|
||||
value: coordinate,
|
||||
relays: decoded.data.relays
|
||||
?.map((url) => {
|
||||
@@ -624,7 +656,7 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
|
||||
}
|
||||
}
|
||||
|
||||
// note1: tag-based filtering (existing behavior)
|
||||
// note1: decode to event ID → #e tag
|
||||
if (value.startsWith("note")) {
|
||||
try {
|
||||
const decoded = nip19.decode(value);
|
||||
@@ -639,7 +671,7 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
|
||||
}
|
||||
}
|
||||
|
||||
// Hex: tag-based filtering (existing behavior)
|
||||
// Hex: → #e tag
|
||||
if (isValidHexEventId(value)) {
|
||||
return {
|
||||
type: "tag-event",
|
||||
@@ -647,5 +679,78 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
|
||||
};
|
||||
}
|
||||
|
||||
// Raw coordinate: kind:pubkey:identifier → #a tag
|
||||
// Format: <kind>:<pubkey>:<d-tag> (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;
|
||||
}
|
||||
|
||||
interface ParsedIdIdentifier {
|
||||
id: string;
|
||||
relays?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse event identifier for -i/--id flag (direct ID lookup via filter.ids)
|
||||
* Supports: note, nevent, and hex event ID
|
||||
*/
|
||||
function parseIdIdentifier(value: string): ParsedIdIdentifier | null {
|
||||
if (!value) return null;
|
||||
|
||||
// nevent: decode and extract event ID
|
||||
if (value.startsWith("nevent")) {
|
||||
try {
|
||||
const decoded = nip19.decode(value);
|
||||
if (decoded.type === "nevent") {
|
||||
return {
|
||||
id: decoded.data.id,
|
||||
relays: decoded.data.relays
|
||||
?.map((url) => {
|
||||
try {
|
||||
return normalizeRelayURL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((url): url is string => url !== null),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Not valid nevent, continue
|
||||
}
|
||||
}
|
||||
|
||||
// note1: decode to event ID
|
||||
if (value.startsWith("note")) {
|
||||
try {
|
||||
const decoded = nip19.decode(value);
|
||||
if (decoded.type === "note") {
|
||||
return {
|
||||
id: decoded.data,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Not valid note, continue
|
||||
}
|
||||
}
|
||||
|
||||
// Hex event ID
|
||||
if (isValidHexEventId(value)) {
|
||||
return {
|
||||
id: normalizeHex(value),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -176,9 +176,14 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
description: "Maximum number of events to return",
|
||||
},
|
||||
{
|
||||
flag: "-e <note|nevent|naddr|hex>",
|
||||
flag: "-i, --id <note|nevent|hex>",
|
||||
description:
|
||||
"Filter by event ID or coordinate. Supports note1 (bare event ID), nevent1 (event with relay hints), naddr1 (addressable event coordinate), or raw hex. Comma-separated values supported: -e note1...,nevent1...,naddr1...",
|
||||
"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 <note|nevent|naddr|coordinate|hex>",
|
||||
description:
|
||||
"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 <npub|hex|nip05|$me|$contacts>",
|
||||
@@ -256,6 +261,10 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
"req -k 1 --since 1h relay.damus.io Get notes from last hour (manual relay override)",
|
||||
"req -k 1 --since 7d --until now Get notes from last week up to now",
|
||||
"req -k 1 --close-on-eose Get recent notes and close after EOSE",
|
||||
"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)",
|
||||
|
||||
Reference in New Issue
Block a user