lib

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

commit 1444b503f120afacd210fd43b9fae11597fcfb9d
parent d0a2ab090fa2a9cf60473327e44aca3528a20ca2
Author: triesap <tyson@radroots.org>
Date:   Sat, 27 Dec 2025 17:37:44 +0000

events-codec: Add farm list sets and standardize profile type tags


- Add farm list-set builders for members/owners/workers/plots and claims
- Introduce plot_address helper and reuse KIND_PLOT in plot encoder
- Rename actor tags/types to profile_type across Rust/TS codecs and adapters
- Allow self-tagging in nostr event builders and add regression test for self p tag

Diffstat:
Aevents-codec/src/farm/list_sets.rs | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mevents-codec/src/farm/mod.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Mevents-codec/src/plot/encode.rs | 23+++++++++++++++++++----
Mevents-codec/src/profile/decode.rs | 16++++++++--------
Mevents-codec/src/profile/encode.rs | 28++++++++++++++++------------
Mevents-codec/tests/profile.rs | 11+++++++----
Mevents-codec/tests/profile_encode.rs | 21+++++++++++++++------
Mevents/bindings/ts/src/types.ts | 6+++---
Mevents/src/profile.rs | 28++++++++++++++--------------
Mnostr/src/event_adapters.rs | 16+++++++++-------
Mnostr/src/events/jobs.rs | 7+++++--
Mnostr/src/events/mod.rs | 24+++++++++++++++++++++++-
12 files changed, 322 insertions(+), 61 deletions(-)

diff --git a/events-codec/src/farm/list_sets.rs b/events-codec/src/farm/list_sets.rs @@ -0,0 +1,157 @@ +#![forbid(unsafe_code)] + +#[cfg(not(feature = "std"))] +use alloc::{format, string::{String, ToString}, vec, vec::Vec}; + +use radroots_events::list::RadrootsListEntry; +use radroots_events::list_set::RadrootsListSet; +use radroots_events::plot::RadrootsPlot; + +use crate::error::EventEncodeError; +use crate::plot::encode::plot_address; + +const MEMBER_OF_FARMS: &str = "member_of.farms"; + +fn farm_list_set_id(farm_id: &str, suffix: &str) -> Result<String, EventEncodeError> { + let farm_id = farm_id.trim(); + if farm_id.is_empty() { + return Err(EventEncodeError::EmptyRequiredField("farm_id")); + } + if suffix.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("list_set_suffix")); + } + Ok(format!("farm:{farm_id}:{suffix}")) +} + +fn list_entries<I, S>(tag: &str, values: I) -> Result<Vec<RadrootsListEntry>, EventEncodeError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + let mut entries = Vec::new(); + for value in values { + let value = value.as_ref().trim(); + if value.is_empty() { + return Err(EventEncodeError::EmptyRequiredField("entry.values")); + } + entries.push(RadrootsListEntry { + tag: tag.to_string(), + values: vec![value.to_string()], + }); + } + Ok(entries) +} + +pub fn farm_members_list_set<I, S>( + farm_id: &str, + members: I, +) -> Result<RadrootsListSet, EventEncodeError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + Ok(RadrootsListSet { + d_tag: farm_list_set_id(farm_id, "members")?, + content: String::new(), + entries: list_entries("p", members)?, + title: None, + description: None, + image: None, + }) +} + +pub fn farm_owners_list_set<I, S>( + farm_id: &str, + owners: I, +) -> Result<RadrootsListSet, EventEncodeError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + Ok(RadrootsListSet { + d_tag: farm_list_set_id(farm_id, "members.owners")?, + content: String::new(), + entries: list_entries("p", owners)?, + title: None, + description: None, + image: None, + }) +} + +pub fn farm_workers_list_set<I, S>( + farm_id: &str, + workers: I, +) -> Result<RadrootsListSet, EventEncodeError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + Ok(RadrootsListSet { + d_tag: farm_list_set_id(farm_id, "members.workers")?, + content: String::new(), + entries: list_entries("p", workers)?, + title: None, + description: None, + image: None, + }) +} + +pub fn farm_plots_list_set<I, S>( + farm_id: &str, + farm_pubkey: &str, + plot_ids: I, +) -> Result<RadrootsListSet, EventEncodeError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + let mut entries = Vec::new(); + for plot_id in plot_ids { + let plot_id = plot_id.as_ref(); + let address = plot_address(farm_pubkey, plot_id)?; + entries.push(RadrootsListEntry { + tag: "a".to_string(), + values: vec![address], + }); + } + Ok(RadrootsListSet { + d_tag: farm_list_set_id(farm_id, "plots")?, + content: String::new(), + entries, + title: None, + description: None, + image: None, + }) +} + +pub fn farm_plots_list_set_from_plots<'a, I>( + farm_id: &str, + farm_pubkey: &str, + plots: I, +) -> Result<RadrootsListSet, EventEncodeError> +where + I: IntoIterator<Item = &'a RadrootsPlot>, +{ + farm_plots_list_set( + farm_id, + farm_pubkey, + plots.into_iter().map(|plot| plot.d_tag.as_str()), + ) +} + +pub fn member_of_farms_list_set<I, S>( + farm_pubkeys: I, +) -> Result<RadrootsListSet, EventEncodeError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + Ok(RadrootsListSet { + d_tag: MEMBER_OF_FARMS.to_string(), + content: String::new(), + entries: list_entries("p", farm_pubkeys)?, + title: None, + description: None, + image: None, + }) +} diff --git a/events-codec/src/farm/mod.rs b/events-codec/src/farm/mod.rs @@ -1,10 +1,17 @@ pub mod decode; pub mod encode; +pub mod list_sets; #[cfg(test)] mod tests { use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; + use radroots_events::plot::RadrootsPlot; use crate::farm::encode::{farm_build_tags, farm_ref_tags}; + use crate::farm::list_sets::{ + farm_members_list_set, + farm_plots_list_set_from_plots, + member_of_farms_list_set, + }; #[test] fn farm_tags_include_required_fields() { @@ -46,4 +53,43 @@ mod tests { assert!(has_a); assert!(has_p); } + + #[test] + fn farm_list_sets_include_expected_tags() { + let members = farm_members_list_set("farm-1", ["owner_pubkey"]).expect("members list"); + assert_eq!(members.d_tag, "farm:farm-1:members"); + assert_eq!(members.entries.len(), 1); + assert_eq!(members.entries[0].tag, "p"); + + let claims = member_of_farms_list_set(["farm_pubkey"]).expect("claims list"); + assert_eq!(claims.d_tag, "member_of.farms"); + assert_eq!(claims.entries.len(), 1); + assert_eq!(claims.entries[0].tag, "p"); + } + + #[test] + fn farm_plots_list_set_uses_plot_addresses() { + let plots = vec![RadrootsPlot { + d_tag: "plot-1".to_string(), + farm: RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: "farm-1".to_string(), + }, + name: "Plot 1".to_string(), + about: None, + location: None, + geometry: None, + tags: None, + }]; + + let plots_list = farm_plots_list_set_from_plots("farm-1", "farm_pubkey", &plots) + .expect("plots list"); + assert_eq!(plots_list.d_tag, "farm:farm-1:plots"); + assert_eq!(plots_list.entries.len(), 1); + assert_eq!(plots_list.entries[0].tag, "a"); + assert_eq!( + plots_list.entries[0].values[0], + "30350:farm_pubkey:plot-1" + ); + } } diff --git a/events-codec/src/plot/encode.rs b/events-codec/src/plot/encode.rs @@ -3,14 +3,11 @@ use alloc::{string::{String, ToString}, vec::Vec}; use radroots_events::{ farm::RadrootsFarmRef, - kinds::KIND_FARM, + kinds::{KIND_FARM, KIND_PLOT}, plot::RadrootsPlot, tags::TAG_D, }; -#[cfg(feature = "serde_json")] -use radroots_events::kinds::KIND_PLOT; - use crate::error::EventEncodeError; #[cfg(feature = "serde_json")] @@ -38,6 +35,24 @@ fn farm_address(farm: &RadrootsFarmRef) -> String { value } +pub fn plot_address(author_pubkey: &str, plot_id: &str) -> Result<String, EventEncodeError> { + let author_pubkey = author_pubkey.trim(); + if author_pubkey.is_empty() { + return Err(EventEncodeError::EmptyRequiredField("plot.author_pubkey")); + } + let plot_id = plot_id.trim(); + if plot_id.is_empty() { + return Err(EventEncodeError::EmptyRequiredField("plot.d_tag")); + } + let mut value = String::new(); + value.push_str(&KIND_PLOT.to_string()); + value.push(':'); + value.push_str(author_pubkey); + value.push(':'); + value.push_str(plot_id); + Ok(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")); diff --git a/events-codec/src/profile/decode.rs b/events-codec/src/profile/decode.rs @@ -6,12 +6,12 @@ use alloc::{string::{String, ToString}, vec::Vec}; use radroots_events::{ RadrootsNostrEvent, profile::{ - RadrootsActorType, + RadrootsProfileType, RadrootsProfile, RadrootsProfileEventIndex, RadrootsProfileEventMetadata, - RADROOTS_ACTOR_TAG_KEY, - radroots_actor_type_from_tag_value, + RADROOTS_PROFILE_TYPE_TAG_KEY, + radroots_profile_type_from_tag_value, }, kinds::KIND_PROFILE, }; @@ -33,11 +33,11 @@ fn parse_bot(value: &Value) -> Option<String> { } } -fn profile_actor_from_tags(tags: &[Vec<String>]) -> Option<RadrootsActorType> { +fn profile_type_from_tags(tags: &[Vec<String>]) -> Option<RadrootsProfileType> { tags.iter() - .filter(|tag| tag.get(0).map(|v| v.as_str()) == Some(RADROOTS_ACTOR_TAG_KEY)) + .filter(|tag| tag.get(0).map(|v| v.as_str()) == Some(RADROOTS_PROFILE_TYPE_TAG_KEY)) .filter_map(|tag| tag.get(1)) - .find_map(|value| radroots_actor_type_from_tag_value(value)) + .find_map(|value| radroots_profile_type_from_tag_value(value)) } pub fn profile_from_content(content: &str) -> Result<RadrootsProfile, EventParseError> { @@ -80,13 +80,13 @@ pub fn metadata_from_event( }); } let profile = profile_from_content(&content)?; - let actor = profile_actor_from_tags(&tags); + let profile_type = profile_type_from_tags(&tags); Ok(RadrootsProfileEventMetadata { id, author, published_at, kind, - actor, + profile_type, profile, }) } diff --git a/events-codec/src/profile/encode.rs b/events-codec/src/profile/encode.rs @@ -1,9 +1,9 @@ use crate::profile::error::ProfileEncodeError; use radroots_events::profile::{ - RadrootsActorType, + RadrootsProfileType, RadrootsProfile, - RADROOTS_ACTOR_TAG_KEY, - radroots_actor_tag_value, + RADROOTS_PROFILE_TYPE_TAG_KEY, + radroots_profile_type_tag_value, }; use radroots_events::kinds::KIND_PROFILE; @@ -23,15 +23,19 @@ fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) { tags.push(tag); } -pub fn profile_actor_tags(actor: RadrootsActorType) -> Vec<Vec<String>> { +pub fn profile_type_tags(profile_type: RadrootsProfileType) -> Vec<Vec<String>> { let mut tags = Vec::with_capacity(1); - push_tag(&mut tags, RADROOTS_ACTOR_TAG_KEY, radroots_actor_tag_value(actor)); + push_tag( + &mut tags, + RADROOTS_PROFILE_TYPE_TAG_KEY, + radroots_profile_type_tag_value(profile_type), + ); tags } -pub fn profile_build_tags(actor: Option<RadrootsActorType>) -> Vec<Vec<String>> { - match actor { - Some(value) => profile_actor_tags(value), +pub fn profile_build_tags(profile_type: Option<RadrootsProfileType>) -> Vec<Vec<String>> { + match profile_type { + Some(value) => profile_type_tags(value), None => Vec::new(), } } @@ -72,17 +76,17 @@ 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) + to_wire_parts_with_profile_type(p, None) } #[cfg(feature = "serde_json")] -pub fn to_wire_parts_with_actor( +pub fn to_wire_parts_with_profile_type( p: &RadrootsProfile, - actor: Option<RadrootsActorType>, + profile_type: Option<RadrootsProfileType>, ) -> 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); + let tags = profile_build_tags(profile_type); Ok(WireEventParts { kind: KIND_PROFILE, content, diff --git a/events-codec/tests/profile.rs b/events-codec/tests/profile.rs @@ -2,7 +2,7 @@ use radroots_events::{ kinds::KIND_POST, - profile::{RadrootsActorType, RADROOTS_ACTOR_TAG_FARM, RADROOTS_ACTOR_TAG_KEY}, + profile::{RadrootsProfileType, RADROOTS_PROFILE_TYPE_TAG_FARM, RADROOTS_PROFILE_TYPE_TAG_KEY}, }; use radroots_events_codec::error::EventParseError; use radroots_events_codec::profile::decode::profile_from_content; @@ -60,16 +60,19 @@ fn profile_metadata_rejects_wrong_kind() { } #[test] -fn profile_metadata_reads_actor_tag() { +fn profile_metadata_reads_profile_type_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()]], + vec![vec![ + RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), + RADROOTS_PROFILE_TYPE_TAG_FARM.to_string(), + ]], ) .expect("metadata"); - assert_eq!(metadata.actor, Some(RadrootsActorType::Farm)); + assert_eq!(metadata.profile_type, Some(RadrootsProfileType::Farm)); } diff --git a/events-codec/tests/profile_encode.rs b/events-codec/tests/profile_encode.rs @@ -2,9 +2,18 @@ use radroots_events::{ kinds::KIND_PROFILE, - profile::{RadrootsActorType, RadrootsProfile, RADROOTS_ACTOR_TAG_FARM, RADROOTS_ACTOR_TAG_KEY}, + profile::{ + RadrootsProfile, + RadrootsProfileType, + RADROOTS_PROFILE_TYPE_TAG_FARM, + RADROOTS_PROFILE_TYPE_TAG_KEY, + }, +}; +use radroots_events_codec::profile::encode::{ + to_metadata, + to_wire_parts, + to_wire_parts_with_profile_type, }; -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; @@ -53,7 +62,7 @@ fn profile_to_wire_parts_writes_json_content() { } #[test] -fn profile_to_wire_parts_with_actor_sets_tag() { +fn profile_to_wire_parts_with_profile_type_sets_tag() { let profile = RadrootsProfile { name: "farm".to_string(), display_name: None, @@ -67,10 +76,10 @@ fn profile_to_wire_parts_with_actor_sets_tag() { bot: None, }; - let parts = to_wire_parts_with_actor(&profile, Some(RadrootsActorType::Farm)).unwrap(); + let parts = to_wire_parts_with_profile_type(&profile, Some(RadrootsProfileType::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))); + .any(|tag| tag.get(0).map(|v| v.as_str()) == Some(RADROOTS_PROFILE_TYPE_TAG_KEY) + && tag.get(1).map(|v| v.as_str()) == Some(RADROOTS_PROFILE_TYPE_TAG_FARM))); } diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts @@ -8,8 +8,6 @@ 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, }; @@ -150,7 +148,9 @@ 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, actor?: RadrootsActorType | null, profile: RadrootsProfile, }; +export type RadrootsProfileEventMetadata = { id: string, author: string, published_at: number, kind: number, profile_type?: RadrootsProfileType | null, profile: RadrootsProfile, }; + +export type RadrootsProfileType = "individual" | "farm"; export type RadrootsReaction = { root: RadrootsNostrEventRef, content: string, }; diff --git a/events/src/profile.rs b/events/src/profile.rs @@ -5,31 +5,31 @@ 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"; +pub const RADROOTS_PROFILE_TYPE_TAG_KEY: &str = "t"; +pub const RADROOTS_PROFILE_TYPE_TAG_INDIVIDUAL: &str = "radroots:type:individual"; +pub const RADROOTS_PROFILE_TYPE_TAG_FARM: &str = "radroots:type: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, +pub enum RadrootsProfileType { + Individual, 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_profile_type_tag_value(profile_type: RadrootsProfileType) -> &'static str { + match profile_type { + RadrootsProfileType::Individual => RADROOTS_PROFILE_TYPE_TAG_INDIVIDUAL, + RadrootsProfileType::Farm => RADROOTS_PROFILE_TYPE_TAG_FARM, } } -pub fn radroots_actor_type_from_tag_value(value: &str) -> Option<RadrootsActorType> { +pub fn radroots_profile_type_from_tag_value(value: &str) -> Option<RadrootsProfileType> { match value { - RADROOTS_ACTOR_TAG_PERSON => Some(RadrootsActorType::Person), - RADROOTS_ACTOR_TAG_FARM => Some(RadrootsActorType::Farm), + RADROOTS_PROFILE_TYPE_TAG_INDIVIDUAL => Some(RadrootsProfileType::Individual), + RADROOTS_PROFILE_TYPE_TAG_FARM => Some(RadrootsProfileType::Farm), _ => None, } } @@ -52,8 +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>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsProfileType | null"))] + pub profile_type: Option<RadrootsProfileType>, pub profile: RadrootsProfile, } diff --git a/nostr/src/event_adapters.rs b/nostr/src/event_adapters.rs @@ -4,8 +4,8 @@ use radroots_events::post::{RadrootsPost, RadrootsPostEventMetadata}; use radroots_events::profile::{ RadrootsProfile, RadrootsProfileEventMetadata, - RADROOTS_ACTOR_TAG_KEY, - radroots_actor_type_from_tag_value, + RADROOTS_PROFILE_TYPE_TAG_KEY, + radroots_profile_type_from_tag_value, }; #[cfg(feature = "events")] @@ -29,15 +29,17 @@ 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 + let profile_type = 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) { + if values.get(0).map(|v| v.as_str()) != Some(RADROOTS_PROFILE_TYPE_TAG_KEY) { return None; } - values.get(1).and_then(|value| radroots_actor_type_from_tag_value(value)) + values + .get(1) + .and_then(|value| radroots_profile_type_from_tag_value(value)) }) .next(); @@ -47,7 +49,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_type, profile: p, }); } @@ -70,7 +72,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_type, profile: p, }); } diff --git a/nostr/src/events/jobs.rs b/nostr/src/events/jobs.rs @@ -12,7 +12,8 @@ pub fn radroots_nostr_build_event_job_result( ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> { let builder = RadrootsNostrEventBuilder::job_result(job_request.clone(), payload, millisats, bolt11)? - .tags(tags.unwrap_or_default()); + .tags(tags.unwrap_or_default()) + .allow_self_tagging(); Ok(builder) } @@ -27,6 +28,8 @@ pub fn radroots_nostr_build_event_job_feedback( .unwrap_or(DataVendingMachineStatus::Error); let feedback_data = JobFeedbackData::new(&job_request.clone(), status) .extra_info(extra_info.unwrap_or_default()); - let builder = RadrootsNostrEventBuilder::job_feedback(feedback_data).tags(tags.unwrap_or_default()); + let builder = RadrootsNostrEventBuilder::job_feedback(feedback_data) + .tags(tags.unwrap_or_default()) + .allow_self_tagging(); Ok(builder) } diff --git a/nostr/src/events/mod.rs b/nostr/src/events/mod.rs @@ -32,6 +32,28 @@ pub fn radroots_nostr_build_event( } let builder = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(kind_u32 as u16), content.into()) - .tags(tags); + .tags(tags) + .allow_self_tagging(); Ok(builder) } + +#[cfg(test)] +mod tests { + use super::radroots_nostr_build_event; + use crate::types::{RadrootsNostrPublicKey, RadrootsNostrTagKind}; + + #[test] + fn build_event_preserves_self_p_tag() { + let pubkey_hex = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; + let pubkey = RadrootsNostrPublicKey::from_hex(pubkey_hex).expect("pubkey"); + let tags = vec![vec!["p".to_string(), pubkey_hex.to_string()]]; + + let builder = radroots_nostr_build_event(1, "test", tags).expect("builder"); + let event = builder.build(pubkey); + + let has_self_tag = event.tags.iter().any(|tag| { + tag.kind() == RadrootsNostrTagKind::p() && tag.content() == Some(pubkey_hex) + }); + assert!(has_self_tag); + } +}