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:
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";