tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit bd21aa73a6f854389475cd9c77cf932ca4ed717c
parent f27291c96996502fed932fc578631d38b12c7da9
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 03:51:08 -0700

groups: introduce pocket event view

- Add a group-facing EventView trait with adapters for Tangle protocol events and Pocket events.

- Expose borrowed group tag access for indexed pairs and full tag values without runtime JSON reparse.

- Pin pocket-types directly in tangle_groups using the approved triesap/pocket revision.

- Validated with cargo fmt --all -- --check, cargo check --workspace --all-targets, cargo test --workspace, and cargo clippy --workspace --all-targets -- -D warnings.

Diffstat:
MCargo.lock | 1+
Mcrates/tangle_groups/Cargo.toml | 1+
Acrates/tangle_groups/src/event_view.rs | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_groups/src/lib.rs | 2++
4 files changed, 247 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1240,6 +1240,7 @@ dependencies = [ name = "tangle_groups" version = "0.1.0" dependencies = [ + "pocket-types", "serde", "serde_json", "tangle_crypto", diff --git a/crates/tangle_groups/Cargo.toml b/crates/tangle_groups/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true description = "NIP-29 group domain and configuration types for tangle" [dependencies] +pocket-types = { git = "https://github.com/triesap/pocket", rev = "329334f20948c796c6016b673b92551ac4855ad7" } serde = { version = "1", features = ["derive"] } serde_json = "1" tangle_crypto = { path = "../tangle_crypto" } diff --git a/crates/tangle_groups/src/event_view.rs b/crates/tangle_groups/src/event_view.rs @@ -0,0 +1,243 @@ +use crate::errors::{GroupError, GroupErrorKind}; +use pocket_types::{ + Event as PocketEvent, OwnedEvent as PocketOwnedEvent, TagsStringIter as PocketTagsStringIter, +}; +use std::str; +use tangle_protocol::{Event, Tag, TagName}; + +pub trait GroupEventView { + fn id_hex(&self) -> String; + + fn pubkey_hex(&self) -> String; + + fn kind_u32(&self) -> u32; + + fn visit_tags<'a, F>(&'a self, visitor: F) -> Result<(), GroupError> + where + F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>; +} + +#[derive(Debug)] +pub enum GroupEventTag<'a> { + Tangle(&'a Tag), + Pocket(PocketTagsStringIter<'a>), +} + +impl<'a> GroupEventTag<'a> { + pub fn first_value(self) -> Result<Option<&'a str>, GroupError> { + match self { + Self::Tangle(tag) => Ok(tag.values().first().map(String::as_str)), + Self::Pocket(mut values) => values.next().map(tag_value_utf8).transpose(), + } + } + + pub fn indexed_pair(self) -> Result<Option<(&'a str, &'a str)>, GroupError> { + match self { + Self::Tangle(tag) => Ok(tag.indexed_pair()), + Self::Pocket(mut values) => { + let Some(name) = values.next() else { + return Ok(None); + }; + let name = tag_value_utf8(name)?; + if !TagName::is_indexable_name(name) { + return Ok(None); + } + let Some(value) = values.next() else { + return Ok(None); + }; + Ok(Some((name, tag_value_utf8(value)?))) + } + } + } + + pub fn values(self) -> Result<Vec<&'a str>, GroupError> { + match self { + Self::Tangle(tag) => Ok(tag.values().iter().map(String::as_str).collect()), + Self::Pocket(values) => values.map(tag_value_utf8).collect(), + } + } +} + +impl GroupEventView for Event { + fn id_hex(&self) -> String { + self.id().as_str().to_owned() + } + + fn pubkey_hex(&self) -> String { + self.unsigned().pubkey().as_str().to_owned() + } + + fn kind_u32(&self) -> u32 { + self.unsigned().kind().as_u32() + } + + fn visit_tags<'a, F>(&'a self, mut visitor: F) -> Result<(), GroupError> + where + F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>, + { + for tag in self.unsigned().tags() { + visitor(GroupEventTag::Tangle(tag))?; + } + Ok(()) + } +} + +impl GroupEventView for PocketEvent { + fn id_hex(&self) -> String { + self.id().as_hex_string() + } + + fn pubkey_hex(&self) -> String { + self.pubkey().as_hex_string() + } + + fn kind_u32(&self) -> u32 { + u32::from(self.kind().as_u16()) + } + + fn visit_tags<'a, F>(&'a self, mut visitor: F) -> Result<(), GroupError> + where + F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>, + { + let tags = self.tags().map_err(pocket_tags_error)?; + for tag in tags.iter() { + visitor(GroupEventTag::Pocket(tag))?; + } + Ok(()) + } +} + +impl GroupEventView for PocketOwnedEvent { + fn id_hex(&self) -> String { + let event: &PocketEvent = self; + event.id_hex() + } + + fn pubkey_hex(&self) -> String { + let event: &PocketEvent = self; + event.pubkey_hex() + } + + fn kind_u32(&self) -> u32 { + let event: &PocketEvent = self; + event.kind_u32() + } + + fn visit_tags<'a, F>(&'a self, visitor: F) -> Result<(), GroupError> + where + F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>, + { + let event: &PocketEvent = self; + event.visit_tags(visitor) + } +} + +fn tag_value_utf8(value: &[u8]) -> Result<&str, GroupError> { + str::from_utf8(value).map_err(|_| { + GroupError::invalid( + GroupErrorKind::MalformedGroupTag, + "group event tag is not valid UTF-8", + ) + }) +} + +fn pocket_tags_error(error: pocket_types::Error) -> GroupError { + GroupError::invalid( + GroupErrorKind::MalformedGroupTag, + format!("malformed Pocket event tags: {error}"), + ) +} + +#[cfg(test)] +mod tests { + use super::GroupEventView; + use pocket_types::Event as PocketEvent; + use tangle_protocol::{ + Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, + event_to_value, + }; + + #[test] + fn tangle_event_view_exposes_group_fields() { + let event = event(); + + assert_eq!(event.kind_u32(), 1); + assert_eq!(event.id_hex(), "0".repeat(64)); + assert_eq!(event.pubkey_hex(), "1".repeat(64)); + assert_eq!( + indexed_pairs(&event), + vec![("h".to_owned(), "Farm".to_owned())] + ); + assert_eq!( + tag_values(&event), + vec![ + vec!["h".to_owned(), "Farm".to_owned()], + vec!["summary".to_owned(), "Harvest".to_owned()], + ] + ); + } + + #[test] + fn pocket_event_view_exposes_group_fields_without_tangle_reparse() { + let event = event(); + let raw = event_to_value(&event).to_string(); + let mut buffer = vec![0; 4096]; + let (_, pocket) = PocketEvent::from_json(raw.as_bytes(), &mut buffer).expect("pocket"); + + assert_eq!(pocket.kind_u32(), 1); + assert_eq!(pocket.id_hex(), "0".repeat(64)); + assert_eq!(pocket.pubkey_hex(), "1".repeat(64)); + assert_eq!( + indexed_pairs(pocket), + vec![("h".to_owned(), "Farm".to_owned())] + ); + assert_eq!( + tag_values(pocket), + vec![ + vec!["h".to_owned(), "Farm".to_owned()], + vec!["summary".to_owned(), "Harvest".to_owned()], + ] + ); + } + + fn indexed_pairs<E: GroupEventView + ?Sized>(event: &E) -> Vec<(String, String)> { + let mut pairs = Vec::new(); + event + .visit_tags(|tag| { + if let Some((name, value)) = tag.indexed_pair()? { + pairs.push((name.to_owned(), value.to_owned())); + } + Ok(()) + }) + .expect("visit tags"); + pairs + } + + fn tag_values<E: GroupEventView + ?Sized>(event: &E) -> Vec<Vec<String>> { + let mut tags = Vec::new(); + event + .visit_tags(|tag| { + tags.push(tag.values()?.into_iter().map(str::to_owned).collect()); + Ok(()) + }) + .expect("visit tags"); + tags + } + + fn event() -> Event { + Event::new( + EventId::new(&"0".repeat(64)).expect("id"), + UnsignedEvent::new( + PublicKeyHex::new(&"1".repeat(64)).expect("pubkey"), + UnixTimestamp::new(1), + Kind::new(1).expect("kind"), + vec![ + Tag::from_parts("h", &["Farm"]).expect("h"), + Tag::from_parts("summary", &["Harvest"]).expect("summary"), + ], + "", + ), + SignatureHex::new(&"2".repeat(128)).expect("sig"), + ) + } +} diff --git a/crates/tangle_groups/src/lib.rs b/crates/tangle_groups/src/lib.rs @@ -2,6 +2,7 @@ pub mod classification; pub mod errors; +pub mod event_view; pub mod ids; pub mod kinds; pub mod metadata; @@ -20,6 +21,7 @@ use tangle_protocol::PublicKeyHex; pub use classification::{GroupEventClass, classify_group_event}; pub use errors::{GroupError, GroupErrorKind, GroupReplyPrefix}; +pub use event_view::{GroupEventTag, GroupEventView}; pub use ids::GroupId; pub use kinds::{ KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE, KIND_GROUP_DELETE_EVENT,