lib

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

commit 5196fbf56edc42a34c35699cdf78f44b84b1263c
parent 23e1e673900138b85dddd86b21a1db7dfce40eba
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Dec 2025 15:41:46 +0000

codec: add NIP-51 list and list_set tag support


- Add list/list_set codecs with encode/decode and round-trip tests
- Expose list_tags and list_set_tags via wasm-bindgen exports
- Implement tag builder support for RadrootsList and RadrootsListSet
- Add list kinds/constants and TS bindings for list and list_set types

Diffstat:
Mevents-codec-wasm/src/lib.rs | 26++++++++++++++++++++++++++
Mevents-codec/src/lib.rs | 2++
Aevents-codec/src/list/decode.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/list/encode.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/list/mod.rs | 37+++++++++++++++++++++++++++++++++++++
Aevents-codec/src/list_set/decode.rs | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/list_set/encode.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/list_set/mod.rs | 40++++++++++++++++++++++++++++++++++++++++
Mevents-codec/src/tag_builders.rs | 20++++++++++++++++++++
Mevents/bindings/ts/src/types.ts | 14++++++++++++++
Mevents/bindings/ts/src/typeshare-types.ts | 31+++++++++++++++++++++++++++++++
Mevents/src/kinds.rs | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mevents/src/lib.rs | 2++
Aevents/src/list.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Aevents/src/list_set.rs | 43+++++++++++++++++++++++++++++++++++++++++++
Mevents/src/typeshare_kinds.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 791 insertions(+), 0 deletions(-)

diff --git a/events-codec-wasm/src/lib.rs b/events-codec-wasm/src/lib.rs @@ -7,6 +7,8 @@ use radroots_events::job_feedback::RadrootsJobFeedback; use radroots_events::job_request::RadrootsJobRequest; use radroots_events::job_result::RadrootsJobResult; use radroots_events::listing::RadrootsListing; +use radroots_events::list::RadrootsList; +use radroots_events::list_set::RadrootsListSet; use radroots_events::message::RadrootsMessage; use radroots_events::reaction::RadrootsReaction; use radroots_events_codec::comment::encode::comment_build_tags; @@ -14,6 +16,8 @@ use radroots_events_codec::follow::encode::follow_build_tags; use radroots_events_codec::job::feedback::encode::job_feedback_build_tags; use radroots_events_codec::job::request::encode::job_request_build_tags; use radroots_events_codec::job::result::encode::job_result_build_tags; +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::reaction::encode::reaction_build_tags; use radroots_events_codec::listing::tags::{ @@ -58,6 +62,14 @@ fn parse_message(message_json: &str) -> Result<RadrootsMessage, JsValue> { serde_json::from_str(message_json).map_err(err_js) } +fn parse_list(list_json: &str) -> Result<RadrootsList, JsValue> { + serde_json::from_str(list_json).map_err(err_js) +} + +fn parse_list_set(list_json: &str) -> Result<RadrootsListSet, JsValue> { + serde_json::from_str(list_json).map_err(err_js) +} + fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, JsValue> { serde_json::to_string(&tags).map_err(err_js) } @@ -90,6 +102,20 @@ pub fn follow_tags(follow_json: &str) -> Result<String, JsValue> { 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)?; + let tags = list_build_tags(&list).map_err(err_js)?; + tags_to_json(tags) +} + +#[wasm_bindgen(js_name = list_set_tags)] +pub fn list_set_tags(list_json: &str) -> Result<String, JsValue> { + let list = parse_list_set(list_json)?; + let tags = list_set_build_tags(&list).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/lib.rs b/events-codec/src/lib.rs @@ -18,6 +18,8 @@ pub mod post; pub mod reaction; pub mod listing; +pub mod list; +pub mod list_set; #[cfg(feature = "serde_json")] pub mod relay_document; diff --git a/events-codec/src/list/decode.rs b/events-codec/src/list/decode.rs @@ -0,0 +1,92 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use radroots_events::{ + RadrootsNostrEvent, + kinds::is_nip51_standard_list_kind, + list::{RadrootsList, RadrootsListEntry, RadrootsListEventIndex, RadrootsListEventMetadata}, +}; + +use crate::error::EventParseError; + +fn entry_from_tag(tag: &[String]) -> Result<RadrootsListEntry, EventParseError> { + let name = tag.get(0).ok_or(EventParseError::InvalidTag("tag"))?; + if name.trim().is_empty() { + return Err(EventParseError::InvalidTag("tag")); + } + let value = tag.get(1).ok_or(EventParseError::InvalidTag("tag"))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag("tag")); + } + Ok(RadrootsListEntry { + tag: name.clone(), + values: tag[1..].to_vec(), + }) +} + +pub fn list_from_tags( + kind: u32, + content: String, + tags: &[Vec<String>], +) -> Result<RadrootsList, EventParseError> { + if !is_nip51_standard_list_kind(kind) { + return Err(EventParseError::InvalidKind { + expected: "nip51 standard list kind", + got: kind, + }); + } + let mut entries = Vec::new(); + for tag in tags.iter().filter(|t| t.len() >= 2) { + entries.push(entry_from_tag(tag)?); + } + Ok(RadrootsList { content, entries }) +} + +pub fn metadata_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, +) -> Result<RadrootsListEventMetadata, EventParseError> { + let list = list_from_tags(kind, content, &tags)?; + Ok(RadrootsListEventMetadata { + id, + author, + published_at, + kind, + list, + }) +} + +pub fn index_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, + sig: String, +) -> Result<RadrootsListEventIndex, EventParseError> { + let metadata = metadata_from_event( + id.clone(), + author.clone(), + published_at, + kind, + content.clone(), + tags.clone(), + )?; + Ok(RadrootsListEventIndex { + event: RadrootsNostrEvent { + id, + author, + created_at: published_at, + kind, + content, + tags, + sig, + }, + metadata, + }) +} diff --git a/events-codec/src/list/encode.rs b/events-codec/src/list/encode.rs @@ -0,0 +1,50 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use radroots_events::{ + kinds::is_nip51_standard_list_kind, + list::{RadrootsList, RadrootsListEntry}, +}; + +use crate::error::EventEncodeError; +use crate::wire::WireEventParts; + +fn entry_tag(entry: &RadrootsListEntry) -> Result<Vec<String>, EventEncodeError> { + if entry.tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("entry.tag")); + } + let first = entry + .values + .get(0) + .ok_or(EventEncodeError::EmptyRequiredField("entry.values"))?; + if first.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("entry.values")); + } + let mut tag = Vec::with_capacity(1 + entry.values.len()); + tag.push(entry.tag.clone()); + tag.extend(entry.values.iter().cloned()); + 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 { + tags.push(entry_tag(entry)?); + } + Ok(tags) +} + +pub fn to_wire_parts_with_kind( + list: &RadrootsList, + kind: u32, +) -> Result<WireEventParts, EventEncodeError> { + if !is_nip51_standard_list_kind(kind) { + return Err(EventEncodeError::InvalidKind(kind)); + } + let tags = list_build_tags(list)?; + Ok(WireEventParts { + kind, + content: list.content.clone(), + tags, + }) +} diff --git a/events-codec/src/list/mod.rs b/events-codec/src/list/mod.rs @@ -0,0 +1,37 @@ +pub mod decode; +pub mod encode; + +#[cfg(test)] +mod tests { + use super::{decode::list_from_tags, encode::list_build_tags}; + use radroots_events::{ + kinds::KIND_LIST_MUTE, + list::{RadrootsList, RadrootsListEntry}, + }; + + #[test] + fn list_tags_round_trip() { + let list = RadrootsList { + content: "private".to_string(), + entries: vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["abc".to_string(), "wss://relay".to_string()], + }, + RadrootsListEntry { + tag: "t".to_string(), + values: vec!["radroots".to_string()], + }, + ], + }; + let tags = list_build_tags(&list).expect("build tags"); + let parsed = list_from_tags(KIND_LIST_MUTE, list.content.clone(), &tags) + .expect("parse list"); + assert_eq!(parsed.content, list.content); + assert_eq!(parsed.entries.len(), list.entries.len()); + assert_eq!(parsed.entries[0].tag, "p"); + assert_eq!(parsed.entries[0].values[0], "abc"); + assert_eq!(parsed.entries[1].tag, "t"); + assert_eq!(parsed.entries[1].values[0], "radroots"); + } +} diff --git a/events-codec/src/list_set/decode.rs b/events-codec/src/list_set/decode.rs @@ -0,0 +1,150 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use radroots_events::{ + RadrootsNostrEvent, + kinds::is_nip51_list_set_kind, + list::{RadrootsListEntry}, + list_set::{RadrootsListSet, RadrootsListSetEventIndex, RadrootsListSetEventMetadata}, +}; + +use crate::error::EventParseError; + +const TAG_D: &str = "d"; +const TAG_TITLE: &str = "title"; +const TAG_DESCRIPTION: &str = "description"; +const TAG_IMAGE: &str = "image"; + +fn entry_from_tag(tag: &[String]) -> Result<RadrootsListEntry, EventParseError> { + let name = tag.get(0).ok_or(EventParseError::InvalidTag("tag"))?; + if name.trim().is_empty() { + return Err(EventParseError::InvalidTag("tag")); + } + let value = tag.get(1).ok_or(EventParseError::InvalidTag("tag"))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag("tag")); + } + Ok(RadrootsListEntry { + tag: name.clone(), + values: tag[1..].to_vec(), + }) +} + +fn take_first_non_empty(tag: &[String]) -> Option<String> { + tag.get(1) + .filter(|v| !v.trim().is_empty()) + .cloned() +} + +pub fn list_set_from_tags( + kind: u32, + content: String, + tags: &[Vec<String>], +) -> Result<RadrootsListSet, EventParseError> { + if !is_nip51_list_set_kind(kind) { + return Err(EventParseError::InvalidKind { + expected: "nip51 list set kind", + got: kind, + }); + } + let mut d_tag: Option<String> = None; + let mut title: Option<String> = None; + let mut description: Option<String> = None; + let mut image: Option<String> = None; + let mut entries = Vec::new(); + + for tag in tags.iter().filter(|t| t.len() >= 2) { + let name = tag.get(0).ok_or(EventParseError::InvalidTag("tag"))?; + if name.trim().is_empty() { + return Err(EventParseError::InvalidTag("tag")); + } + match name.as_str() { + TAG_D => { + if d_tag.is_none() { + let value = tag.get(1).ok_or(EventParseError::InvalidTag("d"))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag("d")); + } + d_tag = Some(value.clone()); + } + } + TAG_TITLE => { + if title.is_none() { + title = take_first_non_empty(tag); + } + } + TAG_DESCRIPTION => { + if description.is_none() { + description = take_first_non_empty(tag); + } + } + TAG_IMAGE => { + if image.is_none() { + image = take_first_non_empty(tag); + } + } + _ => { + entries.push(entry_from_tag(tag)?); + } + } + } + + let d_tag = d_tag.ok_or(EventParseError::MissingTag("d"))?; + Ok(RadrootsListSet { + d_tag, + content, + entries, + title, + description, + image, + }) +} + +pub fn metadata_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, +) -> Result<RadrootsListSetEventMetadata, EventParseError> { + let list_set = list_set_from_tags(kind, content, &tags)?; + Ok(RadrootsListSetEventMetadata { + id, + author, + published_at, + kind, + list_set, + }) +} + +pub fn index_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, + sig: String, +) -> Result<RadrootsListSetEventIndex, EventParseError> { + let metadata = metadata_from_event( + id.clone(), + author.clone(), + published_at, + kind, + content.clone(), + tags.clone(), + )?; + Ok(RadrootsListSetEventIndex { + event: RadrootsNostrEvent { + id, + author, + created_at: published_at, + kind, + content, + tags, + sig, + }, + metadata, + }) +} diff --git a/events-codec/src/list_set/encode.rs b/events-codec/src/list_set/encode.rs @@ -0,0 +1,71 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::{String, ToString}, vec::Vec}; + +use radroots_events::{ + kinds::is_nip51_list_set_kind, + list_set::RadrootsListSet, +}; + +use crate::error::EventEncodeError; +use crate::wire::WireEventParts; + +const TAG_D: &str = "d"; +const TAG_TITLE: &str = "title"; +const TAG_DESCRIPTION: &str = "description"; +const TAG_IMAGE: &str = "image"; + +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 list_set_build_tags(list: &RadrootsListSet) -> Result<Vec<Vec<String>>, EventEncodeError> { + if list.d_tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("d_tag")); + } + let mut tags = Vec::with_capacity(1 + list.entries.len() + 3); + push_tag(&mut tags, TAG_D, &list.d_tag); + if let Some(title) = list.title.as_ref().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_TITLE, title); + } + if let Some(description) = list.description.as_ref().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_DESCRIPTION, description); + } + if let Some(image) = list.image.as_ref().filter(|v| !v.trim().is_empty()) { + push_tag(&mut tags, TAG_IMAGE, image); + } + for entry in &list.entries { + if entry.tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("entry.tag")); + } + let first = entry + .values + .get(0) + .ok_or(EventEncodeError::EmptyRequiredField("entry.values"))?; + if first.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("entry.values")); + } + let mut tag = Vec::with_capacity(1 + entry.values.len()); + tag.push(entry.tag.clone()); + tag.extend(entry.values.iter().cloned()); + tags.push(tag); + } + Ok(tags) +} + +pub fn to_wire_parts_with_kind( + list: &RadrootsListSet, + kind: u32, +) -> Result<WireEventParts, EventEncodeError> { + if !is_nip51_list_set_kind(kind) { + return Err(EventEncodeError::InvalidKind(kind)); + } + let tags = list_set_build_tags(list)?; + Ok(WireEventParts { + kind, + content: list.content.clone(), + tags, + }) +} diff --git a/events-codec/src/list_set/mod.rs b/events-codec/src/list_set/mod.rs @@ -0,0 +1,40 @@ +pub mod decode; +pub mod encode; + +#[cfg(test)] +mod tests { + use super::{decode::list_set_from_tags, encode::list_set_build_tags}; + use radroots_events::{ + kinds::KIND_LIST_SET_FOLLOW, + list::{RadrootsListEntry}, + list_set::RadrootsListSet, + }; + + #[test] + fn list_set_tags_round_trip() { + let list = RadrootsListSet { + d_tag: "members.owners".to_string(), + content: "".to_string(), + entries: vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["owner_pubkey".to_string()], + }, + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["worker_pubkey".to_string(), "wss://relay".to_string()], + }, + ], + title: Some("Owners".to_string()), + description: None, + image: None, + }; + let tags = list_set_build_tags(&list).expect("build tags"); + let parsed = list_set_from_tags(KIND_LIST_SET_FOLLOW, list.content.clone(), &tags) + .expect("parse list set"); + assert_eq!(parsed.d_tag, list.d_tag); + assert_eq!(parsed.title, list.title); + assert_eq!(parsed.entries.len(), list.entries.len()); + assert_eq!(parsed.entries[0].values[0], "owner_pubkey"); + } +} diff --git a/events-codec/src/tag_builders.rs b/events-codec/src/tag_builders.rs @@ -13,6 +13,8 @@ use radroots_events::{ job_request::RadrootsJobRequest, job_result::RadrootsJobResult, listing::RadrootsListing, + list::RadrootsList, + list_set::RadrootsListSet, message::RadrootsMessage, post::RadrootsPost, profile::RadrootsProfile, @@ -28,6 +30,8 @@ use crate::job::feedback::encode::job_feedback_build_tags; use crate::job::request::encode::job_request_build_tags; use crate::job::result::encode::job_result_build_tags; use crate::listing::tags::listing_tags; +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::reaction::encode::reaction_build_tags; @@ -84,6 +88,22 @@ impl RadrootsEventTagBuilder for RadrootsFollow { } } +impl RadrootsEventTagBuilder for RadrootsList { + type Error = EventEncodeError; + + fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> { + list_build_tags(self) + } +} + +impl RadrootsEventTagBuilder for RadrootsListSet { + type Error = EventEncodeError; + + fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> { + list_set_build_tags(self) + } +} + impl RadrootsEventTagBuilder for RadrootsJobRequest { type Error = JobEncodeError; diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts @@ -50,6 +50,20 @@ export type RadrootsJobResultEventIndex = { event: RadrootsNostrEvent, metadata: export type RadrootsJobResultEventMetadata = { id: string, author: string, published_at: number, kind: number, job_result: RadrootsJobResult, }; +export type RadrootsList = { content: string, entries: Array<RadrootsListEntry>, }; + +export type RadrootsListEntry = { tag: string, values: Array<string>, }; + +export type RadrootsListEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsListEventMetadata, }; + +export type RadrootsListEventMetadata = { id: string, author: string, published_at: number, kind: number, list: RadrootsList, }; + +export type RadrootsListSet = { d_tag: string, content: string, entries: Array<RadrootsListEntry>, title?: string | null, description?: string | null, image?: string | null, }; + +export type RadrootsListSetEventIndex = { event: RadrootsNostrEvent, metadata: RadrootsListSetEventMetadata, }; + +export type RadrootsListSetEventMetadata = { id: string, author: string, published_at: number, kind: number, list_set: RadrootsListSet, }; + export type RadrootsListing = { d_tag: string, product: RadrootsListingProduct, quantities: Array<RadrootsListingQuantity>, prices: RadrootsCoreQuantityPrice[], discounts?: RadrootsListingDiscount[] | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingLocation | null, images?: RadrootsListingImage[] | null, }; export type RadrootsListingAvailability = { "kind": "window", "amount": { start?: number | null, end?: number | null, } } | { "kind": "status", "amount": { status: RadrootsListingStatus, } }; diff --git a/events/bindings/ts/src/typeshare-types.ts b/events/bindings/ts/src/typeshare-types.ts @@ -8,6 +8,37 @@ export const KIND_FOLLOW: number = 3; export const KIND_REACTION: number = 7; export const KIND_MESSAGE: number = 14; export const KIND_COMMENT: number = 1111; +export const KIND_LIST_MUTE: number = 10000; +export const KIND_LIST_PINNED_NOTES: number = 10001; +export const KIND_LIST_READ_WRITE_RELAYS: number = 10002; +export const KIND_LIST_BOOKMARKS: number = 10003; +export const KIND_LIST_COMMUNITIES: number = 10004; +export const KIND_LIST_PUBLIC_CHATS: number = 10005; +export const KIND_LIST_BLOCKED_RELAYS: number = 10006; +export const KIND_LIST_SEARCH_RELAYS: number = 10007; +export const KIND_LIST_SIMPLE_GROUPS: number = 10009; +export const KIND_LIST_RELAY_FEEDS: number = 10012; +export const KIND_LIST_INTERESTS: number = 10015; +export const KIND_LIST_MEDIA_FOLLOWS: number = 10020; +export const KIND_LIST_EMOJIS: number = 10030; +export const KIND_LIST_DM_RELAYS: number = 10050; +export const KIND_LIST_GOOD_WIKI_AUTHORS: number = 10101; +export const KIND_LIST_GOOD_WIKI_RELAYS: number = 10102; +export const KIND_LIST_SET_FOLLOW: number = 30000; +export const KIND_LIST_SET_GENERIC: number = 30001; +export const KIND_LIST_SET_RELAY: number = 30002; +export const KIND_LIST_SET_BOOKMARK: number = 30003; +export const KIND_LIST_SET_CURATION: number = 30004; +export const KIND_LIST_SET_VIDEO: number = 30005; +export const KIND_LIST_SET_PICTURE: number = 30006; +export const KIND_LIST_SET_KIND_MUTE: number = 30007; +export const KIND_LIST_SET_INTEREST: number = 30015; +export const KIND_LIST_SET_EMOJI: number = 30030; +export const KIND_LIST_SET_RELEASE_ARTIFACT: number = 30063; +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_APP_DATA: number = 30078; export const KIND_LISTING: number = 30402; export const KIND_APPLICATION_HANDLER: number = 31990; diff --git a/events/src/kinds.rs b/events/src/kinds.rs @@ -11,6 +11,68 @@ pub const KIND_MESSAGE: u32 = 14; #[cfg_attr(feature = "typeshare", typeshare::typeshare)] pub const KIND_COMMENT: u32 = 1111; #[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_MUTE: u32 = 10000; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_PINNED_NOTES: u32 = 10001; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_READ_WRITE_RELAYS: u32 = 10002; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_BOOKMARKS: u32 = 10003; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_COMMUNITIES: u32 = 10004; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_PUBLIC_CHATS: u32 = 10005; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_BLOCKED_RELAYS: u32 = 10006; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SEARCH_RELAYS: u32 = 10007; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SIMPLE_GROUPS: u32 = 10009; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_RELAY_FEEDS: u32 = 10012; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_INTERESTS: u32 = 10015; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_MEDIA_FOLLOWS: u32 = 10020; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_EMOJIS: u32 = 10030; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_DM_RELAYS: u32 = 10050; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_GOOD_WIKI_AUTHORS: u32 = 10101; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_GOOD_WIKI_RELAYS: u32 = 10102; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_FOLLOW: u32 = 30000; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_GENERIC: u32 = 30001; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_RELAY: u32 = 30002; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_BOOKMARK: u32 = 30003; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_CURATION: u32 = 30004; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_VIDEO: u32 = 30005; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_PICTURE: u32 = 30006; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_KIND_MUTE: u32 = 30007; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_INTEREST: u32 = 30015; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_EMOJI: u32 = 30030; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_RELEASE_ARTIFACT: u32 = 30063; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_APP_CURATION: u32 = 30267; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +pub const KIND_LIST_SET_CALENDAR: u32 = 31924; +#[cfg_attr(feature = "typeshare", typeshare::typeshare)] +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_APP_DATA: u32 = 30078; #[cfg_attr(feature = "typeshare", typeshare::typeshare)] pub const KIND_LISTING: u32 = 30402; @@ -29,6 +91,50 @@ pub const KIND_JOB_RESULT_MAX: u32 = 6999; pub const KIND_JOB_FEEDBACK: u32 = 7000; #[inline] +pub const fn is_nip51_standard_list_kind(kind: u32) -> bool { + matches!( + kind, + KIND_LIST_MUTE + | KIND_LIST_PINNED_NOTES + | KIND_LIST_READ_WRITE_RELAYS + | KIND_LIST_BOOKMARKS + | KIND_LIST_COMMUNITIES + | KIND_LIST_PUBLIC_CHATS + | KIND_LIST_BLOCKED_RELAYS + | KIND_LIST_SEARCH_RELAYS + | KIND_LIST_SIMPLE_GROUPS + | KIND_LIST_RELAY_FEEDS + | KIND_LIST_INTERESTS + | KIND_LIST_MEDIA_FOLLOWS + | KIND_LIST_EMOJIS + | KIND_LIST_DM_RELAYS + | KIND_LIST_GOOD_WIKI_AUTHORS + | KIND_LIST_GOOD_WIKI_RELAYS + ) +} +#[inline] +pub const fn is_nip51_list_set_kind(kind: u32) -> bool { + matches!( + kind, + KIND_LIST_SET_FOLLOW + | KIND_LIST_SET_GENERIC + | KIND_LIST_SET_RELAY + | KIND_LIST_SET_BOOKMARK + | KIND_LIST_SET_CURATION + | KIND_LIST_SET_VIDEO + | KIND_LIST_SET_PICTURE + | KIND_LIST_SET_KIND_MUTE + | KIND_LIST_SET_INTEREST + | KIND_LIST_SET_EMOJI + | KIND_LIST_SET_RELEASE_ARTIFACT + | KIND_LIST_SET_APP_CURATION + | KIND_LIST_SET_CALENDAR + | KIND_LIST_SET_STARTER_PACK + | KIND_LIST_SET_MEDIA_STARTER_PACK + ) +} + +#[inline] pub const fn is_request_kind(kind: u32) -> bool { kind >= KIND_JOB_REQUEST_MIN && kind <= KIND_JOB_REQUEST_MAX } diff --git a/events/src/lib.rs b/events/src/lib.rs @@ -17,6 +17,8 @@ pub mod job_request; pub mod job_result; pub mod kinds; pub mod listing; +pub mod list; +pub mod list_set; pub mod app_data; pub mod message; pub mod post; diff --git a/events/src/list.rs b/events/src/list.rs @@ -0,0 +1,45 @@ +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 RadrootsListEventIndex { + pub event: RadrootsNostrEvent, + pub metadata: RadrootsListEventMetadata, +} + +#[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 RadrootsListEventMetadata { + pub id: String, + pub author: String, + pub published_at: u32, + pub kind: u32, + pub list: RadrootsList, +} + +#[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 RadrootsList { + pub content: String, + pub entries: Vec<RadrootsListEntry>, +} + +#[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 RadrootsListEntry { + pub tag: String, + pub values: Vec<String>, +} diff --git a/events/src/list_set.rs b/events/src/list_set.rs @@ -0,0 +1,43 @@ +use crate::{RadrootsNostrEvent, list::RadrootsListEntry}; +#[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 RadrootsListSetEventIndex { + pub event: RadrootsNostrEvent, + pub metadata: RadrootsListSetEventMetadata, +} + +#[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 RadrootsListSetEventMetadata { + pub id: String, + pub author: String, + pub published_at: u32, + pub kind: u32, + pub list_set: RadrootsListSet, +} + +#[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 RadrootsListSet { + pub d_tag: String, + pub content: String, + pub entries: Vec<RadrootsListEntry>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub title: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub description: Option<String>, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] + pub image: Option<String>, +} diff --git a/events/src/typeshare_kinds.rs b/events/src/typeshare_kinds.rs @@ -11,6 +11,68 @@ pub const KIND_MESSAGE: u32 = 14; #[typeshare::typeshare] pub const KIND_COMMENT: u32 = 1111; #[typeshare::typeshare] +pub const KIND_LIST_MUTE: u32 = 10000; +#[typeshare::typeshare] +pub const KIND_LIST_PINNED_NOTES: u32 = 10001; +#[typeshare::typeshare] +pub const KIND_LIST_READ_WRITE_RELAYS: u32 = 10002; +#[typeshare::typeshare] +pub const KIND_LIST_BOOKMARKS: u32 = 10003; +#[typeshare::typeshare] +pub const KIND_LIST_COMMUNITIES: u32 = 10004; +#[typeshare::typeshare] +pub const KIND_LIST_PUBLIC_CHATS: u32 = 10005; +#[typeshare::typeshare] +pub const KIND_LIST_BLOCKED_RELAYS: u32 = 10006; +#[typeshare::typeshare] +pub const KIND_LIST_SEARCH_RELAYS: u32 = 10007; +#[typeshare::typeshare] +pub const KIND_LIST_SIMPLE_GROUPS: u32 = 10009; +#[typeshare::typeshare] +pub const KIND_LIST_RELAY_FEEDS: u32 = 10012; +#[typeshare::typeshare] +pub const KIND_LIST_INTERESTS: u32 = 10015; +#[typeshare::typeshare] +pub const KIND_LIST_MEDIA_FOLLOWS: u32 = 10020; +#[typeshare::typeshare] +pub const KIND_LIST_EMOJIS: u32 = 10030; +#[typeshare::typeshare] +pub const KIND_LIST_DM_RELAYS: u32 = 10050; +#[typeshare::typeshare] +pub const KIND_LIST_GOOD_WIKI_AUTHORS: u32 = 10101; +#[typeshare::typeshare] +pub const KIND_LIST_GOOD_WIKI_RELAYS: u32 = 10102; +#[typeshare::typeshare] +pub const KIND_LIST_SET_FOLLOW: u32 = 30000; +#[typeshare::typeshare] +pub const KIND_LIST_SET_GENERIC: u32 = 30001; +#[typeshare::typeshare] +pub const KIND_LIST_SET_RELAY: u32 = 30002; +#[typeshare::typeshare] +pub const KIND_LIST_SET_BOOKMARK: u32 = 30003; +#[typeshare::typeshare] +pub const KIND_LIST_SET_CURATION: u32 = 30004; +#[typeshare::typeshare] +pub const KIND_LIST_SET_VIDEO: u32 = 30005; +#[typeshare::typeshare] +pub const KIND_LIST_SET_PICTURE: u32 = 30006; +#[typeshare::typeshare] +pub const KIND_LIST_SET_KIND_MUTE: u32 = 30007; +#[typeshare::typeshare] +pub const KIND_LIST_SET_INTEREST: u32 = 30015; +#[typeshare::typeshare] +pub const KIND_LIST_SET_EMOJI: u32 = 30030; +#[typeshare::typeshare] +pub const KIND_LIST_SET_RELEASE_ARTIFACT: u32 = 30063; +#[typeshare::typeshare] +pub const KIND_LIST_SET_APP_CURATION: u32 = 30267; +#[typeshare::typeshare] +pub const KIND_LIST_SET_CALENDAR: u32 = 31924; +#[typeshare::typeshare] +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_APP_DATA: u32 = 30078; #[typeshare::typeshare] pub const KIND_LISTING: u32 = 30402;