tangle_indexer


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

commit ac01d23e01513a9c36e350477a71c3f81cd03422
parent 643040091375a56717dbd0200d8af6b9209640b8
Author: triesap <triesap@radroots.dev>
Date:   Sun, 26 Oct 2025 20:36:30 +0000

Integrate `radroots-core` types and refactor `indexer` listing process  to parse structured product, quantity, price, location, and images from tags, replacing JSON parsing and refining error semantics.

Diffstat:
M.gitignore | 1+
MCargo.lock | 1+
MCargo.toml | 1+
Mapp/src/routes/(market)/(listing)/[0=country]/+page.ts | 8++++----
Aapp/src/routes/(market)/(profile)/[0=nip05]/+error.svelte | 9+++++++++
Mapp/src/routes/(market)/(profile)/[0=nip05]/+page.svelte | 4++++
Mapp/src/routes/(market)/(profile)/[0=nip05]/+page.ts | 10+++++-----
Mapp/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts | 6+++---
Mapp/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts | 6+++---
Mapp/src/routes/+page.ts | 7+++----
Mcrates/indexer/Cargo.toml | 1+
Mcrates/indexer/src/domain/events/listing.rs | 208++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mpackage.json | 3++-
13 files changed, 239 insertions(+), 26 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,5 +1,6 @@ /target node_modules +.turbo # Debug logs/ diff --git a/Cargo.lock b/Cargo.lock @@ -1334,6 +1334,7 @@ dependencies = [ "config", "indexer-utils", "once_cell", + "radroots-core", "radroots-events", "radroots-events-indexed", "regex", diff --git a/Cargo.toml b/Cargo.toml @@ -11,6 +11,7 @@ rust-version = "1.86.0" license = "AGPL-3.0" [workspace.dependencies] +radroots-core = { path = "../../crates/crates/core" } radroots-events = { path = "../../crates/crates/events" } radroots-events-indexed = { path = "../../crates/crates/events-indexed" } diff --git a/app/src/routes/(market)/(listing)/[0=country]/+page.ts b/app/src/routes/(market)/(listing)/[0=country]/+page.ts @@ -5,7 +5,7 @@ import type { RadrootsEventsIndexedManifest } from "@radroots/events-indexed-bin import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; -const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env; +const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env; export const entries: EntryGenerator = async () => { const [ @@ -13,7 +13,7 @@ export const entries: EntryGenerator = async () => { ]: [ string[] ] = await Promise.all([ - fetch(`${indexes_url}/events/30402/country/indexes.json`).then(r => r.json()) + fetch(`${idx_url}/events/30402/country/indexes.json`).then(r => r.json()) ]); return events_0_country_indexes.map(i => ({ 0: i })) }; @@ -30,7 +30,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { const [ res_country_manifest, ] = await Promise.all([ - fetch(`${indexes_url}/events/30402/country/${country}/manifest.json`) + fetch(`${idx_url}/events/30402/country/${country}/manifest.json`) ]); if (!res_country_manifest.ok) error(404, { message: `country:${country}` }); @@ -40,7 +40,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { let events: RadrootsListingEventMetadata[] = []; if (manifest.shards.length > 0) { const shard = manifest.shards[0]; - const res_country_shard = await fetch(`${indexes_url}/events/30402/country/${country}/${shard.file}?v=${shard.sha256}`); + const res_country_shard = await fetch(`${idx_url}/events/30402/country/${country}/${shard.file}?v=${shard.sha256}`); if (!res_country_shard.ok) error(500, { message: `load:${country}:${shard.file}` }); events = await res_country_shard.json(); } diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+error.svelte b/app/src/routes/(market)/(profile)/[0=nip05]/+error.svelte @@ -0,0 +1,9 @@ +<script lang="ts"> + import { page } from "$app/state"; +</script> + +<div class={`flex flex-col w-full justify-start items-start`}> + <p class={`font-sans font-[400] text-base text-ly0-gl`}> + {`error: ${page.error?.message}`} + </p> +</div> diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+page.svelte b/app/src/routes/(market)/(profile)/[0=nip05]/+page.svelte @@ -3,6 +3,10 @@ import type { PageProps } from "./$types"; let { data }: PageProps = $props(); + + $effect(() => { + console.log(JSON.stringify(data, null, 4), `data`); + }); </script> <Profile basis={{ indexed: data }} /> diff --git a/app/src/routes/(market)/(profile)/[0=nip05]/+page.ts b/app/src/routes/(market)/(profile)/[0=nip05]/+page.ts @@ -6,7 +6,7 @@ import { lib_nostr_npub_encode } from "@radroots/utils-nostr"; import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; -const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env; +const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env; export const entries: EntryGenerator = async () => { const [ @@ -14,7 +14,7 @@ export const entries: EntryGenerator = async () => { ]: [ string[] ] = await Promise.all([ - fetch(`${indexes_url}/events/0/nip05/indexes.json`).then(r => r.json()) + fetch(`${idx_url}/events/0/nip05/indexes.json`).then(r => r.json()) ]); return events_0_author_indexes.map(i => ({ 0: i })) }; @@ -28,8 +28,8 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { res_nip05_metadata, res_nip05_listings_manifest, ] = await Promise.all([ - fetch(`${indexes_url}/events/0/nip05/${nip05}/metadata.json`), - fetch(`${indexes_url}/events/30402/nip05/${nip05}/manifest.json`) + fetch(`${idx_url}/events/0/nip05/${nip05}/metadata.json`), + fetch(`${idx_url}/events/30402/nip05/${nip05}/manifest.json`) ]); if (!res_nip05_metadata.ok) error(404, { message: `nip05:${nip05}`, }); @@ -41,7 +41,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { let listings_events: RadrootsListingEventMetadata[] = []; if (listings_manifest.shards.length > 0) { const shard = listings_manifest.shards[0]; - const res_country_shard = await fetch(`${indexes_url}/events/30402/nip05/${nip05}/${shard.file}?v=${shard.sha256}`); + const res_country_shard = await fetch(`${idx_url}/events/30402/nip05/${nip05}/${shard.file}?v=${shard.sha256}`); if (!res_country_shard.ok) error(500, { message: `nip05:listing:shard:${nip05}:${shard.file}` }); listings_events = await res_country_shard.json(); } diff --git a/app/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts b/app/src/routes/(market)/(profile)/profile/[0=npub]/+page.ts @@ -4,7 +4,7 @@ import type { RadrootsProfileEventMetadata } from "@radroots/events-bindings"; import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; -const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env; +const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env; export const entries: EntryGenerator = async () => { const [ @@ -12,7 +12,7 @@ export const entries: EntryGenerator = async () => { ]: [ string[] ] = await Promise.all([ - fetch(`${indexes_url}/events/0/npub/indexes.json`).then(r => r.json()) + fetch(`${idx_url}/events/0/npub/indexes.json`).then(r => r.json()) ]); return events_0_author_indexes.map(i => ({ 0: i })) }; @@ -25,7 +25,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { const [ res_npub_metadata, ] = await Promise.all([ - fetch(`${indexes_url}/events/0/npub/${npub}/metadata.json`) + fetch(`${idx_url}/events/0/npub/${npub}/metadata.json`) ]); if (!res_npub_metadata.ok) error(404, { message: `npub:${npub}` }); diff --git a/app/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts b/app/src/routes/(market)/(profile)/profile/[0=public_key]/+page.ts @@ -5,7 +5,7 @@ import { lib_nostr_npub_encode } from "@radroots/utils-nostr"; import { error } from "@sveltejs/kit"; import type { EntryGenerator, PageLoad } from "./$types"; -const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env; +const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env; export const entries: EntryGenerator = async () => { const [ @@ -13,7 +13,7 @@ export const entries: EntryGenerator = async () => { ]: [ string[] ] = await Promise.all([ - fetch(`${indexes_url}/events/0/author/indexes.json`).then(r => r.json()) + fetch(`${idx_url}/events/0/author/indexes.json`).then(r => r.json()) ]); return events_0_author_indexes.map(i => ({ 0: i })) }; @@ -26,7 +26,7 @@ export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { const [ res_author_metadata, ] = await Promise.all([ - fetch(`${indexes_url}/events/0/author/${public_key}/metadata.json`), + fetch(`${idx_url}/events/0/author/${public_key}/metadata.json`), ]); if (!res_author_metadata.ok) error(404, { message: `public_key:${public_key}` }); diff --git a/app/src/routes/+page.ts b/app/src/routes/+page.ts @@ -2,7 +2,7 @@ import { _env } from "$lib/utils/_env"; import { error } from "@sveltejs/kit"; import type { PageLoad } from "./$types"; -const { RADROOTS_MARKET_RELAY_INDEXES_URL: indexes_url } = _env; +const { RADROOTS_MARKET_RELAY_INDEXES_URL: idx_url } = _env; type PageLoadData = { profiles: string[]; @@ -10,13 +10,12 @@ type PageLoadData = { }; export const load: PageLoad<PageLoadData> = async ({ fetch, params }) => { - const [ res_nip05_indexes, res_country_indexes, ] = await Promise.all([ - fetch(`${indexes_url}/events/30402/nip05/indexes.json`), - fetch(`${indexes_url}/events/30402/country/indexes.json`), + fetch(`${idx_url}/events/30402/nip05/indexes.json`), + fetch(`${idx_url}/events/30402/country/indexes.json`), ]); if (!res_nip05_indexes.ok) error(404, { message: `nip05:indexes` }); diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml @@ -11,6 +11,7 @@ audit = [] [dependencies] indexer-utils = { path = "../indexer-utils" } +radroots-core = { workspace = true } radroots-events = { workspace = true } radroots-events-indexed = { workspace = true } diff --git a/crates/indexer/src/domain/events/listing.rs b/crates/indexer/src/domain/events/listing.rs @@ -1,7 +1,11 @@ use thiserror::Error; use radroots_events::{ - listing::models::{RadrootsListing, RadrootsListingEventIndex, RadrootsListingEventMetadata}, + listing::models::{ + RadrootsListing, RadrootsListingEventIndex, RadrootsListingEventMetadata, + RadrootsListingImage, RadrootsListingImageSize, RadrootsListingLocation, + RadrootsListingPrice, RadrootsListingProduct, RadrootsListingQuantity, + }, RadrootsNostrEvent, }; @@ -9,8 +13,200 @@ use crate::relay::event::RelayIndexerEvent; #[derive(Debug, Error)] pub enum RadrootsListingEventIndexError { - #[error("Failed to parse listing JSON: {0}")] - ParseError(#[from] serde_json::Error), + #[error("Failed to parse listing from tags")] + ParseError, +} + +fn parse_listing_from_tags( + tags: &[Vec<String>], +) -> Result<RadrootsListing, RadrootsListingEventIndexError> { + let get_first = |key: &str| -> Option<String> { + tags.iter() + .find(|t| { + t.get(0) + .map(|s| s.eq_ignore_ascii_case(key)) + .unwrap_or(false) + }) + .and_then(|t| t.get(1).cloned()) + }; + + let required = |v: Option<String>| v.ok_or(RadrootsListingEventIndexError::ParseError); + + let d_tag = required(get_first("d"))?; + + let product = RadrootsListingProduct { + key: required(get_first("key"))?, + title: required(get_first("title"))?, + category: required(get_first("category"))?, + summary: get_first("summary"), + process: get_first("process"), + lot: get_first("lot"), + location: get_first("location"), + profile: get_first("profile"), + year: get_first("year"), + }; + + let mut quantities: Vec<RadrootsListingQuantity> = Vec::new(); + for t in tags + .iter() + .filter(|t| t.first().map(|k| k == "quantity").unwrap_or(false)) + { + if t.len() >= 3 { + let amount = match t[1].parse::<radroots_core::RadrootsCoreDecimal>() { + Ok(v) => v, + Err(_) => continue, + }; + let unit = match t[2].parse::<radroots_core::RadrootsCoreUnit>() { + Ok(v) => v, + Err(_) => continue, + }; + let label = t.get(3).cloned(); + quantities.push(RadrootsListingQuantity { + value: radroots_core::RadrootsCoreQuantity { + amount, + unit, + label: label.clone(), + }, + label, + count: None, + }); + } + } + + let mut prices: Vec<RadrootsListingPrice> = Vec::new(); + for t in tags + .iter() + .filter(|t| t.first().map(|k| k == "price").unwrap_or(false)) + { + if t.len() >= 5 { + let money_amount = match t[1].parse::<radroots_core::RadrootsCoreDecimal>() { + Ok(v) => v, + Err(_) => continue, + }; + let money_currency = match t[2].parse::<radroots_core::RadrootsCoreCurrency>() { + Ok(v) => v, + Err(_) => continue, + }; + let qty_amount = match t[3].parse::<radroots_core::RadrootsCoreDecimal>() { + Ok(v) => v, + Err(_) => continue, + }; + let qty_unit = match t[4].parse::<radroots_core::RadrootsCoreUnit>() { + Ok(v) => v, + Err(_) => continue, + }; + + let price = radroots_core::RadrootsCoreQuantityPrice { + amount: radroots_core::RadrootsCoreMoney { + amount: money_amount, + currency: money_currency, + }, + quantity: radroots_core::RadrootsCoreQuantity { + amount: qty_amount, + unit: qty_unit, + label: None, + }, + }; + prices.push(price); + } + } + + let mut primary: Option<String> = None; + let mut city: Option<String> = None; + let mut region: Option<String> = None; + let mut country: Option<String> = None; + if let Some(t) = tags + .iter() + .find(|t| t.first().map(|k| k == "location").unwrap_or(false)) + { + if t.len() >= 2 { + primary = Some(t[1].clone()); + } + if t.len() >= 3 { + city = Some(t[2].clone()); + } + if t.len() >= 4 { + region = Some(t[3].clone()); + } + if t.len() >= 5 { + country = Some(t[4].clone()); + } + } + + let geohash = tags + .iter() + .filter(|t| t.first().map(|k| k == "g").unwrap_or(false)) + .filter_map(|t| t.get(1).cloned()) + .max_by_key(|s| s.len()); + + let mut lat: Option<f64> = None; + let mut lng: Option<f64> = None; + for t in tags.iter().filter(|t| { + t.first() + .map(|k| k.eq_ignore_ascii_case("l")) + .unwrap_or(false) + }) { + if t.len() >= 3 { + let val = t[1].parse::<f64>().ok(); + let label = t[2].as_str(); + match label { + "dd.lat" => lat = val, + "dd.lon" => lng = val, + _ => {} + } + } + } + + let location = if primary.is_some() + || city.is_some() + || region.is_some() + || country.is_some() + || lat.is_some() + || lng.is_some() + || geohash.is_some() + { + Some(RadrootsListingLocation { + primary: primary.unwrap_or_default(), + city, + region, + country, + lat, + lng, + geohash, + }) + } else { + None + }; + + let images: Option<Vec<RadrootsListingImage>> = tags + .iter() + .filter(|t| t.first().map(|k| k == "img").unwrap_or(false)) + .map(|t| { + let url = t.get(1).cloned().unwrap_or_default(); + let size = if t.len() >= 4 { + let w = t[2].parse::<u32>().ok(); + let h = t[3].parse::<u32>().ok(); + match (w, h) { + (Some(w), Some(h)) => Some(RadrootsListingImageSize { w, h }), + _ => None, + } + } else { + None + }; + RadrootsListingImage { url, size } + }) + .collect::<Vec<_>>() + .into(); + + Ok(RadrootsListing { + d_tag, + product, + quantities, + prices, + discounts: None, + location, + images, + }) } fn create_radroots_listing_event_metadata( @@ -18,10 +214,10 @@ fn create_radroots_listing_event_metadata( author: String, published_at: u32, kind: u32, - content: String, - _tags: Vec<Vec<String>>, + _content: String, + tags: Vec<Vec<String>>, ) -> Result<RadrootsListingEventMetadata, RadrootsListingEventIndexError> { - let listing: RadrootsListing = serde_json::from_str(&content)?; + let listing = parse_listing_from_tags(&tags)?; Ok(RadrootsListingEventMetadata { id, author, diff --git a/package.json b/package.json @@ -5,7 +5,8 @@ "scripts": { "build": "turbo build", "build:app": "turbo build --filter=app --filter=@radroots/*", - "dev": "turbo dev --filter=@radroots/*" + "dev:lib": "turbo dev --filter=@radroots/* --concurrency 16", + "dev:app": "cd app && yarn dev" }, "devDependencies": { "turbo": "2.5.3",