social_helpers.rs (10838B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 format, 4 string::{String, ToString}, 5 vec, 6 vec::Vec, 7 }; 8 9 use radroots_event::{ 10 calendar::CalendarParticipant, 11 social::{SocialFarmAnchor, SocialLocation, SocialMediaDimensions, SocialMediaThumbnail}, 12 }; 13 14 use crate::error::{EventEncodeError, EventParseError}; 15 use crate::field_helpers::{push_tag, push_tag_values, validate_non_empty_field}; 16 17 pub(crate) fn validate_http_url(value: &str, field: &'static str) -> Result<(), EventEncodeError> { 18 if value.starts_with("https://") || value.starts_with("http://") { 19 validate_non_empty_field(value, field) 20 } else { 21 Err(EventEncodeError::InvalidField(field)) 22 } 23 } 24 25 pub(crate) fn push_location_tags(tags: &mut Vec<Vec<String>>, location: &SocialLocation) { 26 if let Some(name) = location 27 .name 28 .as_deref() 29 .filter(|value| !value.trim().is_empty()) 30 { 31 push_tag(tags, "location", name); 32 } 33 if let Some(geohash) = location 34 .geohash 35 .as_deref() 36 .filter(|value| !value.trim().is_empty()) 37 { 38 push_tag(tags, "g", geohash); 39 } 40 } 41 42 pub(crate) fn location_from_tags(tags: &[Vec<String>]) -> Option<SocialLocation> { 43 let name = first_tag_value(tags, "location"); 44 let geohash = first_tag_value(tags, "g"); 45 if name.is_none() && geohash.is_none() { 46 None 47 } else { 48 Some(SocialLocation { name, geohash }) 49 } 50 } 51 52 pub(crate) fn push_farm_anchor(tags: &mut Vec<Vec<String>>, farm: &SocialFarmAnchor) { 53 if farm.farm.pubkey.trim().is_empty() || farm.farm.d_tag.trim().is_empty() { 54 return; 55 } 56 let address = format!("30340:{}:{}", farm.farm.pubkey, farm.farm.d_tag); 57 push_tag(tags, "a", address); 58 } 59 60 pub(crate) fn push_participants( 61 tags: &mut Vec<Vec<String>>, 62 participants: Option<&Vec<CalendarParticipant>>, 63 ) { 64 let Some(participants) = participants else { 65 return; 66 }; 67 for participant in participants { 68 if participant.pubkey.trim().is_empty() { 69 continue; 70 } 71 let mut tag = vec!["p".to_string(), participant.pubkey.clone()]; 72 if let Some(relay) = participant.relay.as_ref() { 73 tag.push(relay.clone()); 74 } 75 if let Some(role) = participant.role.as_ref() { 76 if participant.relay.is_none() { 77 tag.push(String::new()); 78 } 79 tag.push(role.clone()); 80 } 81 tags.push(tag); 82 } 83 } 84 85 pub(crate) fn first_tag_value(tags: &[Vec<String>], key: &str) -> Option<String> { 86 tags.iter() 87 .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) 88 .and_then(|tag| tag.get(1)) 89 .filter(|value| !value.trim().is_empty()) 90 .cloned() 91 } 92 93 pub(crate) fn dimensions_tag(dimensions: &SocialMediaDimensions) -> String { 94 format!("{}x{}", dimensions.width, dimensions.height) 95 } 96 97 pub(crate) fn parse_dimensions_tag( 98 value: &str, 99 tag: &'static str, 100 ) -> Result<SocialMediaDimensions, EventParseError> { 101 let Some((width, height)) = value.split_once('x') else { 102 return Err(EventParseError::InvalidTag(tag)); 103 }; 104 let width = width 105 .parse::<u32>() 106 .map_err(|err| EventParseError::InvalidNumber(tag, err))?; 107 let height = height 108 .parse::<u32>() 109 .map_err(|err| EventParseError::InvalidNumber(tag, err))?; 110 if width == 0 || height == 0 { 111 return Err(EventParseError::InvalidTag(tag)); 112 } 113 Ok(SocialMediaDimensions { width, height }) 114 } 115 116 pub(crate) fn push_thumbnail(tags: &mut Vec<Vec<String>>, thumbnail: &SocialMediaThumbnail) { 117 if thumbnail.url.trim().is_empty() { 118 return; 119 } 120 if let Some(dimensions) = thumbnail.dimensions.as_ref() { 121 push_tag_values( 122 tags, 123 "thumb", 124 [thumbnail.url.clone(), dimensions_tag(dimensions)], 125 ); 126 } else { 127 push_tag(tags, "thumb", thumbnail.url.clone()); 128 } 129 } 130 131 #[cfg(test)] 132 mod tests { 133 use super::*; 134 135 #[test] 136 fn validates_http_urls() { 137 assert!(validate_http_url("https://example.test/file", "url").is_ok()); 138 assert!(validate_http_url("http://example.test/file", "url").is_ok()); 139 assert!(matches!( 140 validate_http_url("ftp://example.test/file", "url"), 141 Err(EventEncodeError::InvalidField("url")) 142 )); 143 } 144 145 #[test] 146 fn encodes_location_participant_and_dimensions_tags() { 147 let mut tags = Vec::new(); 148 push_location_tags( 149 &mut tags, 150 &SocialLocation { 151 name: Some("Pack shed".to_string()), 152 geohash: Some("c23nb62w20st".to_string()), 153 }, 154 ); 155 push_participants( 156 &mut tags, 157 Some(&vec![CalendarParticipant { 158 pubkey: "crew_pubkey".to_string(), 159 relay: None, 160 role: Some("participant".to_string()), 161 }]), 162 ); 163 164 let location = location_from_tags(&tags).expect("location"); 165 assert_eq!(location.name.as_deref(), Some("Pack shed")); 166 assert_eq!(location.geohash.as_deref(), Some("c23nb62w20st")); 167 let named_location = 168 location_from_tags(&[vec!["location".to_string(), "Farm gate".to_string()]]) 169 .expect("named location"); 170 assert_eq!(named_location.name.as_deref(), Some("Farm gate")); 171 assert_eq!(named_location.geohash, None); 172 let geohash_location = 173 location_from_tags(&[vec!["g".to_string(), "c23nb62w20st".to_string()]]) 174 .expect("geohash location"); 175 assert_eq!(geohash_location.name, None); 176 assert_eq!(geohash_location.geohash.as_deref(), Some("c23nb62w20st")); 177 let mut empty_tags = Vec::new(); 178 push_location_tags( 179 &mut empty_tags, 180 &SocialLocation { 181 name: Some(" ".to_string()), 182 geohash: Some(" ".to_string()), 183 }, 184 ); 185 assert!(empty_tags.is_empty()); 186 assert_eq!(location_from_tags(&empty_tags), None); 187 assert_eq!( 188 first_tag_value(&[vec!["location".to_string()]], "location"), 189 None 190 ); 191 assert_eq!( 192 first_tag_value(&[vec!["location".to_string(), " ".to_string()]], "location"), 193 None 194 ); 195 196 let mut anchor_tags = Vec::new(); 197 push_farm_anchor( 198 &mut anchor_tags, 199 &SocialFarmAnchor { 200 farm: radroots_event::farm::FarmRef { 201 pubkey: " ".to_string(), 202 d_tag: "farm-d-tag".to_string(), 203 }, 204 relays: None, 205 }, 206 ); 207 push_farm_anchor( 208 &mut anchor_tags, 209 &SocialFarmAnchor { 210 farm: radroots_event::farm::FarmRef { 211 pubkey: "farm_pubkey".to_string(), 212 d_tag: " ".to_string(), 213 }, 214 relays: None, 215 }, 216 ); 217 push_farm_anchor( 218 &mut anchor_tags, 219 &SocialFarmAnchor { 220 farm: radroots_event::farm::FarmRef { 221 pubkey: "farm_pubkey".to_string(), 222 d_tag: "farm-d-tag".to_string(), 223 }, 224 relays: None, 225 }, 226 ); 227 assert_eq!( 228 anchor_tags, 229 vec![vec![ 230 "a".to_string(), 231 "30340:farm_pubkey:farm-d-tag".to_string() 232 ]] 233 ); 234 235 let mut participant_tags = Vec::new(); 236 push_participants(&mut participant_tags, None); 237 push_participants( 238 &mut participant_tags, 239 Some(&vec![ 240 CalendarParticipant { 241 pubkey: " ".to_string(), 242 relay: None, 243 role: None, 244 }, 245 CalendarParticipant { 246 pubkey: "crew_pubkey".to_string(), 247 relay: Some("wss://relay.example.test".to_string()), 248 role: Some("host".to_string()), 249 }, 250 CalendarParticipant { 251 pubkey: "relay_only_pubkey".to_string(), 252 relay: Some("wss://relay.example.test".to_string()), 253 role: None, 254 }, 255 ]), 256 ); 257 assert_eq!( 258 participant_tags, 259 vec![ 260 vec![ 261 "p".to_string(), 262 "crew_pubkey".to_string(), 263 "wss://relay.example.test".to_string(), 264 "host".to_string() 265 ], 266 vec![ 267 "p".to_string(), 268 "relay_only_pubkey".to_string(), 269 "wss://relay.example.test".to_string() 270 ] 271 ] 272 ); 273 274 let dimensions = parse_dimensions_tag("1200x800", "dim").unwrap(); 275 assert_eq!(dimensions_tag(&dimensions), "1200x800"); 276 assert!(matches!( 277 parse_dimensions_tag("0x800", "dim"), 278 Err(EventParseError::InvalidTag("dim")) 279 )); 280 assert!(matches!( 281 parse_dimensions_tag("1200x0", "dim"), 282 Err(EventParseError::InvalidTag("dim")) 283 )); 284 assert!(matches!( 285 parse_dimensions_tag("badx800", "dim"), 286 Err(EventParseError::InvalidNumber("dim", _)) 287 )); 288 assert!(matches!( 289 parse_dimensions_tag("1200xbad", "dim"), 290 Err(EventParseError::InvalidNumber("dim", _)) 291 )); 292 293 let mut thumbnail_tags = Vec::new(); 294 push_thumbnail( 295 &mut thumbnail_tags, 296 &SocialMediaThumbnail { 297 url: " ".to_string(), 298 dimensions: None, 299 }, 300 ); 301 push_thumbnail( 302 &mut thumbnail_tags, 303 &SocialMediaThumbnail { 304 url: "https://media.example.test/thumb.jpg".to_string(), 305 dimensions: None, 306 }, 307 ); 308 push_thumbnail( 309 &mut thumbnail_tags, 310 &SocialMediaThumbnail { 311 url: "https://media.example.test/thumb-large.jpg".to_string(), 312 dimensions: Some(SocialMediaDimensions { 313 width: 320, 314 height: 240, 315 }), 316 }, 317 ); 318 assert_eq!( 319 thumbnail_tags, 320 vec![ 321 vec![ 322 "thumb".to_string(), 323 "https://media.example.test/thumb.jpg".to_string() 324 ], 325 vec![ 326 "thumb".to_string(), 327 "https://media.example.test/thumb-large.jpg".to_string(), 328 "320x240".to_string() 329 ], 330 ] 331 ); 332 } 333 }