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