lib

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

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:
Mcrates/events/src/kinds.rs | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events/src/lib.rs | 1+
Acrates/events/src/social.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events/src/tags.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"); + } }