commit f72d78acf49cde274c26790beaee84da4d815df3
parent a7b31875ab02f0b054532e8f2b6a16ed843841c8
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:
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)