web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit 027e986a4fe2ab526c992e9cee2026c702ff12ba
parent 9c6fc86afab20c81118be10bd873999478435439
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Fri,  8 Aug 2025 21:01:47 +0000

utils-nostr: add `listing` nip-99 schemas, types, and event parsing utils

Diffstat:
Mutils-nostr/package.json | 3++-
Mutils-nostr/src/events/parse.ts | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mutils-nostr/src/events/subscription.ts | 11++++++++---
Mutils-nostr/src/schemas/lib.ts | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mutils-nostr/src/types/lib.ts | 10+++++++++-
5 files changed, 190 insertions(+), 7 deletions(-)

diff --git a/utils-nostr/package.json b/utils-nostr/package.json @@ -2,6 +2,7 @@ "name": "@radroots/utils-nostr", "version": "0.0.1", "private": true, + "license": "GPLv3", "type": "module", "main": "./dist/cjs/index.cjs", "module": "./dist/esm/index.js", @@ -18,7 +19,7 @@ "build:cjs": "tsc -p tsconfig.cjs.json", "build": "npm run clean && npm run build:esm && npm run build:cjs", "prebuild": "npm run clean", - "clean": "rimraf dist && rimraf tsconfig.tsbuildinfo", + "clean": "rimraf dist", "dev": "npm run watch", "watch": "tsc -w" }, diff --git a/utils-nostr/src/events/parse.ts b/utils-nostr/src/events/parse.ts @@ -1,6 +1,7 @@ import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { nostr_event_metadata_schema } from "../schemas/lib.js"; -import { type NostrEventMetadata } from "../types/lib.js"; +import { nostr_event_listing_schema, nostr_event_metadata_schema, nostr_tag_listing_schema, nostr_tag_location_schema, nostr_tag_price_schema, nostr_tag_quantity_schema } from "../schemas/lib.js"; +import { NostrEventListing, type NostrEventMetadata } from "../types/lib.js"; +import { get_event_tag, get_event_tags } from "./lib.js"; export const parse_nostr_metadata_event = (event: NDKEvent): NostrEventMetadata | undefined => { if (!event || typeof event.content !== 'string' || event.kind !== 0) return undefined; @@ -14,4 +15,74 @@ export const parse_nostr_metadata_event = (event: NDKEvent): NostrEventMetadata } }; +export const parse_nostr_listing_event = (event: NDKEvent): NostrEventListing | undefined => { + if (!event || event.kind !== 30402 || !Array.isArray(event.tags)) return; + try { + const tags = event.tags; + + const d_tag = get_event_tag(tags, 'd'); + + const listing_raw = { + key: get_event_tag(tags, 'key'), + title: get_event_tag(tags, 'title'), + category: get_event_tag(tags, 'category'), + summary: get_event_tag(tags, 'summary'), + process: get_event_tag(tags, 'process'), + lot: get_event_tag(tags, 'lot'), + location: get_event_tag(tags, 'location'), + profile: get_event_tag(tags, 'profile'), + year: get_event_tag(tags, 'year') + }; + + const listing = nostr_tag_listing_schema.parse(listing_raw); + + const quantities = get_event_tags(tags, 'quantity') + .map(q => { + if (q.length < 3) return undefined; + return nostr_tag_quantity_schema.parse({ + amt: q[1], + unit: q[2], + label: q[3] + }); + }) + .filter(Boolean); + + const prices = get_event_tags(tags, 'price') + .map(p => { + if (p.length < 6) return undefined; + return nostr_tag_price_schema.parse({ + amt: p[1], + currency: p[2], + qty_amt: p[3], + qty_unit: p[4], + qty_key: p[5] + }); + }) + .filter(Boolean); + + const location_parts = get_event_tags(tags, 'location')[0]?.slice(1) ?? []; + + const location_raw: any = {}; + if (location_parts[0]) location_raw.primary = location_parts[0]; + if (location_parts[1]) location_raw.city = location_parts[1]; + if (location_parts[2]) location_raw.region = location_parts[2]; + if (location_parts[3]) location_raw.country = location_parts[3]; + + const location = Object.keys(location_raw).length + ? nostr_tag_location_schema.parse(location_raw) + : undefined; + + const parsed = nostr_event_listing_schema.parse({ + d_tag, + listing, + quantities, + prices, + location + }); + + return parsed; + } catch { + return undefined; + } +}; diff --git a/utils-nostr/src/events/subscription.ts b/utils-nostr/src/events/subscription.ts @@ -1,9 +1,10 @@ import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { type NostrEventMetadata } from "../types/lib.js"; -import { parse_nostr_metadata_event } from "./parse.js"; +import { NostrEventListing, type NostrEventMetadata } from "../types/lib.js"; +import { parse_nostr_listing_event, parse_nostr_metadata_event } from "./parse.js"; export type NdkEventPayload = | { kind: 0; metadata: NostrEventMetadata; } + | { kind: 30402; listing: NostrEventListing; } export const on_ndk_event = (event: NDKEvent): NdkEventPayload | undefined => { if (!event || typeof event.kind !== 'number') return undefined; @@ -14,7 +15,11 @@ export const on_ndk_event = (event: NDKEvent): NdkEventPayload | undefined => { if (!data) return; return { kind: event.kind, metadata: data }; }; - + case 30402: { + const data = parse_nostr_listing_event(event); + if (!data) return; + return { kind: event.kind, listing: data }; + }; default: return undefined; } }; diff --git a/utils-nostr/src/schemas/lib.ts b/utils-nostr/src/schemas/lib.ts @@ -12,3 +12,100 @@ export const nostr_event_metadata_schema = z.object({ lud16: z.string().optional(), bot: z.boolean().optional(), }); + +export const nostr_tag_listing_schema = z.object({ + key: z.string(), + title: z.string(), + category: z.string(), + summary: z.string().optional(), + process: z.string().optional(), + lot: z.string().optional(), + location: z.string().optional(), + profile: z.string().optional(), + year: z.string().optional() +}); + +export const nostr_tag_quantity_schema = z.object({ + amt: z.string(), + unit: z.string(), + label: z.string().optional() +}); + +export const nostr_tag_price_schema = z.object({ + amt: z.string(), + currency: z.string(), + qty_amt: z.string(), + qty_unit: z.string(), + qty_key: z.string() +}); + +export const nostr_tag_discount_schema = z.union([ + z.object({ + quantity: z.object({ + ref_quantity: z.string(), + threshold: z.string(), + value: z.string(), + currency: z.string() + }) + }), + z.object({ + mass: z.object({ + unit: z.string(), + threshold: z.string(), + threshold_unit: z.string(), + value: z.string(), + currency: z.string() + }) + }), + z.object({ + subtotal: z.object({ + threshold: z.string(), + currency: z.string(), + value: z.string(), + measure: z.string() + }) + }), + z.object({ + total: z.object({ + total_min: z.string(), + value: z.string(), + measure: z.string() + }) + }) +]); + +export const nostr_tag_location_schema = z.object({ + primary: z.string(), + city: z.string().optional(), + region: z.string().optional(), + country: z.string().optional(), + lat: z.number().optional(), + lng: z.number().optional() +}); + +export const nostr_tag_image_schema = z.object({ + url: z.string(), + size: z + .object({ + w: z.number(), + h: z.number() + }) + .optional() +}); + +export const nostr_tag_client_schema = z.object({ + name: z.string(), + pubkey: z.string(), + relay: z.string() +}); + +export const nostr_event_listing_schema = z.object({ + d_tag: z.string(), + listing: nostr_tag_listing_schema, + quantities: z.array(nostr_tag_quantity_schema), + prices: z.array(nostr_tag_price_schema), + discounts: z.array(nostr_tag_discount_schema).optional(), + location: nostr_tag_location_schema.optional(), + images: z.array(nostr_tag_image_schema).optional(), + client: nostr_tag_client_schema.optional() +}); +\ No newline at end of file diff --git a/utils-nostr/src/types/lib.ts b/utils-nostr/src/types/lib.ts @@ -1,4 +1,12 @@ import { z } from 'zod'; -import { nostr_event_metadata_schema } from "../schemas/lib.js"; +import { nostr_event_listing_schema, nostr_event_metadata_schema, nostr_tag_client_schema, nostr_tag_discount_schema, nostr_tag_image_schema, nostr_tag_listing_schema, nostr_tag_location_schema, nostr_tag_price_schema, nostr_tag_quantity_schema } from "../schemas/lib.js"; export type NostrEventMetadata = z.infer<typeof nostr_event_metadata_schema>; +export type NostrEventListing = z.infer<typeof nostr_event_listing_schema> +export type NostrTagClient = z.infer<typeof nostr_tag_client_schema> +export type NostrTagMediaUpload = z.infer<typeof nostr_tag_image_schema> +export type NostrTagLocation = z.infer<typeof nostr_tag_location_schema> +export type NostrTagPriceDiscount = z.infer<typeof nostr_tag_discount_schema> +export type NostrTagPrice = z.infer<typeof nostr_tag_price_schema> +export type NostrTagQuantity = z.infer<typeof nostr_tag_quantity_schema> +export type NostrTagListing = z.infer<typeof nostr_tag_listing_schema>