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:
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,
});
}