web_lib

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

commit 2cc1c29d2c931c37cede4148cc34a1fa9d8eaf40
parent ef92d54a393c7c1dc81906de910bc7e182f4d9f0
Author: triesap <triesap@radroots.dev>
Date:   Fri, 26 Dec 2025 19:36:12 +0000

nfc: add meshnet packet codecs and payload parsing

- Export meshnet module from package entrypoint
- Define meshnet media type and packet/profile/chat types
- Add record/message builders for meshnet JSON packets
- Implement record/payload parsing with runtime type guards

Diffstat:
Mapps-lib/src/lib/components/view-pane.svelte | 53+++++++++++++++++++++++++++++++++++++++--------------
Mapps-lib/src/lib/components/view-stack.svelte | 41+++++++++++++++++++++--------------------
Mnfc/src/index.ts | 1+
Anfc/src/meshnet.ts | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/farm/lib.ts | 22++++++++++++++++++++++
Anostr/src/events/farm/parse.ts | 25+++++++++++++++++++++++++
Anostr/src/events/farm/tags.ts | 10++++++++++
Anostr/src/events/list/lib.ts | 44++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/list/parse.ts | 37+++++++++++++++++++++++++++++++++++++
Anostr/src/events/list/tags.ts | 10++++++++++
Anostr/src/events/list_set/lib.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/list_set/parse.ts | 47+++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/list_set/tags.ts | 10++++++++++
Anostr/src/events/plot/lib.ts | 22++++++++++++++++++++++
Anostr/src/events/plot/parse.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Anostr/src/events/plot/tags.ts | 10++++++++++
Mnostr/src/events/subscription.ts | 18+++++++++++++++++-
Mnostr/src/index.ts | 12++++++++++++
18 files changed, 540 insertions(+), 35 deletions(-)

diff --git a/apps-lib/src/lib/components/view-pane.svelte b/apps-lib/src/lib/components/view-pane.svelte @@ -7,6 +7,7 @@ type ViewMode, type ViewPointerEvents, } from "./view-context"; + import { writable, type Writable } from "svelte/store"; const DEFAULT_TRANSITION_MS = 260; const DEFAULT_OPACITY_INACTIVE = 0; @@ -72,10 +73,32 @@ children: Snippet; } = $props(); - const view_context = - get_context<ViewContext<string> | undefined>(VIEW_CONTEXT_KEY); + const view_context_store = + get_context<Writable<ViewContext<string>> | undefined>( + VIEW_CONTEXT_KEY, + ) ?? + writable<ViewContext<string>>({ + active_view: basis.active_view ?? basis.view, + mode: basis.mode ?? "stack", + fade: basis.fade ?? true, + transition_ms: basis.transition_ms ?? DEFAULT_TRANSITION_MS, + opacity_inactive: basis.opacity_inactive ?? DEFAULT_OPACITY_INACTIVE, + scale_inactive: basis.scale_inactive ?? DEFAULT_SCALE_INACTIVE, + offset_x: basis.offset_x ?? DEFAULT_OFFSET_X, + offset_y: basis.offset_y ?? DEFAULT_OFFSET_Y, + blur_inactive_px: + basis.blur_inactive_px ?? DEFAULT_BLUR_INACTIVE_PX, + pointer_events_inactive: + basis.pointer_events_inactive ?? DEFAULT_POINTER_EVENTS_INACTIVE, + z_index_active: basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE, + z_index_inactive: basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE, + }); - const active_view = $derived(basis.active_view ?? view_context?.active_view); + const view_context_value = $derived($view_context_store); + + const active_view = $derived( + basis.active_view ?? view_context_value?.active_view, + ); const is_active = $derived( basis.active !== undefined ? basis.active @@ -83,45 +106,47 @@ ? basis.view === active_view : true, ); - const mode = $derived(basis.mode ?? view_context?.mode ?? "stack"); - const fade = $derived(basis.fade ?? view_context?.fade ?? true); + const mode = $derived(basis.mode ?? view_context_value?.mode ?? "stack"); + const fade = $derived(basis.fade ?? view_context_value?.fade ?? true); const transition_ms = $derived( - basis.transition_ms ?? view_context?.transition_ms ?? DEFAULT_TRANSITION_MS, + basis.transition_ms ?? + view_context_value?.transition_ms ?? + DEFAULT_TRANSITION_MS, ); const opacity_inactive = $derived( basis.opacity_inactive ?? - view_context?.opacity_inactive ?? + view_context_value?.opacity_inactive ?? DEFAULT_OPACITY_INACTIVE, ); const scale_inactive = $derived( basis.scale_inactive ?? - view_context?.scale_inactive ?? + view_context_value?.scale_inactive ?? DEFAULT_SCALE_INACTIVE, ); const offset_x = $derived( - basis.offset_x ?? view_context?.offset_x ?? DEFAULT_OFFSET_X, + basis.offset_x ?? view_context_value?.offset_x ?? DEFAULT_OFFSET_X, ); const offset_y = $derived( - basis.offset_y ?? view_context?.offset_y ?? DEFAULT_OFFSET_Y, + basis.offset_y ?? view_context_value?.offset_y ?? DEFAULT_OFFSET_Y, ); const blur_inactive_px = $derived( basis.blur_inactive_px ?? - view_context?.blur_inactive_px ?? + view_context_value?.blur_inactive_px ?? DEFAULT_BLUR_INACTIVE_PX, ); const pointer_events_inactive = $derived( basis.pointer_events_inactive ?? - view_context?.pointer_events_inactive ?? + view_context_value?.pointer_events_inactive ?? DEFAULT_POINTER_EVENTS_INACTIVE, ); const z_index_active = $derived( basis.z_index_active ?? - view_context?.z_index_active ?? + view_context_value?.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE, ); const z_index_inactive = $derived( basis.z_index_inactive ?? - view_context?.z_index_inactive ?? + view_context_value?.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE, ); const display = $derived(basis.display ?? DEFAULT_DISPLAY); diff --git a/apps-lib/src/lib/components/view-stack.svelte b/apps-lib/src/lib/components/view-stack.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import type { Snippet } from "svelte"; import { set_context } from "$lib/utils/app"; + import { writable } from "svelte/store"; import { VIEW_CONTEXT_KEY, type ViewContext, @@ -52,7 +53,7 @@ children: Snippet; } = $props(); - const view_context = $state<ViewContext<string>>({ + const view_context = writable<ViewContext<string>>({ active_view: basis.active_view, mode: basis.mode ?? "stack", fade: basis.fade ?? true, @@ -71,25 +72,25 @@ set_context(VIEW_CONTEXT_KEY, view_context); $effect(() => { - view_context.active_view = basis.active_view; - view_context.mode = basis.mode ?? "stack"; - view_context.fade = basis.fade ?? true; - view_context.transition_ms = - basis.transition_ms ?? DEFAULT_TRANSITION_MS; - view_context.opacity_inactive = - basis.opacity_inactive ?? DEFAULT_OPACITY_INACTIVE; - view_context.scale_inactive = - basis.scale_inactive ?? DEFAULT_SCALE_INACTIVE; - view_context.offset_x = basis.offset_x ?? DEFAULT_OFFSET_X; - view_context.offset_y = basis.offset_y ?? DEFAULT_OFFSET_Y; - view_context.blur_inactive_px = - basis.blur_inactive_px ?? DEFAULT_BLUR_INACTIVE_PX; - view_context.pointer_events_inactive = - basis.pointer_events_inactive ?? DEFAULT_POINTER_EVENTS_INACTIVE; - view_context.z_index_active = - basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE; - view_context.z_index_inactive = - basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE; + view_context.set({ + active_view: basis.active_view, + mode: basis.mode ?? "stack", + fade: basis.fade ?? true, + transition_ms: basis.transition_ms ?? DEFAULT_TRANSITION_MS, + opacity_inactive: + basis.opacity_inactive ?? DEFAULT_OPACITY_INACTIVE, + scale_inactive: basis.scale_inactive ?? DEFAULT_SCALE_INACTIVE, + offset_x: basis.offset_x ?? DEFAULT_OFFSET_X, + offset_y: basis.offset_y ?? DEFAULT_OFFSET_Y, + blur_inactive_px: + basis.blur_inactive_px ?? DEFAULT_BLUR_INACTIVE_PX, + pointer_events_inactive: + basis.pointer_events_inactive ?? + DEFAULT_POINTER_EVENTS_INACTIVE, + z_index_active: basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE, + z_index_inactive: + basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE, + }); }); const mode = $derived(basis.mode ?? "stack"); diff --git a/nfc/src/index.ts b/nfc/src/index.ts @@ -1,4 +1,5 @@ export * from "./error.js"; +export * from "./meshnet.js"; export * from "./records.js"; export * from "./types.js"; export * from "./web.js"; diff --git a/nfc/src/meshnet.ts b/nfc/src/meshnet.ts @@ -0,0 +1,122 @@ +import { nfc_message_json, nfc_record_data_text, nfc_record_json } from "./records.js"; +import type { NfcMessageInput, NfcReadPayload, NfcRecord, NfcRecordInput } from "./types.js"; + +export type NfcMeshnetProfile = { + display_name: string; + device_label: string; +}; + +export type NfcMeshnetPacketProfile = { + type: "meshnet.profile.v1"; + profile: NfcMeshnetProfile; + timestamp_ms: number; +}; + +export type NfcMeshnetPacketChat = { + type: "meshnet.chat.v1"; + message_id: string; + profile: NfcMeshnetProfile; + text: string; + timestamp_ms: number; +}; + +export type NfcMeshnetPacket = NfcMeshnetPacketProfile | NfcMeshnetPacketChat; + +export const NFC_MESHNET_MEDIA_TYPE = "application/vnd.radroots.meshnet+json"; + +const is_record = (value: unknown): value is Record<string, unknown> => + typeof value === "object" && value !== null && !Array.isArray(value); + +const is_string = (value: unknown): value is string => typeof value === "string"; + +const is_number = (value: unknown): value is number => + typeof value === "number" && Number.isFinite(value); + +const is_profile = (value: unknown): value is NfcMeshnetProfile => { + if (!is_record(value)) return false; + return is_string(value.display_name) && is_string(value.device_label); +}; + +const is_packet_profile = (value: unknown): value is NfcMeshnetPacketProfile => { + if (!is_record(value)) return false; + if (value.type !== "meshnet.profile.v1") return false; + if (!is_profile(value.profile)) return false; + if (!is_number(value.timestamp_ms)) return false; + return true; +}; + +const is_packet_chat = (value: unknown): value is NfcMeshnetPacketChat => { + if (!is_record(value)) return false; + if (value.type !== "meshnet.chat.v1") return false; + if (!is_string(value.message_id)) return false; + if (!is_profile(value.profile)) return false; + if (!is_string(value.text)) return false; + if (!is_number(value.timestamp_ms)) return false; + return true; +}; + +export const nfc_meshnet_packet_profile = ( + profile: NfcMeshnetProfile, + timestamp_ms: number, +): NfcMeshnetPacketProfile => { + return { + type: "meshnet.profile.v1", + profile, + timestamp_ms, + }; +}; + +export const nfc_meshnet_packet_chat = ( + profile: NfcMeshnetProfile, + text: string, + message_id: string, + timestamp_ms: number, +): NfcMeshnetPacketChat => { + return { + type: "meshnet.chat.v1", + message_id, + profile, + text, + timestamp_ms, + }; +}; + +export const nfc_meshnet_record_from_packet = ( + packet: NfcMeshnetPacket, +): NfcRecordInput => { + return nfc_record_json(packet, { media_type: NFC_MESHNET_MEDIA_TYPE }); +}; + +export const nfc_meshnet_message_from_packet = ( + packet: NfcMeshnetPacket, +): NfcMessageInput => { + return nfc_message_json(packet, { media_type: NFC_MESHNET_MEDIA_TYPE }); +}; + +export const nfc_meshnet_packet_from_record = ( + record: NfcRecord, +): NfcMeshnetPacket | undefined => { + if (record.record_type !== "mime") return; + if (record.media_type !== NFC_MESHNET_MEDIA_TYPE) return; + const text = nfc_record_data_text(record); + if (!text) return; + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + return; + } + if (is_packet_profile(parsed)) return parsed; + if (is_packet_chat(parsed)) return parsed; +}; + +export const nfc_meshnet_packets_from_payload = ( + payload: NfcReadPayload, +): NfcMeshnetPacket[] => { + const packets: NfcMeshnetPacket[] = []; + for (const record of payload.message.records) { + const packet = nfc_meshnet_packet_from_record(record); + if (packet) packets.push(packet); + } + return packets; +}; diff --git a/nostr/src/events/farm/lib.ts b/nostr/src/events/farm/lib.ts @@ -0,0 +1,22 @@ +import type { RadrootsFarm } from "@radroots/events-bindings"; +import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; +import { nostr_event_create } from "../lib.js"; +import { tags_farm } from "./tags.js"; + +export const KIND_RADROOTS_FARM = 30340; +export type KindRadrootsFarm = typeof KIND_RADROOTS_FARM; + +export const nostr_event_farm = async ( + opts: NostrEventFigure<{ data: RadrootsFarm }>, +): Promise<NostrSignedEvent | undefined> => { + const { data } = opts; + const tags = await tags_farm(data); + return nostr_event_create({ + ...opts, + basis: { + kind: KIND_RADROOTS_FARM, + content: JSON.stringify(data), + tags, + }, + }); +}; diff --git a/nostr/src/events/farm/parse.ts b/nostr/src/events/farm/parse.ts @@ -0,0 +1,25 @@ +import type { RadrootsFarm } from "@radroots/events-bindings"; +import { radroots_farm_schema } from "@radroots/events-bindings"; +import type { NostrEvent } from "../../types/nostr.js"; +import { get_event_tag, parse_nostr_event_basis } from "../lib.js"; +import type { NostrEventBasis } from "../subscription.js"; +import { KIND_RADROOTS_FARM, type KindRadrootsFarm } from "./lib.js"; + +export type RadrootsFarmNostrEvent = NostrEventBasis<KindRadrootsFarm> & { farm: RadrootsFarm }; + +export const parse_nostr_farm_event = ( + event: NostrEvent, +): RadrootsFarmNostrEvent | undefined => { + const ev = parse_nostr_event_basis(event, KIND_RADROOTS_FARM); + if (!ev) return undefined; + const d_tag = get_event_tag(event.tags, "d"); + if (!d_tag) return undefined; + try { + const parsed = JSON.parse(event.content); + const farm = radroots_farm_schema.parse(parsed); + if (farm.d_tag !== d_tag) return undefined; + return { ...ev, farm }; + } catch { + return undefined; + } +}; diff --git a/nostr/src/events/farm/tags.ts b/nostr/src/events/farm/tags.ts @@ -0,0 +1,10 @@ +import type { RadrootsFarm } from "@radroots/events-bindings"; +import { farm_tags } from "@radroots/events-codec-wasm"; +import type { NostrEventTags } from "../../types/lib.js"; +import { ensure_codec_wasm, parse_tags_json } from "../wasm.js"; + +export const tags_farm = async (opts: RadrootsFarm): Promise<NostrEventTags> => { + await ensure_codec_wasm(); + const tags_json = farm_tags(JSON.stringify(opts)); + return parse_tags_json(tags_json); +}; diff --git a/nostr/src/events/list/lib.ts b/nostr/src/events/list/lib.ts @@ -0,0 +1,44 @@ +import type { RadrootsList } from "@radroots/events-bindings"; +import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; +import { nostr_event_create } from "../lib.js"; +import { tags_list } from "./tags.js"; + +export const NIP51_LIST_KINDS = [ + 10000, + 10001, + 10002, + 10003, + 10004, + 10005, + 10006, + 10007, + 10009, + 10012, + 10015, + 10020, + 10030, + 10050, + 10101, + 10102, +] as const; + +export type KindRadrootsList = typeof NIP51_LIST_KINDS[number]; + +export const is_nip51_list_kind = (kind: number): kind is KindRadrootsList => + NIP51_LIST_KINDS.some(value => value === kind); + +export const nostr_event_list = async ( + opts: NostrEventFigure<{ data: RadrootsList; kind: KindRadrootsList }>, +): Promise<NostrSignedEvent | undefined> => { + const { data, kind } = opts; + if (!is_nip51_list_kind(kind)) return undefined; + const tags = await tags_list(data); + return nostr_event_create({ + ...opts, + basis: { + kind, + content: data.content, + tags, + }, + }); +}; diff --git a/nostr/src/events/list/parse.ts b/nostr/src/events/list/parse.ts @@ -0,0 +1,37 @@ +import type { RadrootsList } from "@radroots/events-bindings"; +import { radroots_list_schema } from "@radroots/events-bindings"; +import type { NostrEventTags } from "../../types/lib.js"; +import type { NostrEvent } from "../../types/nostr.js"; +import type { NostrEventBasis } from "../subscription.js"; +import { is_nip51_list_kind, type KindRadrootsList } from "./lib.js"; + +export type RadrootsListNostrEvent = NostrEventBasis<KindRadrootsList> & { list: RadrootsList }; + +const list_entries_from_tags = (tags: NostrEventTags): RadrootsList["entries"] => + tags + .filter(tag => tag.length >= 2) + .map(tag => ({ tag: tag[0], values: tag.slice(1) })); + +export const parse_nostr_list_event = ( + event: NostrEvent, +): RadrootsListNostrEvent | undefined => { + if (!event || typeof event.kind !== "number") return undefined; + if (!is_nip51_list_kind(event.kind)) return undefined; + if (typeof event.created_at !== "number") return undefined; + try { + const list_raw = { + content: event.content ?? "", + entries: list_entries_from_tags(event.tags), + }; + const list = radroots_list_schema.parse(list_raw); + return { + id: event.id, + published_at: event.created_at, + author: event.pubkey, + kind: event.kind, + list, + }; + } catch { + return undefined; + } +}; diff --git a/nostr/src/events/list/tags.ts b/nostr/src/events/list/tags.ts @@ -0,0 +1,10 @@ +import type { RadrootsList } from "@radroots/events-bindings"; +import { list_tags } from "@radroots/events-codec-wasm"; +import type { NostrEventTags } from "../../types/lib.js"; +import { ensure_codec_wasm, parse_tags_json } from "../wasm.js"; + +export const tags_list = async (opts: RadrootsList): Promise<NostrEventTags> => { + await ensure_codec_wasm(); + const tags_json = list_tags(JSON.stringify(opts)); + return parse_tags_json(tags_json); +}; diff --git a/nostr/src/events/list_set/lib.ts b/nostr/src/events/list_set/lib.ts @@ -0,0 +1,43 @@ +import type { RadrootsListSet } from "@radroots/events-bindings"; +import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; +import { nostr_event_create } from "../lib.js"; +import { tags_list_set } from "./tags.js"; + +export const NIP51_LIST_SET_KINDS = [ + 30000, + 30001, + 30002, + 30003, + 30004, + 30005, + 30006, + 30007, + 30015, + 30030, + 30063, + 30267, + 31924, + 39089, + 39092, +] as const; + +export type KindRadrootsListSet = typeof NIP51_LIST_SET_KINDS[number]; + +export const is_nip51_list_set_kind = (kind: number): kind is KindRadrootsListSet => + NIP51_LIST_SET_KINDS.some(value => value === kind); + +export const nostr_event_list_set = async ( + opts: NostrEventFigure<{ data: RadrootsListSet; kind: KindRadrootsListSet }>, +): Promise<NostrSignedEvent | undefined> => { + const { data, kind } = opts; + if (!is_nip51_list_set_kind(kind)) return undefined; + const tags = await tags_list_set(data); + return nostr_event_create({ + ...opts, + basis: { + kind, + content: data.content, + tags, + }, + }); +}; diff --git a/nostr/src/events/list_set/parse.ts b/nostr/src/events/list_set/parse.ts @@ -0,0 +1,47 @@ +import type { RadrootsListSet } from "@radroots/events-bindings"; +import { radroots_list_set_schema } from "@radroots/events-bindings"; +import type { NostrEventTags } from "../../types/lib.js"; +import type { NostrEvent } from "../../types/nostr.js"; +import type { NostrEventBasis } from "../subscription.js"; +import { get_event_tag } from "../lib.js"; +import { is_nip51_list_set_kind, type KindRadrootsListSet } from "./lib.js"; + +export type RadrootsListSetNostrEvent = + NostrEventBasis<KindRadrootsListSet> & { list_set: RadrootsListSet }; + +const RESERVED_TAGS = new Set(["d", "title", "description", "image"]); + +const list_entries_from_tags = (tags: NostrEventTags): RadrootsListSet["entries"] => + tags + .filter(tag => tag.length >= 2 && !RESERVED_TAGS.has(tag[0])) + .map(tag => ({ tag: tag[0], values: tag.slice(1) })); + +export const parse_nostr_list_set_event = ( + event: NostrEvent, +): RadrootsListSetNostrEvent | undefined => { + if (!event || typeof event.kind !== "number") return undefined; + if (!is_nip51_list_set_kind(event.kind)) return undefined; + if (typeof event.created_at !== "number") return undefined; + const d_tag = get_event_tag(event.tags, "d"); + if (!d_tag) return undefined; + try { + const list_set_raw = { + d_tag, + content: event.content ?? "", + entries: list_entries_from_tags(event.tags), + title: get_event_tag(event.tags, "title") || undefined, + description: get_event_tag(event.tags, "description") || undefined, + image: get_event_tag(event.tags, "image") || undefined, + }; + const list_set = radroots_list_set_schema.parse(list_set_raw); + return { + id: event.id, + published_at: event.created_at, + author: event.pubkey, + kind: event.kind, + list_set, + }; + } catch { + return undefined; + } +}; diff --git a/nostr/src/events/list_set/tags.ts b/nostr/src/events/list_set/tags.ts @@ -0,0 +1,10 @@ +import type { RadrootsListSet } from "@radroots/events-bindings"; +import { list_set_tags } from "@radroots/events-codec-wasm"; +import type { NostrEventTags } from "../../types/lib.js"; +import { ensure_codec_wasm, parse_tags_json } from "../wasm.js"; + +export const tags_list_set = async (opts: RadrootsListSet): Promise<NostrEventTags> => { + await ensure_codec_wasm(); + const tags_json = list_set_tags(JSON.stringify(opts)); + return parse_tags_json(tags_json); +}; diff --git a/nostr/src/events/plot/lib.ts b/nostr/src/events/plot/lib.ts @@ -0,0 +1,22 @@ +import type { RadrootsPlot } from "@radroots/events-bindings"; +import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; +import { nostr_event_create } from "../lib.js"; +import { tags_plot } from "./tags.js"; + +export const KIND_RADROOTS_PLOT = 30350; +export type KindRadrootsPlot = typeof KIND_RADROOTS_PLOT; + +export const nostr_event_plot = async ( + opts: NostrEventFigure<{ data: RadrootsPlot }>, +): Promise<NostrSignedEvent | undefined> => { + const { data } = opts; + const tags = await tags_plot(data); + return nostr_event_create({ + ...opts, + basis: { + kind: KIND_RADROOTS_PLOT, + content: JSON.stringify(data), + tags, + }, + }); +}; diff --git a/nostr/src/events/plot/parse.ts b/nostr/src/events/plot/parse.ts @@ -0,0 +1,48 @@ +import type { RadrootsPlot } from "@radroots/events-bindings"; +import { radroots_plot_schema } from "@radroots/events-bindings"; +import type { NostrEvent } from "../../types/nostr.js"; +import { get_event_tag, parse_nostr_event_basis } from "../lib.js"; +import type { NostrEventBasis } from "../subscription.js"; +import { KIND_RADROOTS_FARM } from "../farm/lib.js"; +import { KIND_RADROOTS_PLOT, type KindRadrootsPlot } from "./lib.js"; + +export type RadrootsPlotNostrEvent = NostrEventBasis<KindRadrootsPlot> & { plot: RadrootsPlot }; + +type PlotFarmRef = { + pubkey: string; + d_tag: string; +}; + +const parse_farm_addr = (value: string): PlotFarmRef | undefined => { + const parts = value.split(":"); + if (parts.length < 3) return undefined; + const kind = Number(parts[0]); + if (!Number.isFinite(kind) || kind !== KIND_RADROOTS_FARM) return undefined; + const pubkey = parts[1]?.trim() || ""; + const d_tag = parts.slice(2).join(":").trim(); + if (!pubkey || !d_tag) return undefined; + return { pubkey, d_tag }; +}; + +export const parse_nostr_plot_event = ( + event: NostrEvent, +): RadrootsPlotNostrEvent | undefined => { + const ev = parse_nostr_event_basis(event, KIND_RADROOTS_PLOT); + if (!ev) return undefined; + const d_tag = get_event_tag(event.tags, "d"); + if (!d_tag) return undefined; + const farm_addr = get_event_tag(event.tags, "a"); + if (!farm_addr) return undefined; + const farm_ref = parse_farm_addr(farm_addr); + if (!farm_ref) return undefined; + try { + const parsed = JSON.parse(event.content); + const plot = radroots_plot_schema.parse(parsed); + if (plot.d_tag !== d_tag) return undefined; + if (plot.farm.pubkey !== farm_ref.pubkey) return undefined; + if (plot.farm.d_tag !== farm_ref.d_tag) return undefined; + return { ...ev, plot }; + } catch { + return undefined; + } +}; diff --git a/nostr/src/events/plot/tags.ts b/nostr/src/events/plot/tags.ts @@ -0,0 +1,10 @@ +import type { RadrootsPlot } from "@radroots/events-bindings"; +import { plot_tags } from "@radroots/events-codec-wasm"; +import type { NostrEventTags } from "../../types/lib.js"; +import { ensure_codec_wasm, parse_tags_json } from "../wasm.js"; + +export const tags_plot = async (opts: RadrootsPlot): Promise<NostrEventTags> => { + await ensure_codec_wasm(); + const tags_json = plot_tags(JSON.stringify(opts)); + return parse_tags_json(tags_json); +}; diff --git a/nostr/src/events/subscription.ts b/nostr/src/events/subscription.ts @@ -1,9 +1,17 @@ import type { NostrEvent } from "../types/nostr.js"; import { parse_nostr_comment_event, RadrootsCommentNostrEvent } from "./comment/parse.js"; +import { parse_nostr_farm_event, RadrootsFarmNostrEvent } from "./farm/parse.js"; import { parse_nostr_follow_event, RadrootsFollowNostrEvent } from "./follow/parse.js"; import { parse_nostr_listing_event, RadrootsListingNostrEvent } from "./listing/parse.js"; +import { parse_nostr_list_event, RadrootsListNostrEvent } from "./list/parse.js"; +import { parse_nostr_list_set_event, RadrootsListSetNostrEvent } from "./list_set/parse.js"; +import { is_nip51_list_kind } from "./list/lib.js"; +import { is_nip51_list_set_kind } from "./list_set/lib.js"; +import { parse_nostr_plot_event, RadrootsPlotNostrEvent } from "./plot/parse.js"; import { parse_nostr_profile_event, RadrootsProfileNostrEvent } from "./profile/parse.js"; import { parse_nostr_reaction_event, RadrootsReactionNostrEvent } from "./reaction/parse.js"; +import { KIND_RADROOTS_FARM } from "./farm/lib.js"; +import { KIND_RADROOTS_PLOT } from "./plot/lib.js"; export type NostrEventBasis<T extends number> = { id: string; @@ -17,10 +25,16 @@ export type NostrEventPayload = | RadrootsListingNostrEvent | RadrootsCommentNostrEvent | RadrootsReactionNostrEvent - | RadrootsFollowNostrEvent; + | RadrootsFollowNostrEvent + | RadrootsFarmNostrEvent + | RadrootsPlotNostrEvent + | RadrootsListNostrEvent + | RadrootsListSetNostrEvent; export const nostr_event_on = (event: NostrEvent): NostrEventPayload | undefined => { if (!event || typeof event.kind !== "number") return undefined; + if (is_nip51_list_kind(event.kind)) return parse_nostr_list_event(event); + if (is_nip51_list_set_kind(event.kind)) return parse_nostr_list_set_event(event); switch (event.kind) { case 0: return parse_nostr_profile_event(event); @@ -28,6 +42,8 @@ export const nostr_event_on = (event: NostrEvent): NostrEventPayload | undefined case 1111: return parse_nostr_comment_event(event); case 7: return parse_nostr_reaction_event(event); case 3: return parse_nostr_follow_event(event); + case KIND_RADROOTS_FARM: return parse_nostr_farm_event(event); + case KIND_RADROOTS_PLOT: return parse_nostr_plot_event(event); default: return undefined; } }; diff --git a/nostr/src/index.ts b/nostr/src/index.ts @@ -11,6 +11,9 @@ export * from "./domain/trade/tags.js"; export * from "./events/comment/lib.js"; export * from "./events/comment/parse.js"; export * from "./events/comment/tags.js"; +export * from "./events/farm/lib.js"; +export * from "./events/farm/parse.js"; +export * from "./events/farm/tags.js"; export * from "./events/follow/lib.js"; export * from "./events/follow/parse.js"; export * from "./events/follow/tags.js"; @@ -21,6 +24,15 @@ export * from "./events/lib.js"; export * from "./events/listing/lib.js"; export * from "./events/listing/parse.js"; export * from "./events/listing/tags.js"; +export * from "./events/list/lib.js"; +export * from "./events/list/parse.js"; +export * from "./events/list/tags.js"; +export * from "./events/list_set/lib.js"; +export * from "./events/list_set/parse.js"; +export * from "./events/list_set/tags.js"; +export * from "./events/plot/lib.js"; +export * from "./events/plot/parse.js"; +export * from "./events/plot/tags.js"; export * from "./events/profile/lib.js"; export * from "./events/profile/parse.js"; export * from "./events/reaction/lib.js";