commit fd923f4c367b6a12dee01dea71cb04ed21759f49
parent 41e20a14d30416d13d9864ed2cc2f415d9a8e6ea
Author: triesap <tyson@radroots.org>
Date: Fri, 12 Jun 2026 02:37:18 -0700
events: add social event primitives
- add public social kind constants, aliases, classifiers, and tests
- add social tag constants for comments, media, calendars, listings, and reports
- introduce shared social target, media, calendar, report, and farm anchor primitives
- verify radroots_events with fmt, check, tests, and no-default-features check
Diffstat:
4 files changed, 436 insertions(+), 0 deletions(-)
diff --git a/crates/events/src/kinds.rs b/crates/events/src/kinds.rs
@@ -1,16 +1,20 @@
pub const KIND_PROFILE: u32 = 0;
pub const KIND_POST: u32 = 1;
pub const KIND_FOLLOW: u32 = 3;
+pub const KIND_REPOST: u32 = 6;
pub const KIND_REACTION: u32 = 7;
pub const KIND_SEAL: u32 = 13;
pub const KIND_MESSAGE: u32 = 14;
pub const KIND_MESSAGE_FILE: u32 = 15;
+pub const KIND_GENERIC_REPOST: u32 = 16;
pub const KIND_APP_CUSTOM_DATA: u32 = 78;
pub const KIND_FARM_CRDT_CHANGE: u32 = KIND_APP_CUSTOM_DATA;
pub const KIND_GIFT_WRAP: u32 = 1059;
pub const KIND_FILE_METADATA: u32 = 1063;
pub const KIND_FARM_FILE_METADATA: u32 = KIND_FILE_METADATA;
+pub const KIND_PUBLIC_FILE_METADATA: u32 = KIND_FILE_METADATA;
pub const KIND_COMMENT: u32 = 1111;
+pub const KIND_REPORT: u32 = 1984;
pub const KIND_GROUP_PUT_USER: u32 = 9000;
pub const KIND_GROUP_REMOVE_USER: u32 = 9001;
pub const KIND_GROUP_EDIT_METADATA: u32 = 9002;
@@ -51,7 +55,12 @@ pub const KIND_LIST_SET_INTEREST: u32 = 30015;
pub const KIND_LIST_SET_EMOJI: u32 = 30030;
pub const KIND_LIST_SET_RELEASE_ARTIFACT: u32 = 30063;
pub const KIND_LIST_SET_APP_CURATION: u32 = 30267;
+pub const KIND_ARTICLE: u32 = 30023;
+pub const KIND_CALENDAR_DATE_EVENT: u32 = 31922;
+pub const KIND_CALENDAR_TIME_EVENT: u32 = 31923;
pub const KIND_LIST_SET_CALENDAR: u32 = 31924;
+pub const KIND_CALENDAR: u32 = KIND_LIST_SET_CALENDAR;
+pub const KIND_CALENDAR_EVENT_RSVP: u32 = 31925;
pub const KIND_LIST_SET_STARTER_PACK: u32 = 39089;
pub const KIND_LIST_SET_MEDIA_STARTER_PACK: u32 = 39092;
pub const KIND_FARM: u32 = 30340;
@@ -191,12 +200,124 @@ pub const KIND_JOB_RESULT_MIN: u32 = 6000;
pub const KIND_JOB_RESULT_MAX: u32 = 6999;
pub const KIND_JOB_FEEDBACK: u32 = 7000;
+pub const PUBLIC_SOCIAL_KINDS: [u32; 14] = [
+ KIND_POST,
+ KIND_REPOST,
+ KIND_REACTION,
+ KIND_GENERIC_REPOST,
+ KIND_PUBLIC_FILE_METADATA,
+ KIND_COMMENT,
+ KIND_REPORT,
+ KIND_LIST_READ_WRITE_RELAYS,
+ KIND_ARTICLE,
+ KIND_LISTING_DRAFT,
+ KIND_CALENDAR_DATE_EVENT,
+ KIND_CALENDAR_TIME_EVENT,
+ KIND_CALENDAR,
+ KIND_CALENDAR_EVENT_RSVP,
+];
+
+pub const UNAMBIGUOUS_PUBLIC_SOCIAL_KINDS: [u32; 13] = [
+ KIND_POST,
+ KIND_REPOST,
+ KIND_REACTION,
+ KIND_GENERIC_REPOST,
+ KIND_COMMENT,
+ KIND_REPORT,
+ KIND_LIST_READ_WRITE_RELAYS,
+ KIND_ARTICLE,
+ KIND_LISTING_DRAFT,
+ KIND_CALENDAR_DATE_EVENT,
+ KIND_CALENDAR_TIME_EVENT,
+ KIND_CALENDAR,
+ KIND_CALENDAR_EVENT_RSVP,
+];
+
+pub const MVP_SOCIAL_KINDS: [u32; 5] = [
+ KIND_POST,
+ KIND_PUBLIC_FILE_METADATA,
+ KIND_ARTICLE,
+ KIND_CALENDAR_DATE_EVENT,
+ KIND_CALENDAR_TIME_EVENT,
+];
+
+pub const PRODUCTION_SOCIAL_KINDS: [u32; 7] = [
+ KIND_REPOST,
+ KIND_GENERIC_REPOST,
+ KIND_REPORT,
+ KIND_LIST_READ_WRITE_RELAYS,
+ KIND_LISTING_DRAFT,
+ KIND_CALENDAR,
+ KIND_CALENDAR_EVENT_RSVP,
+];
+
#[inline]
pub const fn is_listing_kind(kind: u32) -> bool {
matches!(kind, KIND_LISTING | KIND_LISTING_DRAFT)
}
#[inline]
+pub const fn is_public_file_metadata_kind(kind: u32) -> bool {
+ kind == KIND_PUBLIC_FILE_METADATA
+}
+
+#[inline]
+pub const fn is_ambiguous_public_social_kind(kind: u32) -> bool {
+ kind == KIND_PUBLIC_FILE_METADATA
+}
+
+#[inline]
+pub const fn is_unambiguous_public_social_kind(kind: u32) -> bool {
+ matches!(
+ kind,
+ KIND_POST
+ | KIND_REPOST
+ | KIND_REACTION
+ | KIND_GENERIC_REPOST
+ | KIND_COMMENT
+ | KIND_REPORT
+ | KIND_LIST_READ_WRITE_RELAYS
+ | KIND_ARTICLE
+ | KIND_LISTING_DRAFT
+ | KIND_CALENDAR_DATE_EVENT
+ | KIND_CALENDAR_TIME_EVENT
+ | KIND_CALENDAR
+ | KIND_CALENDAR_EVENT_RSVP
+ )
+}
+
+#[inline]
+pub const fn is_public_social_kind(kind: u32) -> bool {
+ is_unambiguous_public_social_kind(kind) || is_ambiguous_public_social_kind(kind)
+}
+
+#[inline]
+pub const fn is_mvp_social_kind(kind: u32) -> bool {
+ matches!(
+ kind,
+ KIND_POST
+ | KIND_PUBLIC_FILE_METADATA
+ | KIND_ARTICLE
+ | KIND_CALENDAR_DATE_EVENT
+ | KIND_CALENDAR_TIME_EVENT
+ )
+}
+
+#[inline]
+pub const fn is_production_social_kind(kind: u32) -> bool {
+ matches!(
+ kind,
+ KIND_REPOST
+ | KIND_GENERIC_REPOST
+ | KIND_REPORT
+ | KIND_LIST_READ_WRITE_RELAYS
+ | KIND_LISTING_DRAFT
+ | KIND_CALENDAR
+ | KIND_CALENDAR_EVENT_RSVP
+ )
+}
+
+#[inline]
pub const fn is_trade_service_request_kind(kind: u32) -> bool {
matches!(
kind,
@@ -466,12 +587,60 @@ mod tests {
assert_eq!(KIND_FARM_CRDT_CHANGE, KIND_APP_CUSTOM_DATA);
assert_eq!(KIND_FILE_METADATA, 1063);
assert_eq!(KIND_FARM_FILE_METADATA, KIND_FILE_METADATA);
+ assert_eq!(KIND_PUBLIC_FILE_METADATA, KIND_FILE_METADATA);
assert_eq!(KIND_FARM_WORKSPACE_MANIFEST, KIND_APP_DATA);
assert_eq!(KIND_RELAY_AUTH, 22242);
assert_eq!(KIND_HTTP_AUTH, 27235);
}
#[test]
+ fn exposes_social_event_kind_constants() {
+ assert_eq!(KIND_REPOST, 6);
+ assert_eq!(KIND_GENERIC_REPOST, 16);
+ assert_eq!(KIND_REPORT, 1984);
+ assert_eq!(KIND_ARTICLE, 30023);
+ assert_eq!(KIND_CALENDAR_DATE_EVENT, 31922);
+ assert_eq!(KIND_CALENDAR_TIME_EVENT, 31923);
+ assert_eq!(KIND_CALENDAR, KIND_LIST_SET_CALENDAR);
+ assert_eq!(KIND_CALENDAR_EVENT_RSVP, 31925);
+ }
+
+ #[test]
+ fn classifies_public_social_kinds() {
+ assert_eq!(PUBLIC_SOCIAL_KINDS.len(), 14);
+ assert_eq!(UNAMBIGUOUS_PUBLIC_SOCIAL_KINDS.len(), 13);
+ assert_eq!(MVP_SOCIAL_KINDS.len(), 5);
+ assert_eq!(PRODUCTION_SOCIAL_KINDS.len(), 7);
+
+ assert!(is_public_social_kind(KIND_POST));
+ assert!(is_public_social_kind(KIND_PUBLIC_FILE_METADATA));
+ assert!(is_public_social_kind(KIND_COMMENT));
+ assert!(is_public_social_kind(KIND_REACTION));
+ assert!(is_public_social_kind(KIND_ARTICLE));
+ assert!(is_public_social_kind(KIND_CALENDAR_DATE_EVENT));
+ assert!(is_public_social_kind(KIND_CALENDAR_TIME_EVENT));
+ assert!(is_public_social_kind(KIND_REPOST));
+ assert!(is_public_social_kind(KIND_GENERIC_REPOST));
+ assert!(is_public_social_kind(KIND_REPORT));
+ assert!(is_public_social_kind(KIND_CALENDAR));
+ assert!(is_public_social_kind(KIND_CALENDAR_EVENT_RSVP));
+ assert!(is_public_social_kind(KIND_LISTING_DRAFT));
+ assert!(is_public_social_kind(KIND_LIST_READ_WRITE_RELAYS));
+ assert!(!is_public_social_kind(KIND_FARM_CRDT_CHANGE));
+ assert!(!is_public_social_kind(KIND_FARM_WORKSPACE_MANIFEST));
+
+ assert!(is_mvp_social_kind(KIND_ARTICLE));
+ assert!(!is_mvp_social_kind(KIND_REPORT));
+ assert!(is_production_social_kind(KIND_REPORT));
+ assert!(!is_production_social_kind(KIND_ARTICLE));
+ assert!(is_ambiguous_public_social_kind(KIND_PUBLIC_FILE_METADATA));
+ assert!(!is_unambiguous_public_social_kind(
+ KIND_PUBLIC_FILE_METADATA
+ ));
+ assert!(is_unambiguous_public_social_kind(KIND_ARTICLE));
+ }
+
+ #[test]
fn exposes_nip29_group_kind_constants() {
assert_eq!(KIND_GROUP_PUT_USER, 9000);
assert_eq!(KIND_GROUP_REMOVE_USER, 9001);
diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs
@@ -39,6 +39,7 @@ pub mod relay_document;
pub mod resource_area;
pub mod resource_cap;
pub mod seal;
+pub mod social;
pub mod tags;
pub mod trade;
diff --git a/crates/events/src/social.rs b/crates/events/src/social.rs
@@ -0,0 +1,192 @@
+use crate::farm::RadrootsFarmRef;
+
+#[cfg(not(feature = "std"))]
+use alloc::{string::String, vec::Vec};
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "kind"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RadrootsSocialTarget {
+ Event {
+ id: String,
+ author: Option<String>,
+ event_kind: Option<u32>,
+ relays: Option<Vec<String>>,
+ },
+ Address {
+ address: String,
+ author: Option<String>,
+ event_kind: Option<u32>,
+ relays: Option<Vec<String>>,
+ },
+ External {
+ id: String,
+ external_kind: String,
+ hint: Option<String>,
+ },
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default)]
+pub struct RadrootsSocialFarmAnchor {
+ pub farm: RadrootsFarmRef,
+ pub relays: Option<Vec<String>>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsSocialLocation {
+ pub name: Option<String>,
+ pub geohash: Option<String>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsSocialMediaDimensions {
+ pub width: u32,
+ pub height: u32,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsSocialMediaThumbnail {
+ pub url: String,
+ pub dimensions: Option<RadrootsSocialMediaDimensions>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsSocialMediaMetadata {
+ pub url: Option<String>,
+ pub mime_type: Option<String>,
+ pub sha256: Option<String>,
+ pub original_sha256: Option<String>,
+ pub size: Option<u64>,
+ pub dimensions: Option<RadrootsSocialMediaDimensions>,
+ pub blurhash: Option<String>,
+ pub thumbnails: Option<Vec<RadrootsSocialMediaThumbnail>>,
+ pub image: Option<String>,
+ pub summary: Option<String>,
+ pub alt: Option<String>,
+ pub fallback: Option<String>,
+ pub magnet: Option<String>,
+ pub content_hashes: Option<Vec<String>>,
+ pub services: Option<Vec<String>>,
+ pub imeta: Option<Vec<Vec<String>>>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsCalendarParticipant {
+ pub pubkey: String,
+ pub relay: Option<String>,
+ pub role: Option<String>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsCalendarDateValue {
+ pub value: String,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RadrootsCalendarEventRsvpStatus {
+ Accepted,
+ Declined,
+ Tentative,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RadrootsCalendarEventFreeBusy {
+ Free,
+ Busy,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RadrootsReportType {
+ Nudity,
+ Malware,
+ Profanity,
+ Illegal,
+ Spam,
+ Impersonation,
+ Other,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RadrootsReportFileTarget {
+ pub sha256: Option<String>,
+ pub url: Option<String>,
+ pub magnet: Option<String>,
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsReportTarget {
+ pub reported_pubkey: String,
+ pub event: Option<RadrootsSocialTarget>,
+ pub file: Option<RadrootsReportFileTarget>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn supports_nip22_target_shapes() {
+ let event = RadrootsSocialTarget::Event {
+ id: "a".repeat(64),
+ author: Some("b".repeat(64)),
+ event_kind: Some(30023),
+ relays: Some(vec!["wss://relay.example".to_string()]),
+ };
+ let address = RadrootsSocialTarget::Address {
+ address: "30023:pubkey:d-tag".to_string(),
+ author: None,
+ event_kind: Some(30023),
+ relays: None,
+ };
+ let external = RadrootsSocialTarget::External {
+ id: "https://example.test/object".to_string(),
+ external_kind: "web".to_string(),
+ hint: None,
+ };
+
+ assert!(matches!(event, RadrootsSocialTarget::Event { .. }));
+ assert!(matches!(address, RadrootsSocialTarget::Address { .. }));
+ assert!(matches!(external, RadrootsSocialTarget::External { .. }));
+ }
+
+ #[test]
+ fn defaults_media_and_farm_anchor_primitives() {
+ let media = RadrootsSocialMediaMetadata::default();
+ assert!(media.url.is_none());
+ assert!(media.content_hashes.is_none());
+ assert!(media.services.is_none());
+
+ let anchor = RadrootsSocialFarmAnchor::default();
+ assert!(anchor.farm.pubkey.is_empty());
+ assert!(anchor.farm.d_tag.is_empty());
+ assert!(anchor.relays.is_none());
+ }
+
+ #[test]
+ fn exposes_calendar_and_report_enums() {
+ assert_eq!(
+ RadrootsCalendarEventRsvpStatus::Accepted,
+ RadrootsCalendarEventRsvpStatus::Accepted
+ );
+ assert_eq!(
+ RadrootsCalendarEventFreeBusy::Busy,
+ RadrootsCalendarEventFreeBusy::Busy
+ );
+ assert_eq!(RadrootsReportType::Spam, RadrootsReportType::Spam);
+ }
+}
diff --git a/crates/events/src/tags.rs b/crates/events/src/tags.rs
@@ -1,11 +1,20 @@
pub const TAG_A: &str = "a";
+pub const TAG_A_ROOT: &str = "A";
pub const TAG_E: &str = "e";
+pub const TAG_E_ROOT_NIP22: &str = "E";
pub const TAG_E_ROOT: &str = "e_root";
pub const TAG_E_PREV: &str = "e_prev";
+pub const TAG_I: &str = "i";
+pub const TAG_I_ROOT: &str = "I";
+pub const TAG_K: &str = "k";
+pub const TAG_K_ROOT: &str = "K";
pub const TAG_D: &str = "d";
+pub const TAG_D_DAY: &str = "D";
pub const TAG_G: &str = "g";
pub const TAG_H: &str = "h";
pub const TAG_P: &str = "p";
+pub const TAG_P_ROOT: &str = "P";
+pub const TAG_Q: &str = "q";
pub const TAG_R: &str = "r";
pub const TAG_T: &str = "t";
pub const TAG_U: &str = "u";
@@ -16,8 +25,34 @@ pub const TAG_MIME: &str = "m";
pub const TAG_PAYLOAD: &str = "payload";
pub const TAG_SHA256: &str = "x";
pub const TAG_ORIGINAL_SHA256: &str = "ox";
+pub const TAG_SIZE: &str = "size";
+pub const TAG_DIMENSIONS: &str = "dim";
+pub const TAG_BLURHASH: &str = "blurhash";
+pub const TAG_THUMBNAIL: &str = "thumb";
+pub const TAG_IMAGE: &str = "image";
+pub const TAG_SUMMARY: &str = "summary";
+pub const TAG_ALT: &str = "alt";
+pub const TAG_FALLBACK: &str = "fallback";
+pub const TAG_MAGNET: &str = "magnet";
+pub const TAG_SERVICE: &str = "service";
pub const TAG_RELAY: &str = "relay";
pub const TAG_CHALLENGE: &str = "challenge";
+pub const TAG_TITLE: &str = "title";
+pub const TAG_PUBLISHED_AT: &str = "published_at";
+pub const TAG_START: &str = "start";
+pub const TAG_END: &str = "end";
+pub const TAG_START_TZID: &str = "start_tzid";
+pub const TAG_END_TZID: &str = "end_tzid";
+pub const TAG_LOCATION: &str = "location";
+pub const TAG_STATUS: &str = "status";
+pub const TAG_FREE_BUSY: &str = "fb";
+pub const TAG_DESCRIPTION: &str = "description";
+pub const TAG_AMOUNT: &str = "amount";
+pub const TAG_PRICE: &str = "price";
+pub const TAG_CURRENCY: &str = "currency";
+pub const TAG_SERVER: &str = "server";
+pub const TAG_SUBJECT: &str = "subject";
+pub const TAG_IMETA: &str = "imeta";
#[cfg(test)]
mod tests {
@@ -26,11 +61,20 @@ mod tests {
#[test]
fn exposes_shared_nostr_tag_keys() {
assert_eq!(TAG_A, "a");
+ assert_eq!(TAG_A_ROOT, "A");
assert_eq!(TAG_D, "d");
+ assert_eq!(TAG_D_DAY, "D");
assert_eq!(TAG_E, "e");
+ assert_eq!(TAG_E_ROOT_NIP22, "E");
assert_eq!(TAG_G, "g");
assert_eq!(TAG_H, "h");
+ assert_eq!(TAG_I, "i");
+ assert_eq!(TAG_I_ROOT, "I");
+ assert_eq!(TAG_K, "k");
+ assert_eq!(TAG_K_ROOT, "K");
assert_eq!(TAG_P, "p");
+ assert_eq!(TAG_P_ROOT, "P");
+ assert_eq!(TAG_Q, "q");
assert_eq!(TAG_R, "r");
assert_eq!(TAG_T, "t");
assert_eq!(TAG_U, "u");
@@ -45,7 +89,37 @@ mod tests {
assert_eq!(TAG_PAYLOAD, "payload");
assert_eq!(TAG_SHA256, "x");
assert_eq!(TAG_ORIGINAL_SHA256, "ox");
+ assert_eq!(TAG_SIZE, "size");
+ assert_eq!(TAG_DIMENSIONS, "dim");
+ assert_eq!(TAG_BLURHASH, "blurhash");
+ assert_eq!(TAG_THUMBNAIL, "thumb");
+ assert_eq!(TAG_IMAGE, "image");
+ assert_eq!(TAG_SUMMARY, "summary");
+ assert_eq!(TAG_ALT, "alt");
+ assert_eq!(TAG_FALLBACK, "fallback");
+ assert_eq!(TAG_MAGNET, "magnet");
+ assert_eq!(TAG_SERVICE, "service");
assert_eq!(TAG_RELAY, "relay");
assert_eq!(TAG_CHALLENGE, "challenge");
}
+
+ #[test]
+ fn exposes_social_event_tag_keys() {
+ assert_eq!(TAG_TITLE, "title");
+ assert_eq!(TAG_PUBLISHED_AT, "published_at");
+ assert_eq!(TAG_START, "start");
+ assert_eq!(TAG_END, "end");
+ assert_eq!(TAG_START_TZID, "start_tzid");
+ assert_eq!(TAG_END_TZID, "end_tzid");
+ assert_eq!(TAG_LOCATION, "location");
+ assert_eq!(TAG_STATUS, "status");
+ assert_eq!(TAG_FREE_BUSY, "fb");
+ assert_eq!(TAG_DESCRIPTION, "description");
+ assert_eq!(TAG_AMOUNT, "amount");
+ assert_eq!(TAG_PRICE, "price");
+ assert_eq!(TAG_CURRENCY, "currency");
+ assert_eq!(TAG_SERVER, "server");
+ assert_eq!(TAG_SUBJECT, "subject");
+ assert_eq!(TAG_IMETA, "imeta");
+ }
}