web


git clone https://radroots.dev/git/web.git
Log | Files | Refs | Submodules | README | LICENSE

commit f74fcd8bedf704c791b0ab377f2370cec42fbaa9
parent 57eb89ecfda7d538b1fddbbe65fbcdde6179c951
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sun, 17 Nov 2024 08:35:00 +0000

Add nostr relay document polling subscriber and document fetch util. Add database nostr sync util for nip-99 events. Edit layout subscribers. Add/edit env, conf, types.

Diffstat:
M.env.example | 1+
Msrc/lib/conf.ts | 5++++-
Msrc/lib/types.ts | 8++++++--
Msrc/lib/utils/fetch.ts | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Asrc/lib/utils/nostr.ts | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/routes/(app)/+layout.svelte | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/routes/(app)/+page.svelte | 2--
Msrc/routes/(app)/models/trade-product/+page.svelte | 5+++--
Msrc/routes/(app)/test/+page.svelte | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/routes/(cfg)/cfg/init/+page.svelte | 6+-----
Msrc/routes/+layout.svelte | 1+
11 files changed, 336 insertions(+), 23 deletions(-)

diff --git a/.env.example b/.env.example @@ -1,5 +1,6 @@ PUBLIC_NOSTR_RELAY_DEFAULTS= PUBLIC_RADROOTS_URL= +PUBLIC_RADROOTS_NOSTR_PUBKEY= VITE_PUBLIC_KV_NAME= VITE_PUBLIC_NDK_CACHE_NAME= VITE_PUBLIC_NDK_CLIENT_NAME= diff --git a/src/lib/conf.ts b/src/lib/conf.ts @@ -1,3 +1,4 @@ +import { PUBLIC_RADROOTS_NOSTR_PUBKEY } from "$env/static/public"; import type { NumberTuple } from "@radroots/utils"; //import tailwindConfig from '../..//tailwind.config'; //export const tw = tailwindConfig; @@ -17,6 +18,8 @@ export const ks = { } }; +export const root_symbol = "»--`--,---"; + export const ascii = { bullet: '•', dash: `—` @@ -24,12 +27,12 @@ export const ascii = { export const cfg = { app: { - root_symbol: "»--`--,---", title: `Radroots`, description: `Creating networks between farmers, communities and small businesses that give customers greater access to natural foods and grow circular economies where profits are more fairly distributed. Radroots is built on the Nostr protocol and released under a copyleft open source license to provide transparency and give users the option to offer feedback and add or request new features.` }, nostr: { relay_url: `wss://radroots.org`, + relay_pubkey: PUBLIC_RADROOTS_NOSTR_PUBKEY, relay_polling_count_max: 10, }, delay: { diff --git a/src/lib/types.ts b/src/lib/types.ts @@ -1,7 +1,11 @@ +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import type { ExtendedBaseType, NDKEventStore } from "@nostr-dev-kit/ndk-svelte"; import type { LocationGcs, MediaUpload, TradeProduct } from "@radroots/models"; export type TradeProductBundle = { trade_product: TradeProduct; location_gcs: LocationGcs; media_uploads?: MediaUpload[]; -}; -\ No newline at end of file +}; + +export type NostrEventPageStore = NDKEventStore<ExtendedBaseType<NDKEvent>>; +\ No newline at end of file diff --git a/src/lib/utils/fetch.ts b/src/lib/utils/fetch.ts @@ -1,8 +1,9 @@ -import { fs, http, keystore } from "$lib/client"; -import { ks } from "$lib/conf"; +import { db, fs, http, keystore } from "$lib/client"; +import { cfg, ks } from "$lib/conf"; import type { IClientHttpResponseError } from "@radroots/client"; -import { app_nostr_key } from "@radroots/svelte-lib"; -import { err_msg, err_res, nostr_event_sign_attest, type ErrorMessage, type ErrorResponse, type FilePath } from "@radroots/utils"; +import { parse_nostr_relay_form_keys, type NostrRelayFormFields } from "@radroots/models"; +import { app_nostr_key, nostr_relays_connected, nostr_relays_poll_documents, nostr_relays_poll_documents_count } from "@radroots/svelte-lib"; +import { err_msg, err_res, nostr_event_sign_attest, parse_nostr_relay_information_document_fields, type ErrorMessage, type ErrorResponse, type FilePath } from "@radroots/utils"; import { get as get_store } from "svelte/store"; export const fetch_put_upload = async (opts: { @@ -31,7 +32,6 @@ export const fetch_put_upload = async (opts: { authorization: nostr_public_key, data_bin: file_data, }); - console.log(JSON.stringify(res, null, 4), `res`) if (`err` in res) err_msg(`error.client.request_failure`); else if (res.error) { return err_res(res.error); @@ -56,3 +56,76 @@ export const fetch_put_upload = async (opts: { return err_msg(`error.client.network_failure`); } }; + +export const fetch_relay_documents = async (): Promise<void> => { + try { + const $nostr_relays_poll_documents_count = get_store(nostr_relays_poll_documents_count); + const $app_nostr_key = get_store(app_nostr_key); + const $nostr_relays_connected = get_store(nostr_relays_connected); + if ( + $nostr_relays_poll_documents_count >= + cfg.nostr.relay_polling_count_max + ) { + nostr_relays_poll_documents.set(false); + return; + } + nostr_relays_poll_documents_count.set( + $nostr_relays_poll_documents_count + 1, + ); + const nostr_relays = await db.nostr_relay_get({ + list: [`on_profile`, { public_key: $app_nostr_key }], + }); + if (`err` in nostr_relays) throw new Error(nostr_relays.err); + + const unconnected_relays = nostr_relays.results.filter( + (i) => !$nostr_relays_connected.includes(i.id), + ); + if (unconnected_relays.length === 0) { + nostr_relays_poll_documents.set(false); + return; + } + + for (const nostr_relay of unconnected_relays) { + const res = await http.fetch({ + url: nostr_relay.url.replace(`ws://`, `http://`), + headers: { + Accept: "application/nostr+json", + }, + }); + if (`err` in res) continue; + else if (res.status === 200 && res.data) { + const doc = parse_nostr_relay_information_document_fields( + res.data, + ); + if (!doc) continue; + const fields: Partial<NostrRelayFormFields> = {}; + for (const [k, v] of Object.entries(doc)) { + const field_k = parse_nostr_relay_form_keys(k); + if (field_k) fields[field_k] = v; + } + if (Object.keys(fields).length < 1) continue; + await db.nostr_relay_update({ + on: { + url: nostr_relay.url, + }, + fields, + }); + nostr_relays_connected.set( + Array.from( + new Set([ + ...$nostr_relays_connected, + nostr_relay.id, + ]), + ), + ); + } + } + + setTimeout( + fetch_relay_documents, + cfg.delay.nostr_relay_poll_document, + ); + } catch (e) { + console.log(`(error) fetch_relay_documents `, e); + } +}; +\ No newline at end of file diff --git a/src/lib/utils/nostr.ts b/src/lib/utils/nostr.ts @@ -0,0 +1,110 @@ +import { db, dialog } from "$lib/client"; +import { cfg, root_symbol } from "$lib/conf"; +import { NDKKind } from "@nostr-dev-kit/ndk"; +import { app_nostr_key, ndk, ndk_user, nostr_sync_prevent, t } from "@radroots/svelte-lib"; +import { fmt_tags_basis_nip99, ndk_event, nevent_encode, num_str } from "@radroots/utils"; +import { get as get_store } from "svelte/store"; + +export const nostr_sync = async (): Promise<void> => { + try { + const $t = get_store(t); + const $nostr_sync_prevent = get_store(nostr_sync_prevent); + const $app_nostr_key = get_store(app_nostr_key); + + if ($nostr_sync_prevent) { + const confirm = await dialog.confirm({ + message: `${$t(`error.client.nostr_sync_disabled`)}`, + cancel_label: `${$t(`common.cancel`)}`, + ok_label: `${$t(`common.ok`)}` + }); + if (confirm) { + nostr_sync_prevent.set(false); + await nostr_sync(); + } + return; + } + + const $ndk = get_store(ndk); + const $ndk_user = get_store(ndk_user); + + const nostr_relays_active = await db.nostr_relay_get({ + list: [`on_profile`, { public_key: $app_nostr_key }], + }); + if (`err` in nostr_relays_active) return; //@todo + if (!nostr_relays_active.results.length) return; //@todo + const trade_products_all = await db.trade_product_get({ + list: [`all`], + }); + if (`err` in trade_products_all) return; //@todo + for (const trade_product of trade_products_all.results) { + const trade_product_location_res = await db.location_gcs_get({ + list: [`on_trade_product`, { id: trade_product.id }], + }); + if (`err` in trade_product_location_res) continue; //@todo + const trade_product_location = trade_product_location_res.results[0]; + + const media_upload_res = await db.media_upload_get({ + list: [`on_trade_product`, { id: trade_product.id }], + }); + if (`err` in media_upload_res) continue; //@todo + + const ev = await ndk_event({ + $ndk, + $ndk_user, + basis: { + kind: NDKKind.Classified, + content: ``, + tags: await fmt_tags_basis_nip99({ + d_tag: trade_product.id, + client: { + name: root_symbol, + pubkey: cfg.nostr.relay_pubkey, + relay: cfg.nostr.relay_url + }, + listing: { + title: trade_product.title, + summary: trade_product.summary, + process: trade_product.process, + lot: trade_product.lot, + profile: trade_product.profile, + year: num_str(trade_product.year), + }, + quantity: { + amt: num_str(trade_product.qty_amt), + unit: trade_product.qty_unit, + label: trade_product.qty_label + }, + price: { + amt: num_str(trade_product.price_amt), + currency: trade_product.price_currency, + qty_amt: num_str(trade_product.price_qty_amt), + qty_unit: trade_product.price_qty_unit, + }, + location: { + city: trade_product_location.gc_name, + region: trade_product_location.gc_admin1_name, + region_code: trade_product_location.gc_admin1_id, + country: trade_product_location.gc_country_name, + country_code: trade_product_location.gc_country_id, + lat: trade_product_location.lat, + lng: trade_product_location.lng, + geohash: trade_product_location.geohash, + }, + images: media_upload_res.results.length ? media_upload_res.results.map(i => ({ url: `${i.res_base}/${i.res_path}.${i.mime_type}` })) : undefined + }), + }, + }); + if (ev) { + ev.content = `radroots:[nostr:${nevent_encode({ + id: ev.id, + author: ev.pubkey, + relays: nostr_relays_active.results.map(i => i.url), + kind: NDKKind.Classified, + })}]` + await ev.publish(); + } + } + } catch (e) { + console.log(`(error) nostr_sync `, e); + } +}; +\ No newline at end of file diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte @@ -1,11 +1,21 @@ <script lang="ts"> - import { geoc, notification } from "$lib/client"; + import { db, geoc, keystore, notification } from "$lib/client"; + import { ks } from "$lib/conf"; + import { fetch_relay_documents } from "$lib/utils/fetch"; + import { nostr_sync } from "$lib/utils/nostr"; import { app_cfg_type, app_geoc, + app_init, + app_nostr_key, app_splash, + ndk, + ndk_user, + nostr_ndk_configured, + nostr_relays_poll_documents, sleep, } from "@radroots/svelte-lib"; + import { ndk_init } from "@radroots/utils"; import { onMount } from "svelte"; onMount(async () => { @@ -16,6 +26,7 @@ console.log(`e (app) onMount`, e); } finally { app_splash.set(false); + app_init.set(true); } }); @@ -24,10 +35,72 @@ }); app_splash.subscribe(async (_app_splash) => { - if (_app_splash) return; - await sleep(4000); - await notification.init(); + //@todo + }); + + app_init.subscribe(async (_app_init) => { + try { + if (!app_init) return; + await sleep(4000); + await notification.init(); + } catch (e) { + console.log(`(app_init) error `, e); + } }); + + app_nostr_key.subscribe(async (_app_nostr_key) => { + console.log(`_app_nostr_key `, _app_nostr_key); + if (!_app_nostr_key) return; + + const ks_nostr_secretkey = await keystore.get( + ks.keys.nostr_secretkey($app_nostr_key), + ); + + if (`err` in ks_nostr_secretkey) { + //@todo; + return; + } + + const nostr_relays = await db.nostr_relay_get({ + list: [`on_profile`, { public_key: $app_nostr_key }], + }); + if (`err` in nostr_relays) throw new Error(nostr_relays.err); + for (const { url } of nostr_relays.results) $ndk.addExplicitRelay(url); + await $ndk.connect(); + const ndk_user = await ndk_init({ + $ndk, + secret_key: ks_nostr_secretkey.result, + }); + if (!ndk_user) { + nostr_ndk_configured.set(false); + return; + } + $ndk_user = ndk_user; + $ndk_user.ndk = $ndk; + nostr_ndk_configured.set(true); + }); + + nostr_ndk_configured.subscribe(async (_nostr_ndk_configured) => { + try { + if (!_nostr_ndk_configured) return; + console.log(`(nostr_ndk_configured) success`); + nostr_relays_poll_documents.set(true); + await nostr_sync(); + } catch (e) { + console.log(`(nostr_ndk_configured) error `, e); + } + }); + + nostr_relays_poll_documents.subscribe( + async (_nostr_relays_poll_documents) => { + try { + if (!_nostr_relays_poll_documents) return; + await fetch_relay_documents(); + } catch (e) { + console.log(`(error) nostr_relays_poll_documents`, e); + } + }, + ); </script> <slot /> diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte @@ -207,8 +207,6 @@ label: `${$t(`common.home`)}`, callback: async () => { await route(`/`); - const res = await db.nostr_relay_get({ list: [`all`] }); - console.log(JSON.stringify(res, null, 4), `res`); }, }, { diff --git a/src/routes/(app)/models/trade-product/+page.svelte b/src/routes/(app)/models/trade-product/+page.svelte @@ -64,14 +64,15 @@ const data: LoadData = { results, }; - console.log(JSON.stringify(data, null, 4), `data`); return data; } catch (e) { console.log(`(error) load_data `, e); } }; - console.log(JSON.stringify(ld, null, 4), `ld`); + $: { + console.log(JSON.stringify(ld, null, 4), `ld`); + } </script> {#if ld && ld.results.length > 0} diff --git a/src/routes/(app)/test/+page.svelte b/src/routes/(app)/test/+page.svelte @@ -1,8 +1,59 @@ <script lang="ts"> - import { LayoutView, Nav, t } from "@radroots/svelte-lib"; + import type { NostrEventPageStore } from "$lib/types"; + import { type NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; + import { + app_nostr_key, + LayoutView, + Nav, + ndk, + t, + } from "@radroots/svelte-lib"; + import { onDestroy } from "svelte"; + + let events_store: NostrEventPageStore; + + $: { + let authors = [$app_nostr_key]; + let ndk_filter: NDKFilter = { + kinds: [NDKKind.Classified], + ...{ authors }, + }; + + fetch_events(ndk_filter).then(() => { + events_store?.startSubscription(); + }); + } + + const fetch_events = async (filter: NDKFilter): Promise<void> => { + try { + events_store = $ndk.storeSubscribe(filter, { + closeOnEose: true, + groupable: false, + autoStart: false, + }); + if (events_store) events_store.onEose(() => {}); + } catch (e) { + console.log(`(error) fetch_events `, e); + } + }; + + onDestroy(() => events_store?.unsubscribe()); </script> -<LayoutView>test</LayoutView> +<LayoutView> + <div class={`flex flex-col w-full px-4 gap-4 justify-start items-center`}> + {#if $events_store?.length} + {#each $events_store as ev, ev_i (ev.id)} + <p class={`font-sans font-[400] text-layer-0-glyph break-all`}> + {JSON.stringify(ev.content)} + </p> + <p class={`font-sans font-[400] text-layer-0-glyph break-all`}> + {JSON.stringify(ev.tags)} + </p> + {/each} + {/if} + </div> +</LayoutView> <Nav basis={{ diff --git a/src/routes/(cfg)/cfg/init/+page.svelte b/src/routes/(cfg)/cfg/init/+page.svelte @@ -455,7 +455,6 @@ url: `${PUBLIC_RADROOTS_URL}/public/accounts/list`, method: `post`, }); - console.log(JSON.stringify(res, null, 4), `res`); if (`err` in res) return err_msg(`${$t(`error.client.network_failure`)}`); else if (Array.isArray(res.data.results)) { @@ -501,7 +500,6 @@ : [cfg.nostr.relay_url].join(`,`), }, }); - console.log(JSON.stringify(res, null, 4), `res`); if (`err` in res) return res; else if (res.data && `tok` in res.data) { return { tok: res.data.tok }; @@ -525,7 +523,6 @@ method: `post`, authorization, }); - console.log(JSON.stringify(res, null, 4), `res`); if (`err` in res) return res; return { pass: true }; } catch (e) { @@ -546,7 +543,6 @@ method: `post`, authorization, }); - console.log(JSON.stringify(res, null, 4), `res`); if (`err` in res) return res; else if ( `public_key` in res.data && @@ -627,7 +623,7 @@ ); for (const url of Array.from( new Set([ - cfg.nostr.relay_url, + //cfg.nostr.relay_url, ...PUBLIC_NOSTR_RELAY_DEFAULTS.split(","), ]), )) { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte @@ -32,6 +32,7 @@ let route_render: NavigationRoute | undefined = undefined; let log_unlisten: IClientUnlisten | undefined = undefined; + onMount(async () => { try { if (`paintWorklet` in CSS)