diff --git a/src/lib/nip89-helpers.test.ts b/src/lib/nip89-helpers.test.ts index 9fe6138..eb49b57 100644 --- a/src/lib/nip89-helpers.test.ts +++ b/src/lib/nip89-helpers.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { getAppName, getAppDescription, - getAppImage, + getAppWebsite, getSupportedKinds, getPlatformUrls, getAvailablePlatforms, @@ -10,11 +10,7 @@ import { getRecommendedKind, parseAddressPointer, getHandlerReferences, - getHandlersByPlatform, getRecommendedPlatforms, - substituteTemplate, - hasPlaceholder, - formatAddressPointer, } from "./nip89-helpers"; import { NostrEvent } from "@/types/nostr"; @@ -119,29 +115,24 @@ describe("Kind 31990 (Application Handler) Helpers", () => { }); }); - describe("getAppImage", () => { - it("should extract image from content JSON", () => { + describe("getAppWebsite", () => { + it("should extract website from content JSON", () => { const event = createHandlerEvent({ - content: JSON.stringify({ image: "https://example.com/logo.png" }), + content: JSON.stringify({ website: "https://example.com" }), }); - expect(getAppImage(event)).toBe("https://example.com/logo.png"); + expect(getAppWebsite(event)).toBe("https://example.com"); }); - it("should extract picture field as fallback", () => { + it("should return undefined if no website field", () => { const event = createHandlerEvent({ - content: JSON.stringify({ picture: "https://example.com/pic.png" }), + content: JSON.stringify({ name: "App" }), }); - expect(getAppImage(event)).toBe("https://example.com/pic.png"); + expect(getAppWebsite(event)).toBeUndefined(); }); - it("should prefer image over picture", () => { - const event = createHandlerEvent({ - content: JSON.stringify({ - image: "https://example.com/logo.png", - picture: "https://example.com/pic.png", - }), - }); - expect(getAppImage(event)).toBe("https://example.com/logo.png"); + it("should return undefined if content is empty", () => { + const event = createHandlerEvent({ content: "" }); + expect(getAppWebsite(event)).toBeUndefined(); }); }); @@ -351,39 +342,6 @@ describe("Kind 31989 (Handler Recommendation) Helpers", () => { }); }); - describe("getHandlersByPlatform", () => { - it("should filter handlers by platform", () => { - const event = createRecommendationEvent({ - tags: [ - ["d", "9802"], - ["a", "31990:pubkey1:handler1", "", "web"], - ["a", "31990:pubkey2:handler2", "", "ios"], - ["a", "31990:pubkey3:handler3", "", "web"], - ], - }); - const webHandlers = getHandlersByPlatform(event, "web"); - expect(webHandlers).toHaveLength(2); - expect(webHandlers[0].platform).toBe("web"); - expect(webHandlers[1].platform).toBe("web"); - - const iosHandlers = getHandlersByPlatform(event, "ios"); - expect(iosHandlers).toHaveLength(1); - expect(iosHandlers[0].platform).toBe("ios"); - }); - - it("should return all handlers if no platform specified", () => { - const event = createRecommendationEvent({ - tags: [ - ["d", "9802"], - ["a", "31990:pubkey1:handler1", "", "web"], - ["a", "31990:pubkey2:handler2", "", "ios"], - ], - }); - const allHandlers = getHandlersByPlatform(event); - expect(allHandlers).toHaveLength(2); - }); - }); - describe("getRecommendedPlatforms", () => { it("should return unique platforms from handler references", () => { const event = createRecommendationEvent({ @@ -410,46 +368,3 @@ describe("Kind 31989 (Handler Recommendation) Helpers", () => { }); }); }); - -describe("URL Template Utilities", () => { - describe("substituteTemplate", () => { - it("should replace placeholder with entity", () => { - const template = "https://app.com/view/"; - const result = substituteTemplate(template, "nevent1abc123"); - expect(result).toBe("https://app.com/view/nevent1abc123"); - }); - - it("should replace multiple occurrences", () => { - const template = "https://app.com//view/"; - const result = substituteTemplate(template, "note1xyz"); - expect(result).toBe("https://app.com/note1xyz/view/note1xyz"); - }); - - it("should return unchanged if no placeholder", () => { - const template = "https://app.com/view"; - const result = substituteTemplate(template, "nevent1abc"); - expect(result).toBe("https://app.com/view"); - }); - }); - - describe("hasPlaceholder", () => { - it("should return true if template contains ", () => { - expect(hasPlaceholder("https://app.com/")).toBe(true); - }); - - it("should return false if template does not contain ", () => { - expect(hasPlaceholder("https://app.com/view")).toBe(false); - }); - }); - - describe("formatAddressPointer", () => { - it("should format address pointer as string", () => { - const pointer = { - kind: 31990, - pubkey: "abcd1234", - identifier: "my-handler", - }; - expect(formatAddressPointer(pointer)).toBe("31990:abcd1234:my-handler"); - }); - }); -}); diff --git a/src/lib/nip89-helpers.ts b/src/lib/nip89-helpers.ts index 0df2124..79a0085 100644 --- a/src/lib/nip89-helpers.ts +++ b/src/lib/nip89-helpers.ts @@ -22,27 +22,41 @@ function getTagValues(event: NostrEvent, tagName: string): string[] { // Kind 31990 (Application Handler) Helpers // ============================================================================ +/** + * Get parsed metadata from kind 31990 event content JSON + * Caches the parsed result to avoid redundant JSON.parse calls + */ +function getAppMetadata(event: NostrEvent): Record | null { + if (event.kind !== 31990 || !event.content) return null; + + // Use a symbol as cache key to avoid property name conflicts + const cacheKey = Symbol.for("nip89-metadata"); + const cached = (event as any)[cacheKey]; + if (cached !== undefined) return cached; + + try { + const metadata = JSON.parse(event.content); + if (metadata && typeof metadata === "object") { + (event as any)[cacheKey] = metadata; + return metadata; + } + } catch { + // Invalid JSON + } + + (event as any)[cacheKey] = null; + return null; +} + /** * Extract app name from kind 31990 event content JSON or fallback to d tag */ export function getAppName(event: NostrEvent): string { if (event.kind !== 31990) return ""; - // Try to parse content as JSON - if (event.content) { - try { - const metadata = JSON.parse(event.content); - if ( - metadata && - typeof metadata === "object" && - metadata.name && - typeof metadata.name === "string" - ) { - return metadata.name; - } - } catch { - // Not valid JSON, continue to fallback - } + const metadata = getAppMetadata(event); + if (metadata?.name && typeof metadata.name === "string") { + return metadata.name; } // Fallback to d tag identifier @@ -55,40 +69,31 @@ export function getAppName(event: NostrEvent): string { * Checks both 'description' and 'about' fields */ export function getAppDescription(event: NostrEvent): string | undefined { - if (event.kind !== 31990 || !event.content) return undefined; + if (event.kind !== 31990) return undefined; - try { - const metadata = JSON.parse(event.content); - if (metadata && typeof metadata === "object") { - // Check description first, then about (common in kind 0 profile format) - const desc = metadata.description || metadata.about; - if (desc && typeof desc === "string") { - return desc; - } + const metadata = getAppMetadata(event); + if (metadata) { + // Check description first, then about (common in kind 0 profile format) + const desc = metadata.description || metadata.about; + if (desc && typeof desc === "string") { + return desc; } - } catch { - // Invalid JSON } + return undefined; } /** - * Extract app image URL from kind 31990 event content JSON + * Extract website URL from kind 31990 event content JSON */ -export function getAppImage(event: NostrEvent): string | undefined { - if (event.kind !== 31990 || !event.content) return undefined; +export function getAppWebsite(event: NostrEvent): string | undefined { + if (event.kind !== 31990) return undefined; - try { - const metadata = JSON.parse(event.content); - if (metadata && typeof metadata === "object") { - const image = metadata.image || metadata.picture; - if (image && typeof image === "string") { - return image; - } - } - } catch { - // Invalid JSON + const metadata = getAppMetadata(event); + if (metadata?.website && typeof metadata.website === "string") { + return metadata.website; } + return undefined; } @@ -150,26 +155,6 @@ export function getAvailablePlatforms(event: NostrEvent): string[] { return Object.keys(getPlatformUrls(event)); } -/** - * Extract website URL from kind 31990 event content JSON - */ -export function getAppWebsite(event: NostrEvent): string | undefined { - if (event.kind !== 31990 || !event.content) return undefined; - - try { - const metadata = JSON.parse(event.content); - if (metadata && typeof metadata === "object") { - const website = metadata.website; - if (website && typeof website === "string") { - return website; - } - } - } catch { - // Invalid JSON - } - return undefined; -} - /** * Get the d tag identifier from kind 31990 event */ @@ -255,20 +240,6 @@ export function getHandlerReferences(event: NostrEvent): HandlerReference[] { return references; } -/** - * Get handler references filtered by platform - */ -export function getHandlersByPlatform( - event: NostrEvent, - platform?: string, -): HandlerReference[] { - const allRefs = getHandlerReferences(event); - - if (!platform) return allRefs; - - return allRefs.filter((ref) => ref.platform === platform); -} - /** * Get unique platforms from handler references in kind 31989 */ @@ -284,32 +255,3 @@ export function getRecommendedPlatforms(event: NostrEvent): string[] { return Array.from(platforms).sort(); } - -// ============================================================================ -// URL Template Utilities -// ============================================================================ - -/** - * Substitute placeholder in URL template with actual bech32 entity - */ -export function substituteTemplate( - template: string, - bech32Entity: string, -): string { - return template.replace(//g, bech32Entity); -} - -/** - * Check if a string contains the placeholder - */ -export function hasPlaceholder(template: string): boolean { - return template.includes(""); -} - -/** - * Format an address pointer as a string for display - * Format: "kind:pubkey:identifier" - */ -export function formatAddressPointer(pointer: AddressPointer): string { - return `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`; -}