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:
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;