mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
149 lines
3.7 KiB
TypeScript
149 lines
3.7 KiB
TypeScript
import { nip19 } from "nostr-tools";
|
|
import {
|
|
isValidHexEventId,
|
|
isValidHexPubkey,
|
|
normalizeHex,
|
|
} from "./nostr-validation";
|
|
import { normalizeRelayURL } from "./relay-url";
|
|
|
|
// Define pointer types locally since they're not exported from nostr-tools
|
|
export interface EventPointer {
|
|
id: string;
|
|
relays?: string[];
|
|
author?: string;
|
|
}
|
|
|
|
export interface AddressPointer {
|
|
kind: number;
|
|
pubkey: string;
|
|
identifier: string;
|
|
relays?: string[];
|
|
}
|
|
|
|
export interface ParsedOpenCommand {
|
|
pointer: EventPointer | AddressPointer;
|
|
}
|
|
|
|
/**
|
|
* Parse OPEN command arguments into an event pointer
|
|
* Supports:
|
|
* - note1... (bech32 note)
|
|
* - nevent1... (bech32 nevent with relay hints)
|
|
* - naddr1... (bech32 naddr for addressable events)
|
|
* - abc123... (64-char hex event ID)
|
|
* - kind:pubkey:d-tag (address pointer format)
|
|
*/
|
|
export function parseOpenCommand(args: string[]): ParsedOpenCommand {
|
|
const identifier = args[0];
|
|
|
|
if (!identifier) {
|
|
throw new Error("Event identifier required");
|
|
}
|
|
|
|
// Try bech32 decode first (note, nevent, naddr)
|
|
if (
|
|
identifier.startsWith("note") ||
|
|
identifier.startsWith("nevent") ||
|
|
identifier.startsWith("naddr")
|
|
) {
|
|
try {
|
|
const decoded = nip19.decode(identifier);
|
|
|
|
if (decoded.type === "note") {
|
|
// note1... -> EventPointer with just ID
|
|
return {
|
|
pointer: {
|
|
id: decoded.data,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (decoded.type === "nevent") {
|
|
// nevent1... -> EventPointer (already has id and optional relays)
|
|
return {
|
|
pointer: {
|
|
...decoded.data,
|
|
relays: decoded.data.relays
|
|
?.map((url) => {
|
|
try {
|
|
return normalizeRelayURL(url);
|
|
} catch (error) {
|
|
console.warn(
|
|
`Skipping invalid relay hint in nevent: ${url}`,
|
|
error,
|
|
);
|
|
return null;
|
|
}
|
|
})
|
|
.filter((url): url is string => url !== null),
|
|
},
|
|
};
|
|
}
|
|
|
|
if (decoded.type === "naddr") {
|
|
// naddr1... -> AddressPointer (already has kind, pubkey, identifier)
|
|
return {
|
|
pointer: {
|
|
...decoded.data,
|
|
relays: decoded.data.relays
|
|
?.map((url) => {
|
|
try {
|
|
return normalizeRelayURL(url);
|
|
} catch (error) {
|
|
console.warn(
|
|
`Skipping invalid relay hint in naddr: ${url}`,
|
|
error,
|
|
);
|
|
return null;
|
|
}
|
|
})
|
|
.filter((url): url is string => url !== null),
|
|
},
|
|
};
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Invalid bech32 identifier: ${error}`);
|
|
}
|
|
}
|
|
|
|
// Check if it's a hex event ID
|
|
if (isValidHexEventId(identifier)) {
|
|
return {
|
|
pointer: {
|
|
id: normalizeHex(identifier),
|
|
},
|
|
};
|
|
}
|
|
|
|
// Check if it's an address format (kind:pubkey:d-tag)
|
|
if (identifier.includes(":")) {
|
|
const parts = identifier.split(":");
|
|
|
|
if (parts.length >= 2) {
|
|
const kind = parseInt(parts[0], 10);
|
|
const pubkey = parts[1];
|
|
const dTag = parts[2] || "";
|
|
|
|
if (isNaN(kind)) {
|
|
throw new Error("Invalid address format: kind must be a number");
|
|
}
|
|
|
|
if (!isValidHexPubkey(pubkey)) {
|
|
throw new Error("Invalid address format: pubkey must be 64 hex chars");
|
|
}
|
|
|
|
return {
|
|
pointer: {
|
|
kind,
|
|
pubkey: normalizeHex(pubkey),
|
|
identifier: dTag,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
"Invalid event identifier. Supported formats: note1..., nevent1..., naddr1..., hex ID, or kind:pubkey:d-tag",
|
|
);
|
|
}
|