lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit d0a2ab090fa2a9cf60473327e44aca3528a20ca2
parent 210afb3014fddc7a8debebfce0d29302491595cc
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Dec 2025 20:41:03 +0000

codec: add farm/plot event support and actor tags


- Add farm/plot event models and kind constants
- Implement farm/plot tag encode/decode with required tag validation
- Expose farm_tags/plot_tags in wasm and tag builder trait impls
- Add list private entry JSON helpers and profile actor tag parsing/encoding

Diffstat:
Mevents-codec-wasm/src/lib.rs | 26++++++++++++++++++++++++++
Aevents-codec/src/farm/decode.rs | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/farm/encode.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/farm/mod.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mevents-codec/src/lib.rs | 2++
Mevents-codec/src/list/decode.rs | 24++++++++++++++++++++----
Mevents-codec/src/list/encode.rs | 20+++++++++++++++++---
Mevents-codec/src/list_set/decode.rs | 11+++++++++++
Mevents-codec/src/list_set/encode.rs | 10++++++++++
Aevents-codec/src/plot/decode.rs | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/plot/encode.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/plot/mod.rs | 38++++++++++++++++++++++++++++++++++++++
Mevents-codec/src/profile/decode.rs | 20++++++++++++++++++--
Mevents-codec/src/profile/encode.rs | 42++++++++++++++++++++++++++++++++++++++----
Mevents-codec/src/tag_builders.rs | 20++++++++++++++++++++
Aevents-codec/tests/list_private.rs | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mevents-codec/tests/profile.rs | 20+++++++++++++++++++-
Mevents-codec/tests/profile_encode.rs | 30++++++++++++++++++++++++++++--
Mevents/bindings/ts/src/schemas.ts | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mevents/bindings/ts/src/types.ts | 22+++++++++++++++++++++-
Mevents/bindings/ts/src/typeshare-types.ts | 2++
Aevents/src/farm.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mevents/src/kinds.rs | 4++++
Mevents/src/lib.rs | 2++
Aevents/src/plot.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mevents/src/profile.rs | 31+++++++++++++++++++++++++++++++
Mevents/src/typeshare_kinds.rs | 4++++
Mnostr/src/event_adapters.rs | 21++++++++++++++++++++-
28 files changed, 1077 insertions(+), 18 deletions(-)

diff --git a/events-codec-wasm/src/lib.rs b/events-codec-wasm/src/lib.rs @@ -3,6 +3,7 @@ use radroots_events::comment::RadrootsComment; use radroots_events::follow::RadrootsFollow; +use radroots_events::farm::RadrootsFarm; use radroots_events::job_feedback::RadrootsJobFeedback; use radroots_events::job_request::RadrootsJobRequest; use radroots_events::job_result::RadrootsJobResult; @@ -11,11 +12,13 @@ use radroots_events::list::RadrootsList; use radroots_events::list_set::RadrootsListSet; use radroots_events::message::RadrootsMessage; use radroots_events::message_file::RadrootsMessageFile; +use radroots_events::plot::RadrootsPlot; use radroots_events::reaction::RadrootsReaction; use radroots_events::gift_wrap::RadrootsGiftWrap; use radroots_events::seal::RadrootsSeal; use radroots_events_codec::comment::encode::comment_build_tags; use radroots_events_codec::follow::encode::follow_build_tags; +use radroots_events_codec::farm::encode::farm_build_tags; use radroots_events_codec::gift_wrap::encode::gift_wrap_build_tags; use radroots_events_codec::job::feedback::encode::job_feedback_build_tags; use radroots_events_codec::job::request::encode::job_request_build_tags; @@ -24,6 +27,7 @@ use radroots_events_codec::list::encode::list_build_tags; use radroots_events_codec::list_set::encode::list_set_build_tags; use radroots_events_codec::message::encode::message_build_tags; use radroots_events_codec::message_file::encode::message_file_build_tags; +use radroots_events_codec::plot::encode::plot_build_tags; use radroots_events_codec::reaction::encode::reaction_build_tags; use radroots_events_codec::seal::encode::seal_build_tags; use radroots_events_codec::listing::tags::{ @@ -48,6 +52,10 @@ fn parse_follow(follow_json: &str) -> Result<RadrootsFollow, JsValue> { serde_json::from_str(follow_json).map_err(err_js) } +fn parse_farm(farm_json: &str) -> Result<RadrootsFarm, JsValue> { + serde_json::from_str(farm_json).map_err(err_js) +} + fn parse_job_request(job_json: &str) -> Result<RadrootsJobRequest, JsValue> { serde_json::from_str(job_json).map_err(err_js) } @@ -72,6 +80,10 @@ fn parse_message_file(message_json: &str) -> Result<RadrootsMessageFile, JsValue serde_json::from_str(message_json).map_err(err_js) } +fn parse_plot(plot_json: &str) -> Result<RadrootsPlot, JsValue> { + serde_json::from_str(plot_json).map_err(err_js) +} + fn parse_gift_wrap(gift_wrap_json: &str) -> Result<RadrootsGiftWrap, JsValue> { serde_json::from_str(gift_wrap_json).map_err(err_js) } @@ -120,6 +132,13 @@ pub fn follow_tags(follow_json: &str) -> Result<String, JsValue> { tags_to_json(tags) } +#[wasm_bindgen(js_name = farm_tags)] +pub fn farm_tags(farm_json: &str) -> Result<String, JsValue> { + let farm = parse_farm(farm_json)?; + let tags = farm_build_tags(&farm).map_err(err_js)?; + tags_to_json(tags) +} + #[wasm_bindgen(js_name = list_tags)] pub fn list_tags(list_json: &str) -> Result<String, JsValue> { let list = parse_list(list_json)?; @@ -134,6 +153,13 @@ pub fn list_set_tags(list_json: &str) -> Result<String, JsValue> { tags_to_json(tags) } +#[wasm_bindgen(js_name = plot_tags)] +pub fn plot_tags(plot_json: &str) -> Result<String, JsValue> { + let plot = parse_plot(plot_json)?; + let tags = plot_build_tags(&plot).map_err(err_js)?; + tags_to_json(tags) +} + #[wasm_bindgen(js_name = job_request_tags)] pub fn job_request_tags(job_json: &str) -> Result<String, JsValue> { let job = parse_job_request(job_json)?; diff --git a/events-codec/src/farm/decode.rs b/events-codec/src/farm/decode.rs @@ -0,0 +1,106 @@ +#![cfg(feature = "serde_json")] + +#[cfg(not(feature = "std"))] +use alloc::{string::{String, ToString}, vec::Vec}; + +use radroots_events::{ + RadrootsNostrEvent, + farm::{RadrootsFarm, RadrootsFarmEventIndex, RadrootsFarmEventMetadata}, + kinds::KIND_FARM, + tags::TAG_D, +}; + +use crate::error::EventParseError; + +const DEFAULT_KIND: u32 = KIND_FARM; + +fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { + let tag = tags + .iter() + .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D)) + .ok_or(EventParseError::MissingTag(TAG_D))?; + let value = tag + .get(1) + .map(|s| s.to_string()) + .ok_or(EventParseError::InvalidTag(TAG_D))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_D)); + } + Ok(value) +} + +pub fn farm_from_event( + kind: u32, + tags: &[Vec<String>], + content: &str, +) -> Result<RadrootsFarm, EventParseError> { + if kind != DEFAULT_KIND { + return Err(EventParseError::InvalidKind { + expected: "30340", + got: kind, + }); + } + if content.trim().is_empty() { + return Err(EventParseError::InvalidJson("content")); + } + let d_tag = parse_d_tag(tags)?; + let mut farm: RadrootsFarm = + serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?; + + if farm.d_tag.trim().is_empty() { + farm.d_tag = d_tag; + } else if farm.d_tag != d_tag { + return Err(EventParseError::InvalidTag(TAG_D)); + } + + Ok(farm) +} + +pub fn metadata_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, +) -> Result<RadrootsFarmEventMetadata, EventParseError> { + let farm = farm_from_event(kind, &tags, &content)?; + Ok(RadrootsFarmEventMetadata { + id, + author, + published_at, + kind, + farm, + }) +} + +pub fn index_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, + sig: String, +) -> Result<RadrootsFarmEventIndex, EventParseError> { + let metadata = metadata_from_event( + id.clone(), + author.clone(), + published_at, + kind, + content.clone(), + tags.clone(), + )?; + Ok(RadrootsFarmEventIndex { + event: RadrootsNostrEvent { + id, + author, + created_at: published_at, + kind, + content, + tags, + sig, + }, + metadata, + }) +} diff --git a/events-codec/src/farm/encode.rs b/events-codec/src/farm/encode.rs @@ -0,0 +1,82 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::{String, ToString}, vec::Vec}; + +use radroots_events::{ + farm::{RadrootsFarm, RadrootsFarmRef}, + kinds::KIND_FARM, + tags::TAG_D, +}; + +use crate::error::EventEncodeError; + +#[cfg(feature = "serde_json")] +use crate::wire::WireEventParts; + +const TAG_T: &str = "t"; +const TAG_G: &str = "g"; + +fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) { + let mut tag = Vec::with_capacity(2); + tag.push(key.to_string()); + tag.push(value.to_string()); + tags.push(tag); +} + +pub fn farm_build_tags(farm: &RadrootsFarm) -> Result<Vec<Vec<String>>, EventEncodeError> { + if farm.d_tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("d_tag")); + } + if farm.name.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("name")); + } + let mut tags = Vec::new(); + push_tag(&mut tags, TAG_D, &farm.d_tag); + if let Some(items) = farm.tags.as_ref() { + for item in items.iter().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_T, item); + } + } + if let Some(location) = farm.location.as_ref() { + if let Some(geohash) = location.geohash.as_ref().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_G, geohash); + } + } + Ok(tags) +} + +pub fn farm_ref_tags(farm: &RadrootsFarmRef) -> Result<Vec<Vec<String>>, EventEncodeError> { + if farm.pubkey.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("farm.pubkey")); + } + if farm.d_tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("farm.d_tag")); + } + let mut addr = String::new(); + addr.push_str(&KIND_FARM.to_string()); + addr.push(':'); + addr.push_str(&farm.pubkey); + addr.push(':'); + addr.push_str(&farm.d_tag); + let mut tags = Vec::with_capacity(2); + push_tag(&mut tags, "p", &farm.pubkey); + push_tag(&mut tags, "a", &addr); + Ok(tags) +} + +#[cfg(feature = "serde_json")] +pub fn to_wire_parts(farm: &RadrootsFarm) -> Result<WireEventParts, EventEncodeError> { + to_wire_parts_with_kind(farm, KIND_FARM) +} + +#[cfg(feature = "serde_json")] +pub fn to_wire_parts_with_kind( + farm: &RadrootsFarm, + kind: u32, +) -> Result<WireEventParts, EventEncodeError> { + if kind != KIND_FARM { + return Err(EventEncodeError::InvalidKind(kind)); + } + let tags = farm_build_tags(farm)?; + let content = serde_json::to_string(farm).map_err(|_| EventEncodeError::Json)?; + Ok(WireEventParts { kind, content, tags }) +} diff --git a/events-codec/src/farm/mod.rs b/events-codec/src/farm/mod.rs @@ -0,0 +1,49 @@ +pub mod decode; +pub mod encode; + +#[cfg(test)] +mod tests { + use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; + use crate::farm::encode::{farm_build_tags, farm_ref_tags}; + + #[test] + fn farm_tags_include_required_fields() { + let farm = RadrootsFarm { + d_tag: "farm-1".to_string(), + name: "Test Farm".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location: Some(RadrootsFarmLocation { + primary: "Somewhere".to_string(), + city: None, + region: None, + country: None, + lat: None, + lng: None, + geohash: Some("9q8yy".to_string()), + }), + tags: Some(vec!["orchard".to_string()]), + }; + + let tags = farm_build_tags(&farm).expect("tags"); + assert!(tags.iter().any(|tag| tag.get(0) == Some(&"d".to_string()))); + assert!(tags.iter().any(|tag| tag.get(0) == Some(&"t".to_string()))); + assert!(tags.iter().any(|tag| tag.get(0) == Some(&"g".to_string()))); + } + + #[test] + fn farm_ref_tags_include_p_and_a() { + let farm = RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: "farm-1".to_string(), + }; + + let tags = farm_ref_tags(&farm).expect("farm ref tags"); + let has_a = tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("a")); + let has_p = tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("p")); + assert!(has_a); + assert!(has_p); + } +} diff --git a/events-codec/src/lib.rs b/events-codec/src/lib.rs @@ -13,10 +13,12 @@ pub mod wire; pub mod comment; pub mod follow; pub mod app_data; +pub mod farm; pub mod gift_wrap; pub mod message; pub mod message_file; pub mod post; +pub mod plot; pub mod reaction; pub mod seal; diff --git a/events-codec/src/list/decode.rs b/events-codec/src/list/decode.rs @@ -24,6 +24,16 @@ fn entry_from_tag(tag: &[String]) -> Result<RadrootsListEntry, EventParseError> }) } +pub fn list_entries_from_tags( + tags: &[Vec<String>], +) -> Result<Vec<RadrootsListEntry>, EventParseError> { + let mut entries = Vec::with_capacity(tags.len()); + for tag in tags.iter().filter(|t| t.len() >= 2) { + entries.push(entry_from_tag(tag)?); + } + Ok(entries) +} + pub fn list_from_tags( kind: u32, content: String, @@ -35,10 +45,7 @@ pub fn list_from_tags( got: kind, }); } - let mut entries = Vec::new(); - for tag in tags.iter().filter(|t| t.len() >= 2) { - entries.push(entry_from_tag(tag)?); - } + let entries = list_entries_from_tags(tags)?; Ok(RadrootsList { content, entries }) } @@ -90,3 +97,12 @@ pub fn index_from_event( metadata, }) } + +#[cfg(feature = "serde_json")] +pub fn list_private_entries_from_json( + content: &str, +) -> Result<Vec<RadrootsListEntry>, EventParseError> { + let tags: Vec<Vec<String>> = + serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?; + list_entries_from_tags(&tags) +} diff --git a/events-codec/src/list/encode.rs b/events-codec/src/list/encode.rs @@ -26,14 +26,20 @@ fn entry_tag(entry: &RadrootsListEntry) -> Result<Vec<String>, EventEncodeError> Ok(tag) } -pub fn list_build_tags(list: &RadrootsList) -> Result<Vec<Vec<String>>, EventEncodeError> { - let mut tags = Vec::with_capacity(list.entries.len()); - for entry in &list.entries { +pub fn list_entries_to_tags( + entries: &[RadrootsListEntry], +) -> Result<Vec<Vec<String>>, EventEncodeError> { + let mut tags = Vec::with_capacity(entries.len()); + for entry in entries { tags.push(entry_tag(entry)?); } Ok(tags) } +pub fn list_build_tags(list: &RadrootsList) -> Result<Vec<Vec<String>>, EventEncodeError> { + list_entries_to_tags(&list.entries) +} + pub fn to_wire_parts_with_kind( list: &RadrootsList, kind: u32, @@ -48,3 +54,11 @@ pub fn to_wire_parts_with_kind( tags, }) } + +#[cfg(feature = "serde_json")] +pub fn list_private_entries_json( + entries: &[RadrootsListEntry], +) -> Result<String, EventEncodeError> { + let tags = list_entries_to_tags(entries)?; + serde_json::to_string(&tags).map_err(|_| EventEncodeError::Json) +} diff --git a/events-codec/src/list_set/decode.rs b/events-codec/src/list_set/decode.rs @@ -9,6 +9,8 @@ use radroots_events::{ }; use crate::error::EventParseError; +#[cfg(feature = "serde_json")] +use crate::list::decode::list_entries_from_tags; const TAG_D: &str = "d"; const TAG_TITLE: &str = "title"; @@ -148,3 +150,12 @@ pub fn index_from_event( metadata, }) } + +#[cfg(feature = "serde_json")] +pub fn list_set_private_entries_from_json( + content: &str, +) -> Result<Vec<RadrootsListEntry>, EventParseError> { + let tags: Vec<Vec<String>> = + serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?; + list_entries_from_tags(&tags) +} diff --git a/events-codec/src/list_set/encode.rs b/events-codec/src/list_set/encode.rs @@ -7,6 +7,8 @@ use radroots_events::{ }; use crate::error::EventEncodeError; +#[cfg(feature = "serde_json")] +use crate::list::encode::list_entries_to_tags; use crate::wire::WireEventParts; const TAG_D: &str = "d"; @@ -69,3 +71,11 @@ pub fn to_wire_parts_with_kind( tags, }) } + +#[cfg(feature = "serde_json")] +pub fn list_set_private_entries_json( + entries: &[radroots_events::list::RadrootsListEntry], +) -> Result<String, EventEncodeError> { + let tags = list_entries_to_tags(entries)?; + serde_json::to_string(&tags).map_err(|_| EventEncodeError::Json) +} diff --git a/events-codec/src/plot/decode.rs b/events-codec/src/plot/decode.rs @@ -0,0 +1,166 @@ +#![cfg(feature = "serde_json")] + +#[cfg(not(feature = "std"))] +use alloc::{string::{String, ToString}, vec::Vec}; + +use radroots_events::{ + RadrootsNostrEvent, + kinds::{KIND_FARM, KIND_PLOT}, + farm::RadrootsFarmRef, + plot::{RadrootsPlot, RadrootsPlotEventIndex, RadrootsPlotEventMetadata}, + tags::TAG_D, +}; + +use crate::error::EventParseError; + +const TAG_A: &str = "a"; +const TAG_P: &str = "p"; +const DEFAULT_KIND: u32 = KIND_PLOT; + +fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { + let tag = tags + .iter() + .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D)) + .ok_or(EventParseError::MissingTag(TAG_D))?; + let value = tag + .get(1) + .map(|s| s.to_string()) + .ok_or(EventParseError::InvalidTag(TAG_D))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_D)); + } + Ok(value) +} + +fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, EventParseError> { + let tag = tags + .iter() + .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A)) + .ok_or(EventParseError::MissingTag(TAG_A))?; + let value = tag + .get(1) + .map(|s| s.to_string()) + .ok_or(EventParseError::InvalidTag(TAG_A))?; + let mut parts = value.splitn(3, ':'); + let kind = parts + .next() + .and_then(|v| v.parse::<u32>().ok()) + .ok_or(EventParseError::InvalidTag(TAG_A))?; + if kind != KIND_FARM { + return Err(EventParseError::InvalidTag(TAG_A)); + } + let pubkey = parts + .next() + .ok_or(EventParseError::InvalidTag(TAG_A))? + .to_string(); + let d_tag = parts + .next() + .ok_or(EventParseError::InvalidTag(TAG_A))? + .to_string(); + if pubkey.trim().is_empty() || d_tag.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_A)); + } + Ok(RadrootsFarmRef { pubkey, d_tag }) +} + +fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, EventParseError> { + let tag = tags + .iter() + .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_P)) + .ok_or(EventParseError::MissingTag(TAG_P))?; + let value = tag + .get(1) + .map(|s| s.to_string()) + .ok_or(EventParseError::InvalidTag(TAG_P))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_P)); + } + Ok(value) +} + +pub fn plot_from_event( + kind: u32, + tags: &[Vec<String>], + content: &str, +) -> Result<RadrootsPlot, EventParseError> { + if kind != DEFAULT_KIND { + return Err(EventParseError::InvalidKind { + expected: "30350", + got: kind, + }); + } + if content.trim().is_empty() { + return Err(EventParseError::InvalidJson("content")); + } + let d_tag = parse_d_tag(tags)?; + let farm_ref = parse_farm_ref(tags)?; + let farm_pubkey = parse_farm_pubkey(tags)?; + let mut plot: RadrootsPlot = + serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?; + + if plot.d_tag.trim().is_empty() { + plot.d_tag = d_tag; + } else if plot.d_tag != d_tag { + return Err(EventParseError::InvalidTag(TAG_D)); + } + + if plot.farm.pubkey.trim().is_empty() || plot.farm.d_tag.trim().is_empty() { + plot.farm = farm_ref; + } else if plot.farm.pubkey != farm_ref.pubkey || plot.farm.d_tag != farm_ref.d_tag { + return Err(EventParseError::InvalidTag(TAG_A)); + } + if plot.farm.pubkey != farm_pubkey { + return Err(EventParseError::InvalidTag(TAG_P)); + } + + Ok(plot) +} + +pub fn metadata_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, +) -> Result<RadrootsPlotEventMetadata, EventParseError> { + let plot = plot_from_event(kind, &tags, &content)?; + Ok(RadrootsPlotEventMetadata { + id, + author, + published_at, + kind, + plot, + }) +} + +pub fn index_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, + sig: String, +) -> Result<RadrootsPlotEventIndex, EventParseError> { + let metadata = metadata_from_event( + id.clone(), + author.clone(), + published_at, + kind, + content.clone(), + tags.clone(), + )?; + Ok(RadrootsPlotEventIndex { + event: RadrootsNostrEvent { + id, + author, + created_at: published_at, + kind, + content, + tags, + sig, + }, + metadata, + }) +} diff --git a/events-codec/src/plot/encode.rs b/events-codec/src/plot/encode.rs @@ -0,0 +1,87 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::{String, ToString}, vec::Vec}; + +use radroots_events::{ + farm::RadrootsFarmRef, + kinds::KIND_FARM, + plot::RadrootsPlot, + tags::TAG_D, +}; + +#[cfg(feature = "serde_json")] +use radroots_events::kinds::KIND_PLOT; + +use crate::error::EventEncodeError; + +#[cfg(feature = "serde_json")] +use crate::wire::WireEventParts; + +const TAG_T: &str = "t"; +const TAG_G: &str = "g"; +const TAG_A: &str = "a"; +const TAG_P: &str = "p"; + +fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) { + let mut tag = Vec::with_capacity(2); + tag.push(key.to_string()); + tag.push(value.to_string()); + tags.push(tag); +} + +fn farm_address(farm: &RadrootsFarmRef) -> String { + let mut value = String::new(); + value.push_str(&KIND_FARM.to_string()); + value.push(':'); + value.push_str(&farm.pubkey); + value.push(':'); + value.push_str(&farm.d_tag); + value +} + +pub fn plot_build_tags(plot: &RadrootsPlot) -> Result<Vec<Vec<String>>, EventEncodeError> { + if plot.d_tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("d_tag")); + } + if plot.name.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("name")); + } + if plot.farm.pubkey.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("farm.pubkey")); + } + if plot.farm.d_tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("farm.d_tag")); + } + let mut tags = Vec::new(); + push_tag(&mut tags, TAG_D, &plot.d_tag); + push_tag(&mut tags, TAG_A, &farm_address(&plot.farm)); + push_tag(&mut tags, TAG_P, &plot.farm.pubkey); + if let Some(items) = plot.tags.as_ref() { + for item in items.iter().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_T, item); + } + } + if let Some(location) = plot.location.as_ref() { + if let Some(geohash) = location.geohash.as_ref().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_G, geohash); + } + } + Ok(tags) +} + +#[cfg(feature = "serde_json")] +pub fn to_wire_parts(plot: &RadrootsPlot) -> Result<WireEventParts, EventEncodeError> { + to_wire_parts_with_kind(plot, KIND_PLOT) +} + +#[cfg(feature = "serde_json")] +pub fn to_wire_parts_with_kind( + plot: &RadrootsPlot, + kind: u32, +) -> Result<WireEventParts, EventEncodeError> { + if kind != KIND_PLOT { + return Err(EventEncodeError::InvalidKind(kind)); + } + let tags = plot_build_tags(plot)?; + let content = serde_json::to_string(plot).map_err(|_| EventEncodeError::Json)?; + Ok(WireEventParts { kind, content, tags }) +} diff --git a/events-codec/src/plot/mod.rs b/events-codec/src/plot/mod.rs @@ -0,0 +1,38 @@ +pub mod decode; +pub mod encode; + +#[cfg(test)] +mod tests { + use radroots_events::{farm::RadrootsFarmRef, plot::{RadrootsPlot, RadrootsPlotLocation}}; + use crate::plot::encode::plot_build_tags; + + #[test] + fn plot_tags_include_farm_address() { + let plot = RadrootsPlot { + d_tag: "plot-1".to_string(), + farm: RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: "farm-1".to_string(), + }, + name: "Orchard".to_string(), + about: None, + location: Some(RadrootsPlotLocation { + primary: "Somewhere".to_string(), + city: None, + region: None, + country: None, + lat: None, + lng: None, + geohash: None, + }), + geometry: None, + tags: Some(vec!["orchard".to_string()]), + }; + + let tags = plot_build_tags(&plot).expect("tags"); + let has_a = tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("a")); + let has_p = tags.iter().any(|tag| tag.get(0).map(|v| v.as_str()) == Some("p")); + assert!(has_a); + assert!(has_p); + } +} diff --git a/events-codec/src/profile/decode.rs b/events-codec/src/profile/decode.rs @@ -5,7 +5,14 @@ use alloc::{string::{String, ToString}, vec::Vec}; use radroots_events::{ RadrootsNostrEvent, - profile::{RadrootsProfile, RadrootsProfileEventIndex, RadrootsProfileEventMetadata}, + profile::{ + RadrootsActorType, + RadrootsProfile, + RadrootsProfileEventIndex, + RadrootsProfileEventMetadata, + RADROOTS_ACTOR_TAG_KEY, + radroots_actor_type_from_tag_value, + }, kinds::KIND_PROFILE, }; @@ -26,6 +33,13 @@ fn parse_bot(value: &Value) -> Option<String> { } } +fn profile_actor_from_tags(tags: &[Vec<String>]) -> Option<RadrootsActorType> { + tags.iter() + .filter(|tag| tag.get(0).map(|v| v.as_str()) == Some(RADROOTS_ACTOR_TAG_KEY)) + .filter_map(|tag| tag.get(1)) + .find_map(|value| radroots_actor_type_from_tag_value(value)) +} + pub fn profile_from_content(content: &str) -> Result<RadrootsProfile, EventParseError> { let value: Value = serde_json::from_str(content).map_err(|_| EventParseError::InvalidJson("content"))?; @@ -57,7 +71,7 @@ pub fn metadata_from_event( published_at: u32, kind: u32, content: String, - _tags: Vec<Vec<String>>, + tags: Vec<Vec<String>>, ) -> Result<RadrootsProfileEventMetadata, EventParseError> { if kind != PROFILE_KIND { return Err(EventParseError::InvalidKind { @@ -66,11 +80,13 @@ pub fn metadata_from_event( }); } let profile = profile_from_content(&content)?; + let actor = profile_actor_from_tags(&tags); Ok(RadrootsProfileEventMetadata { id, author, published_at, kind, + actor, profile, }) } diff --git a/events-codec/src/profile/encode.rs b/events-codec/src/profile/encode.rs @@ -1,15 +1,40 @@ use crate::profile::error::ProfileEncodeError; -use radroots_events::profile::RadrootsProfile; +use radroots_events::profile::{ + RadrootsActorType, + RadrootsProfile, + RADROOTS_ACTOR_TAG_KEY, + radroots_actor_tag_value, +}; use radroots_events::kinds::KIND_PROFILE; use nostr::Metadata; use nostr::prelude::Url; +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + #[cfg(feature = "serde_json")] use crate::wire::WireEventParts; -#[cfg(all(feature = "serde_json", not(feature = "std")))] -use alloc::{string::String, vec::Vec}; +fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) { + let mut tag = Vec::with_capacity(2); + tag.push(key.to_string()); + tag.push(value.to_string()); + tags.push(tag); +} + +pub fn profile_actor_tags(actor: RadrootsActorType) -> Vec<Vec<String>> { + let mut tags = Vec::with_capacity(1); + push_tag(&mut tags, RADROOTS_ACTOR_TAG_KEY, radroots_actor_tag_value(actor)); + tags +} + +pub fn profile_build_tags(actor: Option<RadrootsActorType>) -> Vec<Vec<String>> { + match actor { + Some(value) => profile_actor_tags(value), + None => Vec::new(), + } +} pub fn to_metadata(p: &RadrootsProfile) -> Result<Metadata, ProfileEncodeError> { let mut md = Metadata::new().name(p.name.clone()); @@ -47,11 +72,20 @@ pub fn to_metadata(p: &RadrootsProfile) -> Result<Metadata, ProfileEncodeError> #[cfg(feature = "serde_json")] pub fn to_wire_parts(p: &RadrootsProfile) -> Result<WireEventParts, ProfileEncodeError> { + to_wire_parts_with_actor(p, None) +} + +#[cfg(feature = "serde_json")] +pub fn to_wire_parts_with_actor( + p: &RadrootsProfile, + actor: Option<RadrootsActorType>, +) -> Result<WireEventParts, ProfileEncodeError> { let md = to_metadata(p)?; let content = serde_json::to_string(&md).map_err(|_| ProfileEncodeError::Json)?; + let tags = profile_build_tags(actor); Ok(WireEventParts { kind: KIND_PROFILE, content, - tags: Vec::new(), + tags, }) } diff --git a/events-codec/src/tag_builders.rs b/events-codec/src/tag_builders.rs @@ -9,6 +9,7 @@ use radroots_events::{ app_data::RadrootsAppData, comment::RadrootsComment, follow::RadrootsFollow, + farm::RadrootsFarm, gift_wrap::RadrootsGiftWrap, job_feedback::RadrootsJobFeedback, job_request::RadrootsJobRequest, @@ -18,6 +19,7 @@ use radroots_events::{ list_set::RadrootsListSet, message::RadrootsMessage, message_file::RadrootsMessageFile, + plot::RadrootsPlot, post::RadrootsPost, profile::RadrootsProfile, reaction::RadrootsReaction, @@ -28,6 +30,7 @@ use crate::comment::encode::comment_build_tags; use crate::error::EventEncodeError; use crate::app_data::encode::app_data_build_tags; use crate::follow::encode::follow_build_tags; +use crate::farm::encode::farm_build_tags; use crate::job::encode::JobEncodeError; use crate::job::feedback::encode::job_feedback_build_tags; use crate::job::request::encode::job_request_build_tags; @@ -37,6 +40,7 @@ use crate::list::encode::list_build_tags; use crate::list_set::encode::list_set_build_tags; use crate::message::encode::message_build_tags; use crate::message_file::encode::message_file_build_tags; +use crate::plot::encode::plot_build_tags; use crate::reaction::encode::reaction_build_tags; use crate::gift_wrap::encode::gift_wrap_build_tags; use crate::seal::encode::seal_build_tags; @@ -102,6 +106,14 @@ impl RadrootsEventTagBuilder for RadrootsFollow { } } +impl RadrootsEventTagBuilder for RadrootsFarm { + type Error = EventEncodeError; + + fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> { + farm_build_tags(self) + } +} + impl RadrootsEventTagBuilder for RadrootsList { type Error = EventEncodeError; @@ -118,6 +130,14 @@ impl RadrootsEventTagBuilder for RadrootsListSet { } } +impl RadrootsEventTagBuilder for RadrootsPlot { + type Error = EventEncodeError; + + fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> { + plot_build_tags(self) + } +} + impl RadrootsEventTagBuilder for RadrootsJobRequest { type Error = JobEncodeError; diff --git a/events-codec/tests/list_private.rs b/events-codec/tests/list_private.rs @@ -0,0 +1,47 @@ +#![cfg(feature = "serde_json")] + +use radroots_events::list::RadrootsListEntry; +use radroots_events_codec::list::decode::list_private_entries_from_json; +use radroots_events_codec::list::encode::list_private_entries_json; +use radroots_events_codec::list_set::decode::list_set_private_entries_from_json; +use radroots_events_codec::list_set::encode::list_set_private_entries_json; + +#[test] +fn list_private_entries_roundtrip() { + let entries = vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["pubkey".to_string()], + }, + RadrootsListEntry { + tag: "a".to_string(), + values: vec!["30340:pubkey:farm-1".to_string()], + }, + ]; + + let json = list_private_entries_json(&entries).expect("json"); + let parsed = list_private_entries_from_json(&json).expect("parsed"); + assert_eq!(parsed.len(), entries.len()); + assert_eq!(parsed[0].tag, "p"); + assert_eq!(parsed[1].values[0], "30340:pubkey:farm-1"); +} + +#[test] +fn list_set_private_entries_roundtrip() { + let entries = vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["member".to_string()], + }, + RadrootsListEntry { + tag: "t".to_string(), + values: vec!["orchard".to_string()], + }, + ]; + + let json = list_set_private_entries_json(&entries).expect("json"); + let parsed = list_set_private_entries_from_json(&json).expect("parsed"); + assert_eq!(parsed.len(), entries.len()); + assert_eq!(parsed[0].tag, "p"); + assert_eq!(parsed[1].values[0], "orchard"); +} diff --git a/events-codec/tests/profile.rs b/events-codec/tests/profile.rs @@ -1,6 +1,9 @@ #![cfg(feature = "serde_json")] -use radroots_events::kinds::KIND_POST; +use radroots_events::{ + kinds::KIND_POST, + profile::{RadrootsActorType, RADROOTS_ACTOR_TAG_FARM, RADROOTS_ACTOR_TAG_KEY}, +}; use radroots_events_codec::error::EventParseError; use radroots_events_codec::profile::decode::profile_from_content; @@ -55,3 +58,18 @@ fn profile_metadata_rejects_wrong_kind() { } )); } + +#[test] +fn profile_metadata_reads_actor_tag() { + let metadata = radroots_events_codec::profile::decode::metadata_from_event( + "id".to_string(), + "author".to_string(), + 1, + 0, + "{\"name\":\"alice\"}".to_string(), + vec![vec![RADROOTS_ACTOR_TAG_KEY.to_string(), RADROOTS_ACTOR_TAG_FARM.to_string()]], + ) + .expect("metadata"); + + assert_eq!(metadata.actor, Some(RadrootsActorType::Farm)); +} diff --git a/events-codec/tests/profile_encode.rs b/events-codec/tests/profile_encode.rs @@ -1,7 +1,10 @@ #![cfg(all(feature = "nostr", feature = "serde_json"))] -use radroots_events::{kinds::KIND_PROFILE, profile::RadrootsProfile}; -use radroots_events_codec::profile::encode::{to_metadata, to_wire_parts}; +use radroots_events::{ + kinds::KIND_PROFILE, + profile::{RadrootsActorType, RadrootsProfile, RADROOTS_ACTOR_TAG_FARM, RADROOTS_ACTOR_TAG_KEY}, +}; +use radroots_events_codec::profile::encode::{to_metadata, to_wire_parts, to_wire_parts_with_actor}; use radroots_events_codec::profile::error::ProfileEncodeError; use serde_json::Value; @@ -48,3 +51,26 @@ fn profile_to_wire_parts_writes_json_content() { let value: Value = serde_json::from_str(&parts.content).unwrap(); assert_eq!(value.get("name").and_then(|v| v.as_str()), Some("alice")); } + +#[test] +fn profile_to_wire_parts_with_actor_sets_tag() { + let profile = RadrootsProfile { + name: "farm".to_string(), + display_name: None, + nip05: None, + about: None, + website: None, + picture: None, + banner: None, + lud06: None, + lud16: None, + bot: None, + }; + + let parts = to_wire_parts_with_actor(&profile, Some(RadrootsActorType::Farm)).unwrap(); + assert!(parts + .tags + .iter() + .any(|tag| tag.get(0).map(|v| v.as_str()) == Some(RADROOTS_ACTOR_TAG_KEY) + && tag.get(1).map(|v| v.as_str()) == Some(RADROOTS_ACTOR_TAG_FARM))); +} diff --git a/events/bindings/ts/src/schemas.ts b/events/bindings/ts/src/schemas.ts @@ -134,3 +134,70 @@ export const radroots_follow_profile_schema = z.object({ export const radroots_follow_schema = z.object({ list: z.array(radroots_follow_profile_schema) }); + +export const radroots_list_entry_schema = z.object({ + tag: z.string(), + values: z.array(z.string()) +}); + +export const radroots_list_schema = z.object({ + content: z.string(), + entries: z.array(radroots_list_entry_schema) +}); + +export const radroots_list_set_schema = z.object({ + d_tag: z.string(), + content: z.string(), + entries: z.array(radroots_list_entry_schema), + title: z.string().optional(), + description: z.string().optional(), + image: z.string().optional() +}); + +export const radroots_farm_location_schema = z.object({ + primary: z.string(), + city: z.string().optional(), + region: z.string().optional(), + country: z.string().optional(), + lat: z.number().optional(), + lng: z.number().optional(), + geohash: z.string().optional() +}); + +export const radroots_farm_schema = z.object({ + d_tag: z.string(), + name: z.string(), + about: z.string().optional(), + website: z.string().optional(), + picture: z.string().optional(), + banner: z.string().optional(), + location: radroots_farm_location_schema.optional(), + tags: z.array(z.string()).optional() +}); + +export const radroots_farm_ref_schema = z.object({ + pubkey: z.string(), + d_tag: z.string() +}); + +export const radroots_plot_location_schema = z.object({ + primary: z.string(), + city: z.string().optional(), + region: z.string().optional(), + country: z.string().optional(), + lat: z.number().optional(), + lng: z.number().optional(), + geohash: z.string().optional() +}); + +export const radroots_plot_schema = z.object({ + d_tag: z.string(), + farm: radroots_farm_ref_schema, + name: z.string(), + about: z.string().optional(), + location: radroots_plot_location_schema.optional(), + geometry: z.string().optional(), + tags: z.array(z.string()).optional() +}); + +export const radroots_plot_farm_ref_schema = radroots_farm_ref_schema; diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts @@ -8,6 +8,8 @@ export type JobInputType = "url" | "event" | "job" | "text"; export type JobPaymentRequest = { amount_sat: number, bolt11?: string | null, }; +export type RadrootsActorType = "person" | "farm"; + export type RadrootsAppData = { d_tag: string, content: string, }; export type RadrootsAppDataEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsAppDataEventMetadata, }; @@ -20,6 +22,16 @@ export type RadrootsCommentEventIndex = { event: RadrootsNostrEvent, metadata: R export type RadrootsCommentEventMetadata = { id: string, author: string, published_at: number, kind: number, comment: RadrootsComment, }; +export type RadrootsFarm = { d_tag: string, name: string, about?: string | null, website?: string | null, picture?: string | null, banner?: string | null, location?: RadrootsFarmLocation | null, tags?: string[] | null, }; + +export type RadrootsFarmEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsFarmEventMetadata, }; + +export type RadrootsFarmEventMetadata = { id: string, author: string, published_at: number, kind: number, farm: RadrootsFarm, }; + +export type RadrootsFarmLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, lat?: number | null, lng?: number | null, geohash?: string | null, }; + +export type RadrootsFarmRef = { pubkey: string, d_tag: string, }; + export type RadrootsFollow = { list: Array<RadrootsFollowProfile>, }; export type RadrootsFollowEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsFollowEventMetadata, }; @@ -120,6 +132,14 @@ export type RadrootsNostrEventPtr = { id: string, relays?: string | null, }; export type RadrootsNostrEventRef = { id: string, author: string, kind: number, d_tag?: string | null, relays?: string[] | null, }; +export type RadrootsPlot = { d_tag: string, farm: RadrootsFarmRef, name: string, about?: string | null, location?: RadrootsPlotLocation | null, geometry?: string | null, tags?: string[] | null, }; + +export type RadrootsPlotEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsPlotEventMetadata, }; + +export type RadrootsPlotEventMetadata = { id: string, author: string, published_at: number, kind: number, plot: RadrootsPlot, }; + +export type RadrootsPlotLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, lat?: number | null, lng?: number | null, geohash?: string | null, }; + export type RadrootsPost = { content: string, }; export type RadrootsPostEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsPostEventMetadata, }; @@ -130,7 +150,7 @@ export type RadrootsProfile = { name: string, display_name?: string | null, nip0 export type RadrootsProfileEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsProfileEventMetadata, }; -export type RadrootsProfileEventMetadata = { id: string, author: string, published_at: number, kind: number, profile: RadrootsProfile, }; +export type RadrootsProfileEventMetadata = { id: string, author: string, published_at: number, kind: number, actor?: RadrootsActorType | null, profile: RadrootsProfile, }; export type RadrootsReaction = { root: RadrootsNostrEventRef, content: string, }; diff --git a/events/bindings/ts/src/typeshare-types.ts b/events/bindings/ts/src/typeshare-types.ts @@ -42,6 +42,8 @@ export const KIND_LIST_SET_APP_CURATION: number = 30267; export const KIND_LIST_SET_CALENDAR: number = 31924; export const KIND_LIST_SET_STARTER_PACK: number = 39089; export const KIND_LIST_SET_MEDIA_STARTER_PACK: number = 39092; +export const KIND_FARM: number = 30340; +export const KIND_PLOT: number = 30350; export const KIND_APP_DATA: number = 30078; export const KIND_LISTING: number = 30402; export const KIND_APPLICATION_HANDLER: number = 31990; diff --git a/events/src/farm.rs b/events/src/farm.rs @@ -0,0 +1,77 @@ +use crate::RadrootsNostrEvent; +#[cfg(feature = "ts-rs")] +use ts_rs::TS; + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsFarmEventIndex { + pub event: RadrootsNostrEvent, + pub metadata: RadrootsFarmEventMetadata, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsFarmEventMetadata { + pub id: String, + pub author: String, + pub published_at: u32, + pub kind: u32, + pub farm: RadrootsFarm, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsFarm { + pub d_tag: String, + pub name: String, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub about: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub website: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub picture: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub banner: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsFarmLocation | null"))] + pub location: Option<RadrootsFarmLocation>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string[] | null"))] + pub tags: Option<Vec<String>>, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsFarmRef { + pub pubkey: String, + pub d_tag: String, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsFarmLocation { + pub primary: String, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub city: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub region: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub country: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] + pub lat: Option<f64>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] + pub lng: Option<f64>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub geohash: Option<String>, +} diff --git a/events/src/kinds.rs b/events/src/kinds.rs @@ -79,6 +79,10 @@ pub const KIND_LIST_SET_STARTER_PACK: u32 = 39089; #[cfg_attr(feature = "typeshare", typeshare::typeshare)] pub const KIND_LIST_SET_MEDIA_STARTER_PACK: u32 = 39092; #[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_FARM: u32 = 30340; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_PLOT: u32 = 30350; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] pub const KIND_APP_DATA: u32 = 30078; #[cfg_attr(feature = "typeshare", typeshare::typeshare)] pub const KIND_LISTING: u32 = 30402; diff --git a/events/src/lib.rs b/events/src/lib.rs @@ -21,7 +21,9 @@ pub mod listing; pub mod list; pub mod list_set; pub mod app_data; +pub mod farm; pub mod message; +pub mod plot; pub mod message_file; pub mod post; pub mod profile; diff --git a/events/src/plot.rs b/events/src/plot.rs @@ -0,0 +1,65 @@ +use crate::{RadrootsNostrEvent, farm::RadrootsFarmRef}; +#[cfg(feature = "ts-rs")] +use ts_rs::TS; + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsPlotEventIndex { + pub event: RadrootsNostrEvent, + pub metadata: RadrootsPlotEventMetadata, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsPlotEventMetadata { + pub id: String, + pub author: String, + pub published_at: u32, + pub kind: u32, + pub plot: RadrootsPlot, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsPlot { + pub d_tag: String, + pub farm: RadrootsFarmRef, + pub name: String, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub about: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsPlotLocation | null"))] + pub location: Option<RadrootsPlotLocation>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub geometry: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string[] | null"))] + pub tags: Option<Vec<String>>, +} + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct RadrootsPlotLocation { + pub primary: String, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub city: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub region: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub country: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] + pub lat: Option<f64>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "number | null"))] + pub lng: Option<f64>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub geohash: Option<String>, +} diff --git a/events/src/profile.rs b/events/src/profile.rs @@ -5,6 +5,35 @@ use ts_rs::TS; #[cfg(not(feature = "std"))] use alloc::string::String; +pub const RADROOTS_ACTOR_TAG_KEY: &str = "t"; +pub const RADROOTS_ACTOR_TAG_PERSON: &str = "radroots:actor:person"; +pub const RADROOTS_ACTOR_TAG_FARM: &str = "radroots:actor:farm"; + +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Debug, PartialEq, Eq, Copy)] +pub enum RadrootsActorType { + Person, + Farm, +} + +pub fn radroots_actor_tag_value(actor: RadrootsActorType) -> &'static str { + match actor { + RadrootsActorType::Person => RADROOTS_ACTOR_TAG_PERSON, + RadrootsActorType::Farm => RADROOTS_ACTOR_TAG_FARM, + } +} + +pub fn radroots_actor_type_from_tag_value(value: &str) -> Option<RadrootsActorType> { + match value { + RADROOTS_ACTOR_TAG_PERSON => Some(RadrootsActorType::Person), + RADROOTS_ACTOR_TAG_FARM => Some(RadrootsActorType::Farm), + _ => None, + } +} + #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -23,6 +52,8 @@ pub struct RadrootsProfileEventMetadata { pub author: String, pub published_at: u32, pub kind: u32, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsActorType | null"))] + pub actor: Option<RadrootsActorType>, pub profile: RadrootsProfile, } diff --git a/events/src/typeshare_kinds.rs b/events/src/typeshare_kinds.rs @@ -79,6 +79,10 @@ pub const KIND_LIST_SET_STARTER_PACK: u32 = 39089; #[typeshare::typeshare] pub const KIND_LIST_SET_MEDIA_STARTER_PACK: u32 = 39092; #[typeshare::typeshare] +pub const KIND_FARM: u32 = 30340; +#[typeshare::typeshare] +pub const KIND_PLOT: u32 = 30350; +#[typeshare::typeshare] pub const KIND_APP_DATA: u32 = 30078; #[typeshare::typeshare] pub const KIND_LISTING: u32 = 30402; diff --git a/nostr/src/event_adapters.rs b/nostr/src/event_adapters.rs @@ -1,7 +1,12 @@ #[cfg(feature = "events")] use radroots_events::post::{RadrootsPost, RadrootsPostEventMetadata}; #[cfg(feature = "events")] -use radroots_events::profile::{RadrootsProfile, RadrootsProfileEventMetadata}; +use radroots_events::profile::{ + RadrootsProfile, + RadrootsProfileEventMetadata, + RADROOTS_ACTOR_TAG_KEY, + radroots_actor_type_from_tag_value, +}; #[cfg(feature = "events")] use crate::types::{RadrootsNostrEvent, RadrootsNostrMetadata}; @@ -24,12 +29,25 @@ pub fn to_post_event_metadata(e: &RadrootsNostrEvent) -> RadrootsPostEventMetada #[cfg(feature = "events")] pub fn to_profile_event_metadata(e: &RadrootsNostrEvent) -> Option<RadrootsProfileEventMetadata> { + let actor = e + .tags + .iter() + .filter_map(|tag| { + let values = tag.as_slice(); + if values.get(0).map(|v| v.as_str()) != Some(RADROOTS_ACTOR_TAG_KEY) { + return None; + } + values.get(1).and_then(|value| radroots_actor_type_from_tag_value(value)) + }) + .next(); + if let Ok(p) = serde_json::from_str::<RadrootsProfile>(&e.content) { return Some(RadrootsProfileEventMetadata { id: e.id.to_string(), author: e.pubkey.to_string(), published_at: created_at_u32_saturating(e.created_at), kind: e.kind.as_u16() as u32, + actor, profile: p, }); } @@ -52,6 +70,7 @@ pub fn to_profile_event_metadata(e: &RadrootsNostrEvent) -> Option<RadrootsProfi author: e.pubkey.to_string(), published_at: created_at_u32_saturating(e.created_at), kind: e.kind.as_u16() as u32, + actor, profile: p, }); }