metadata.rs (10624B)
1 use std::collections::BTreeSet; 2 3 use crate::{ 4 GroupLimitsConfig, 5 errors::{GroupError, GroupErrorKind}, 6 event_view::{GroupEventTag, GroupEventView}, 7 }; 8 use tangle_protocol::Kind; 9 10 pub const MAX_METADATA_NAME_BYTES: usize = 128; 11 pub const MAX_METADATA_PICTURE_BYTES: usize = 2_048; 12 pub const MAX_METADATA_ABOUT_BYTES: usize = 4_096; 13 14 #[derive(Debug, Clone, PartialEq, Eq)] 15 pub struct GroupMetadata { 16 name: Option<String>, 17 picture: Option<String>, 18 about: Option<String>, 19 private: bool, 20 restricted: bool, 21 hidden: bool, 22 closed: bool, 23 supported_kinds: SupportedKinds, 24 } 25 26 impl GroupMetadata { 27 pub fn from_parts( 28 text: GroupMetadataText, 29 flags: GroupMetadataFlags, 30 supported_kinds: SupportedKinds, 31 ) -> Self { 32 Self { 33 name: text.name, 34 picture: text.picture, 35 about: text.about, 36 private: flags.private, 37 restricted: flags.restricted, 38 hidden: flags.hidden, 39 closed: flags.closed, 40 supported_kinds, 41 } 42 } 43 44 pub fn empty() -> Self { 45 Self { 46 name: None, 47 picture: None, 48 about: None, 49 private: false, 50 restricted: false, 51 hidden: false, 52 closed: false, 53 supported_kinds: SupportedKinds::UnspecifiedAll, 54 } 55 } 56 57 pub fn name(&self) -> Option<&str> { 58 self.name.as_deref() 59 } 60 61 pub fn picture(&self) -> Option<&str> { 62 self.picture.as_deref() 63 } 64 65 pub fn about(&self) -> Option<&str> { 66 self.about.as_deref() 67 } 68 69 pub fn private(&self) -> bool { 70 self.private 71 } 72 73 pub fn restricted(&self) -> bool { 74 self.restricted 75 } 76 77 pub fn hidden(&self) -> bool { 78 self.hidden 79 } 80 81 pub fn closed(&self) -> bool { 82 self.closed 83 } 84 85 pub fn supported_kinds(&self) -> &SupportedKinds { 86 &self.supported_kinds 87 } 88 } 89 90 #[derive(Debug, Clone, PartialEq, Eq)] 91 pub struct GroupMetadataText { 92 name: Option<String>, 93 picture: Option<String>, 94 about: Option<String>, 95 } 96 97 impl GroupMetadataText { 98 pub fn new(name: Option<String>, picture: Option<String>, about: Option<String>) -> Self { 99 Self { 100 name, 101 picture, 102 about, 103 } 104 } 105 106 pub fn empty() -> Self { 107 Self { 108 name: None, 109 picture: None, 110 about: None, 111 } 112 } 113 } 114 115 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 116 pub struct GroupMetadataFlags { 117 private: bool, 118 restricted: bool, 119 hidden: bool, 120 closed: bool, 121 } 122 123 impl GroupMetadataFlags { 124 pub fn new(private: bool, restricted: bool, hidden: bool, closed: bool) -> Self { 125 Self { 126 private, 127 restricted, 128 hidden, 129 closed, 130 } 131 } 132 } 133 134 #[derive(Debug, Clone, PartialEq, Eq)] 135 pub enum SupportedKinds { 136 UnspecifiedAll, 137 None, 138 Only(BTreeSet<Kind>), 139 } 140 141 pub fn parse_group_metadata( 142 event: &(impl GroupEventView + ?Sized), 143 limits: GroupLimitsConfig, 144 ) -> Result<GroupMetadata, GroupError> { 145 let mut builder = MetadataBuilder::default(); 146 event.visit_tags(|tag| { 147 let Some(name) = tag.first_value() else { 148 return Ok(()); 149 }; 150 match name { 151 "name" => builder.name = parse_text_tag(&tag, "name", MAX_METADATA_NAME_BYTES)?, 152 "picture" => { 153 builder.picture = parse_text_tag(&tag, "picture", MAX_METADATA_PICTURE_BYTES)? 154 } 155 "about" => builder.about = parse_text_tag(&tag, "about", MAX_METADATA_ABOUT_BYTES)?, 156 "private" => builder.private = true, 157 "restricted" => builder.restricted = true, 158 "hidden" => builder.hidden = true, 159 "closed" => builder.closed = true, 160 "supported_kinds" => { 161 if builder.supported_kinds.is_some() { 162 return Err(GroupError::invalid( 163 GroupErrorKind::TooManySupportedKinds, 164 "metadata must contain at most one supported_kinds tag", 165 )); 166 } 167 builder.supported_kinds = Some(parse_supported_kinds_tag(&tag, limits)?); 168 } 169 _ => {} 170 } 171 Ok(()) 172 })?; 173 Ok(GroupMetadata { 174 name: builder.name, 175 picture: builder.picture, 176 about: builder.about, 177 private: builder.private, 178 restricted: builder.restricted, 179 hidden: builder.hidden, 180 closed: builder.closed, 181 supported_kinds: builder 182 .supported_kinds 183 .unwrap_or(SupportedKinds::UnspecifiedAll), 184 }) 185 } 186 187 #[derive(Default)] 188 struct MetadataBuilder { 189 name: Option<String>, 190 picture: Option<String>, 191 about: Option<String>, 192 private: bool, 193 restricted: bool, 194 hidden: bool, 195 closed: bool, 196 supported_kinds: Option<SupportedKinds>, 197 } 198 199 fn parse_text_tag( 200 tag: &GroupEventTag<'_>, 201 field: &'static str, 202 max_bytes: usize, 203 ) -> Result<Option<String>, GroupError> { 204 let value = tag.value(1).map(str::to_owned); 205 if let Some(value) = &value 206 && value.len() > max_bytes 207 { 208 return Err(GroupError::invalid( 209 GroupErrorKind::MetadataTooLarge, 210 format!("metadata {field} must be at most {max_bytes} bytes"), 211 )); 212 } 213 Ok(value) 214 } 215 216 fn parse_supported_kinds_tag( 217 tag: &GroupEventTag<'_>, 218 limits: GroupLimitsConfig, 219 ) -> Result<SupportedKinds, GroupError> { 220 let values = tag.values().iter().skip(1).copied().collect::<Vec<_>>(); 221 if values.is_empty() { 222 return Ok(SupportedKinds::None); 223 } 224 let max = usize::from(limits.max_supported_kinds()); 225 if values.len() > max { 226 return Err(GroupError::invalid( 227 GroupErrorKind::TooManySupportedKinds, 228 format!( 229 "supported_kinds has {} values, maximum is {max}", 230 values.len() 231 ), 232 )); 233 } 234 let mut kinds = BTreeSet::new(); 235 for value in values { 236 let raw = value.parse::<u64>().map_err(|_| { 237 GroupError::invalid( 238 GroupErrorKind::UnsupportedGroupKind, 239 "supported_kinds values must be unsigned integers", 240 ) 241 })?; 242 kinds.insert(Kind::new(raw).map_err(|reason| { 243 GroupError::invalid( 244 GroupErrorKind::UnsupportedGroupKind, 245 format!("supported_kinds value is invalid: {reason}"), 246 ) 247 })?); 248 } 249 Ok(SupportedKinds::Only(kinds)) 250 } 251 252 #[cfg(test)] 253 mod tests { 254 use std::collections::BTreeSet; 255 256 use super::{SupportedKinds, parse_group_metadata}; 257 use crate::{GroupErrorKind, GroupLimitsConfig}; 258 use tangle_protocol::{ 259 Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, 260 }; 261 262 #[test] 263 fn parses_group_metadata_flags_and_fields() { 264 let metadata = parse_group_metadata( 265 &event(vec![ 266 Tag::from_parts("name", &["Farmers"]).expect("name"), 267 Tag::from_parts("picture", &["https://radroots.test/group.png"]).expect("picture"), 268 Tag::from_parts("about", &["Local harvest coordination"]).expect("about"), 269 Tag::from_parts("private", &[]).expect("private"), 270 Tag::from_parts("restricted", &[]).expect("restricted"), 271 Tag::from_parts("hidden", &[]).expect("hidden"), 272 Tag::from_parts("closed", &[]).expect("closed"), 273 Tag::from_parts("supported_kinds", &["1", "7"]).expect("supported"), 274 ]), 275 GroupLimitsConfig::default(), 276 ) 277 .expect("metadata"); 278 279 assert_eq!(metadata.name(), Some("Farmers")); 280 assert_eq!(metadata.picture(), Some("https://radroots.test/group.png")); 281 assert_eq!(metadata.about(), Some("Local harvest coordination")); 282 assert!(metadata.private()); 283 assert!(metadata.restricted()); 284 assert!(metadata.hidden()); 285 assert!(metadata.closed()); 286 assert_eq!( 287 metadata.supported_kinds(), 288 &SupportedKinds::Only(BTreeSet::from([ 289 Kind::new(1).expect("kind"), 290 Kind::new(7).expect("kind") 291 ])) 292 ); 293 } 294 295 #[test] 296 fn supported_kinds_absent_empty_and_list_forms_are_distinct() { 297 assert_eq!( 298 parse_group_metadata(&event(Vec::new()), GroupLimitsConfig::default()) 299 .expect("absent") 300 .supported_kinds(), 301 &SupportedKinds::UnspecifiedAll 302 ); 303 assert_eq!( 304 parse_group_metadata( 305 &event(vec![ 306 Tag::from_parts("supported_kinds", &[]).expect("supported") 307 ]), 308 GroupLimitsConfig::default() 309 ) 310 .expect("empty") 311 .supported_kinds(), 312 &SupportedKinds::None 313 ); 314 assert!(matches!( 315 parse_group_metadata( 316 &event(vec![Tag::from_parts("supported_kinds", &["1"]).expect("supported")]), 317 GroupLimitsConfig::default() 318 ) 319 .expect("list") 320 .supported_kinds(), 321 SupportedKinds::Only(kinds) if kinds.contains(&Kind::new(1).expect("kind")) 322 )); 323 } 324 325 #[test] 326 fn metadata_parser_rejects_oversize_fields_and_kind_limits() { 327 let error = parse_group_metadata( 328 &event(vec![ 329 Tag::from_parts("name", &[&"a".repeat(129)]).expect("name"), 330 ]), 331 GroupLimitsConfig::default(), 332 ) 333 .expect_err("name"); 334 assert_eq!(error.kind(), GroupErrorKind::MetadataTooLarge); 335 336 let limits = GroupLimitsConfig::new(128, 8, 1, 1, 1).expect("limits"); 337 let error = parse_group_metadata( 338 &event(vec![ 339 Tag::from_parts("supported_kinds", &["1", "2"]).expect("supported"), 340 ]), 341 limits, 342 ) 343 .expect_err("supported kinds"); 344 assert_eq!(error.kind(), GroupErrorKind::TooManySupportedKinds); 345 } 346 347 fn event(tags: Vec<Tag>) -> Event { 348 Event::new( 349 EventId::new(&"0".repeat(64)).expect("id"), 350 UnsignedEvent::new( 351 PublicKeyHex::new(&"1".repeat(64)).expect("pubkey"), 352 UnixTimestamp::new(1), 353 Kind::new(1).expect("kind"), 354 tags, 355 "", 356 ), 357 SignatureHex::new(&"2".repeat(128)).expect("sig"), 358 ) 359 } 360 }