mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 07:56:50 +02:00
* feat: add configurable zap tagging for chat messages Implements a protocol adapter interface for configuring how zap requests should be tagged for chat messages. This enables proper NIP-53 live activity zapping with appropriate a-tag and goal e-tag support. Changes: - Add ZapConfig interface to base-adapter for protocol-specific zap configuration - Add getZapConfig() method to ChatProtocolAdapter (default: unsupported) - Implement getZapConfig() in NIP-53 adapter with proper tagging: - Always a-tag the live activity (kind 30311) - If zapping host with goal, also e-tag the goal event - Add goal tag parsing to live-activity.ts and types - Update createZapRequest to accept custom tags parameter - Add Zap action to ChatMessageContextMenu (shown when supported) - Update ZapWindow to pass custom tags through to zap request - NIP-29 groups inherit default (unsupported) behavior * feat: add custom tags and relays to zap command Extends the zap command to support custom tags and relay specification, enabling full translation from chat zap config to zap command. Changes: - Add -T/--tag flag to specify custom tags (type, value, optional relay hint) - Add -r/--relay flag to specify where zap receipt should be published - Update ZapWindow to accept and pass through relays prop - Update ChatMessageContextMenu to pass relays from zapConfig - Update man page with new options and examples - Add comprehensive tests for zap parser flag handling Example usage: zap npub... -T a 30311:pk:id wss://relay.example.com zap npub... -r wss://relay1.com -r wss://relay2.com * fix: include event pointer when zapping chat messages Pass the message event as eventPointer when opening ZapWindow from chat context menu. This enables: - Event preview in the zap window - Proper window title showing "Zap [username]" * feat: add zap command reconstruction for Edit feature Add zap case to command-reconstructor.ts so that clicking "Edit" on a zap window title shows a complete command with: - Recipient as npub - Event pointer as nevent/naddr - Custom tags with -T flags - Relays with -r flags This enables users to see and modify the full zap configuration. * fix: separate eventPointer and addressPointer for proper zap tagging - Refactor createZapRequest to use separate eventPointer (for e-tag) and addressPointer (for a-tag) instead of a union type - Remove duplicate p-tag issue (only tag recipient, not event author) - Remove duplicate e-tag issue (only one e-tag with relay hint if available) - Update ZapConfig interface to include addressPointer field - Update NIP-53 adapter to return addressPointer for live activity context - Update ChatMessageContextMenu to pass addressPointer from zapConfig - Update command-reconstructor to properly serialize addressPointer as -T a - Update ZapWindow to pass addressPointer to createZapRequest This ensures proper NIP-53 zap tagging: message author gets p-tag, live activity gets a-tag, and message event gets e-tag (all separate). * refactor: move eventPointer to ZapConfig for NIP-53 adapter - Add eventPointer field to ZapConfig interface for message e-tag - NIP-53 adapter now returns eventPointer from getZapConfig - ChatMessageContextMenu uses eventPointer from zapConfig directly - Remove goal logic from NIP-53 zap config (simplify for now) This gives the adapter full control over zap configuration, including which event to reference in the e-tag. * fix: update zap-parser to return separate eventPointer and addressPointer The ParsedZapCommand interface now properly separates: - eventPointer: for regular events (nevent, note, hex ID) → e-tag - addressPointer: for addressable events (naddr) → a-tag This aligns with ZapWindowProps which expects separate fields, fixing the issue where addressPointer from naddr was being passed as eventPointer and ignored. * feat: improve relay selection for zap requests with e+a tags When both eventPointer and addressPointer are provided: - Collect outbox relays from both semantic authors - Include relay hints from both pointers - Deduplicate and use combined relay set Priority order: 1. Explicit params.relays (respects CLI -r flags) 2. Semantic author outbox relays + pointer relay hints 3. Sender read relays (fallback) 4. Aggregator relays (final fallback) * fix: pass all zap props from WindowRenderer to ZapWindow WindowRenderer was only passing recipientPubkey and eventPointer, dropping addressPointer, customTags, and relays. This caused CLI flags like -T (custom tags) and -r (relays) to be ignored. Now all parsed zap command props flow through to ZapWindow and subsequently to createZapRequest. * refactor: let createZapRequest collect relays from both authors Remove top-level relays from NIP-53 zapConfig so createZapRequest can automatically collect outbox relays from both: - eventPointer.author (message author / zap recipient) - addressPointer.pubkey (stream host) The relay hints in the pointers are still included via the existing logic in createZapRequest. * fix: deduplicate explicit relays in createZapRequest Ensure params.relays is deduplicated before use, not just the automatically collected relays. This handles cases where CLI -r flags might specify duplicate relay URLs. --------- Co-authored-by: Claude <noreply@anthropic.com>
208 lines
6.5 KiB
TypeScript
208 lines
6.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { parseZapCommand } from "./zap-parser";
|
|
|
|
describe("parseZapCommand", () => {
|
|
describe("positional arguments", () => {
|
|
it("should parse npub as recipient", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
]);
|
|
// npub decodes to this hex pubkey
|
|
expect(result.recipientPubkey).toBe(
|
|
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
|
);
|
|
});
|
|
|
|
it("should parse $me alias with active account", async () => {
|
|
const activePubkey = "abc123def456";
|
|
const result = await parseZapCommand(["$me"], activePubkey);
|
|
expect(result.recipientPubkey).toBe(activePubkey);
|
|
});
|
|
|
|
it("should throw when $me used without active account", async () => {
|
|
await expect(parseZapCommand(["$me"])).rejects.toThrow(
|
|
"No active account",
|
|
);
|
|
});
|
|
|
|
it("should throw for empty arguments", async () => {
|
|
await expect(parseZapCommand([])).rejects.toThrow(
|
|
"Recipient or event required",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("custom tags (-T, --tag)", () => {
|
|
it("should parse single custom tag with -T", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-T",
|
|
"a",
|
|
"30311:pubkey:identifier",
|
|
]);
|
|
expect(result.customTags).toEqual([["a", "30311:pubkey:identifier"]]);
|
|
});
|
|
|
|
it("should parse custom tag with --tag", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"--tag",
|
|
"e",
|
|
"abc123",
|
|
]);
|
|
expect(result.customTags).toEqual([["e", "abc123"]]);
|
|
});
|
|
|
|
it("should parse custom tag with relay hint", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-T",
|
|
"a",
|
|
"30311:pubkey:identifier",
|
|
"wss://relay.example.com",
|
|
]);
|
|
expect(result.customTags).toEqual([
|
|
["a", "30311:pubkey:identifier", "wss://relay.example.com/"],
|
|
]);
|
|
});
|
|
|
|
it("should parse multiple custom tags", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-T",
|
|
"a",
|
|
"30311:pubkey:identifier",
|
|
"-T",
|
|
"e",
|
|
"goal123",
|
|
]);
|
|
expect(result.customTags).toEqual([
|
|
["a", "30311:pubkey:identifier"],
|
|
["e", "goal123"],
|
|
]);
|
|
});
|
|
|
|
it("should throw for incomplete tag", async () => {
|
|
await expect(
|
|
parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-T",
|
|
"a",
|
|
]),
|
|
).rejects.toThrow("Tag requires at least 2 arguments");
|
|
});
|
|
|
|
it("should not include customTags when none provided", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
]);
|
|
expect(result.customTags).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("relays (-r, --relay)", () => {
|
|
it("should parse single relay with -r", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-r",
|
|
"wss://relay1.example.com",
|
|
]);
|
|
expect(result.relays).toEqual(["wss://relay1.example.com/"]);
|
|
});
|
|
|
|
it("should parse relay with --relay", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"--relay",
|
|
"wss://relay.example.com",
|
|
]);
|
|
expect(result.relays).toEqual(["wss://relay.example.com/"]);
|
|
});
|
|
|
|
it("should parse multiple relays", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-r",
|
|
"wss://relay1.example.com",
|
|
"-r",
|
|
"wss://relay2.example.com",
|
|
]);
|
|
expect(result.relays).toEqual([
|
|
"wss://relay1.example.com/",
|
|
"wss://relay2.example.com/",
|
|
]);
|
|
});
|
|
|
|
it("should throw for missing relay URL", async () => {
|
|
await expect(
|
|
parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-r",
|
|
]),
|
|
).rejects.toThrow("Relay option requires a URL");
|
|
});
|
|
|
|
it("should normalize relay URLs", async () => {
|
|
// normalizeRelayURL is liberal - it normalizes most inputs
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-r",
|
|
"relay.example.com",
|
|
]);
|
|
expect(result.relays).toEqual(["wss://relay.example.com/"]);
|
|
});
|
|
|
|
it("should not include relays when none provided", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
]);
|
|
expect(result.relays).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("combined flags", () => {
|
|
it("should parse tags and relays together", async () => {
|
|
const result = await parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"-T",
|
|
"a",
|
|
"30311:pubkey:identifier",
|
|
"-r",
|
|
"wss://relay.example.com",
|
|
"-T",
|
|
"e",
|
|
"goalid",
|
|
"wss://relay.example.com",
|
|
]);
|
|
expect(result.customTags).toEqual([
|
|
["a", "30311:pubkey:identifier"],
|
|
["e", "goalid", "wss://relay.example.com/"],
|
|
]);
|
|
expect(result.relays).toEqual(["wss://relay.example.com/"]);
|
|
});
|
|
|
|
it("should handle flags before positional args", async () => {
|
|
const result = await parseZapCommand([
|
|
"-r",
|
|
"wss://relay.example.com",
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
]);
|
|
expect(result.recipientPubkey).toBe(
|
|
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
|
);
|
|
expect(result.relays).toEqual(["wss://relay.example.com/"]);
|
|
});
|
|
});
|
|
|
|
describe("unknown options", () => {
|
|
it("should throw for unknown flags", async () => {
|
|
await expect(
|
|
parseZapCommand([
|
|
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
"--unknown",
|
|
]),
|
|
).rejects.toThrow("Unknown option: --unknown");
|
|
});
|
|
});
|
|
});
|