The smokesignal.events web application
at main 342 lines 11 kB view raw
1use atproto_record::lexicon::app::bsky::richtext::facet::FacetFeature; 2use atproto_record::lexicon::community::lexicon::calendar::event::{Event, EventLink}; 3use atproto_record::lexicon::community::lexicon::location::{ 4 Address, Geo, LocationOrRef, TypedAddress, TypedGeo, 5}; 6use atproto_record::typed::TypedLexicon; 7 8/// Helper function to create a LocationOrRef from an Address 9pub fn location_from_address(address: Address) -> LocationOrRef { 10 LocationOrRef::InlineAddress(TypedAddress::new(address)) 11} 12 13/// Helper function to create a LocationOrRef from a Geo 14pub fn location_from_geo(geo: Geo) -> LocationOrRef { 15 LocationOrRef::InlineGeo(TypedGeo::new(geo)) 16} 17 18/// Helper function to create a TypedEventLink from an EventLink 19pub fn typed_event_link(link: EventLink) -> TypedLexicon<EventLink> { 20 TypedLexicon::new(link) 21} 22 23/// Extract hashtags from an Event's facets 24/// 25/// This function iterates through all facets in the event and extracts 26/// tag features (hashtags), returning them as a vector of strings. 27/// 28/// # Arguments 29/// * `event` - The event to extract hashtags from 30/// 31/// # Returns 32/// A vector of hashtag strings (without the # prefix). Returns an empty 33/// vector if there are no facets or no tag features. 34/// 35/// # Example 36/// ```ignore 37/// let tags = get_event_hashtags(&event); 38/// // tags might be vec!["rust", "golang", "meetup"] 39/// ``` 40pub fn get_event_hashtags(event: &Event) -> Vec<String> { 41 event 42 .facets 43 .as_ref() 44 .map(|facets| { 45 facets 46 .iter() 47 .flat_map(|facet| { 48 facet.features.iter().filter_map(|feature| { 49 if let FacetFeature::Tag(tag) = feature { 50 Some(tag.tag.clone()) 51 } else { 52 None 53 } 54 }) 55 }) 56 .collect() 57 }) 58 .unwrap_or_default() 59} 60 61/// Extract hashtags from a Profile's facets 62/// 63/// This function iterates through all facets in the profile description and extracts 64/// tag features (hashtags), returning them as a vector of strings. 65/// 66/// # Arguments 67/// * `profile` - The profile to extract hashtags from 68/// 69/// # Returns 70/// A vector of hashtag strings (without the # prefix). Returns an empty 71/// vector if there are no facets or no tag features. 72/// 73/// # Example 74/// ```ignore 75/// let tags = get_profile_hashtags(&profile); 76/// // tags might be vec!["rust", "developer", "opensource"] 77/// ``` 78pub fn get_profile_hashtags(profile: &crate::atproto::lexicon::profile::Profile) -> Vec<String> { 79 profile 80 .facets 81 .as_ref() 82 .map(|facets| { 83 facets 84 .iter() 85 .flat_map(|facet| { 86 facet.features.iter().filter_map(|feature| { 87 if let FacetFeature::Tag(tag) = feature { 88 Some(tag.tag.clone()) 89 } else { 90 None 91 } 92 }) 93 }) 94 .collect() 95 }) 96 .unwrap_or_default() 97} 98 99#[cfg(test)] 100mod tests { 101 use super::*; 102 use atproto_record::lexicon::app::bsky::richtext::facet::{ 103 ByteSlice, Facet, Link, Mention, Tag, 104 }; 105 use std::collections::HashMap; 106 107 #[test] 108 fn test_get_event_hashtags_with_tags() { 109 let event = Event { 110 name: "Test Event".to_string(), 111 description: "Event with #rust #golang tags".to_string(), 112 created_at: chrono::Utc::now(), 113 starts_at: None, 114 ends_at: None, 115 mode: None, 116 status: None, 117 locations: vec![], 118 uris: vec![], 119 media: vec![], 120 facets: Some(vec![ 121 Facet { 122 index: ByteSlice { 123 byte_start: 11, 124 byte_end: 16, 125 }, 126 features: vec![FacetFeature::Tag(Tag { 127 tag: "rust".to_string(), 128 })], 129 }, 130 Facet { 131 index: ByteSlice { 132 byte_start: 17, 133 byte_end: 24, 134 }, 135 features: vec![FacetFeature::Tag(Tag { 136 tag: "golang".to_string(), 137 })], 138 }, 139 ]), 140 extra: HashMap::new(), 141 }; 142 143 let tags = get_event_hashtags(&event); 144 assert_eq!(tags, vec!["rust", "golang"]); 145 } 146 147 #[test] 148 fn test_get_event_hashtags_no_facets() { 149 let event = Event { 150 name: "Test Event".to_string(), 151 description: "Event without tags".to_string(), 152 created_at: chrono::Utc::now(), 153 starts_at: None, 154 ends_at: None, 155 mode: None, 156 status: None, 157 locations: vec![], 158 uris: vec![], 159 media: vec![], 160 facets: None, 161 extra: HashMap::new(), 162 }; 163 164 let tags = get_event_hashtags(&event); 165 assert_eq!(tags, Vec::<String>::new()); 166 } 167 168 #[test] 169 fn test_get_event_hashtags_mixed_facets() { 170 use atproto_record::lexicon::app::bsky::richtext::facet::{Link, Mention}; 171 172 let event = Event { 173 name: "Test Event".to_string(), 174 description: "Event with @user https://example.com #rust".to_string(), 175 created_at: chrono::Utc::now(), 176 starts_at: None, 177 ends_at: None, 178 mode: None, 179 status: None, 180 locations: vec![], 181 uris: vec![], 182 media: vec![], 183 facets: Some(vec![ 184 Facet { 185 index: ByteSlice { 186 byte_start: 11, 187 byte_end: 16, 188 }, 189 features: vec![FacetFeature::Mention(Mention { 190 did: "did:plc:test123".to_string(), 191 })], 192 }, 193 Facet { 194 index: ByteSlice { 195 byte_start: 17, 196 byte_end: 36, 197 }, 198 features: vec![FacetFeature::Link(Link { 199 uri: "https://example.com".to_string(), 200 })], 201 }, 202 Facet { 203 index: ByteSlice { 204 byte_start: 37, 205 byte_end: 42, 206 }, 207 features: vec![FacetFeature::Tag(Tag { 208 tag: "rust".to_string(), 209 })], 210 }, 211 ]), 212 extra: HashMap::new(), 213 }; 214 215 let tags = get_event_hashtags(&event); 216 // Should only extract the tag, not the mention or link 217 assert_eq!(tags, vec!["rust"]); 218 } 219 220 #[test] 221 fn test_get_event_hashtags_empty_facets() { 222 let event = Event { 223 name: "Test Event".to_string(), 224 description: "Event with empty facets".to_string(), 225 created_at: chrono::Utc::now(), 226 starts_at: None, 227 ends_at: None, 228 mode: None, 229 status: None, 230 locations: vec![], 231 uris: vec![], 232 media: vec![], 233 facets: Some(vec![]), 234 extra: HashMap::new(), 235 }; 236 237 let tags = get_event_hashtags(&event); 238 assert_eq!(tags, Vec::<String>::new()); 239 } 240 241 #[test] 242 fn test_get_profile_hashtags_with_tags() { 243 use crate::atproto::lexicon::profile::Profile; 244 245 let profile = Profile { 246 display_name: Some("Test User".to_string()), 247 description: Some("I love #rust and #opensource".to_string()), 248 profile_host: None, 249 facets: Some(vec![ 250 Facet { 251 index: ByteSlice { 252 byte_start: 7, 253 byte_end: 12, 254 }, 255 features: vec![FacetFeature::Tag(Tag { 256 tag: "rust".to_string(), 257 })], 258 }, 259 Facet { 260 index: ByteSlice { 261 byte_start: 17, 262 byte_end: 28, 263 }, 264 features: vec![FacetFeature::Tag(Tag { 265 tag: "opensource".to_string(), 266 })], 267 }, 268 ]), 269 avatar: None, 270 banner: None, 271 extra: HashMap::new(), 272 }; 273 274 let tags = get_profile_hashtags(&profile); 275 assert_eq!(tags, vec!["rust", "opensource"]); 276 } 277 278 #[test] 279 fn test_get_profile_hashtags_no_facets() { 280 use crate::atproto::lexicon::profile::Profile; 281 282 let profile = Profile { 283 display_name: Some("Test User".to_string()), 284 description: Some("Just a regular description".to_string()), 285 profile_host: None, 286 facets: None, 287 avatar: None, 288 banner: None, 289 extra: HashMap::new(), 290 }; 291 292 let tags = get_profile_hashtags(&profile); 293 assert_eq!(tags, Vec::<String>::new()); 294 } 295 296 #[test] 297 fn test_get_profile_hashtags_mixed_facets() { 298 use crate::atproto::lexicon::profile::Profile; 299 300 let profile = Profile { 301 display_name: Some("Test User".to_string()), 302 description: Some("Follow @user at https://example.com #rust".to_string()), 303 profile_host: None, 304 facets: Some(vec![ 305 Facet { 306 index: ByteSlice { 307 byte_start: 7, 308 byte_end: 12, 309 }, 310 features: vec![FacetFeature::Mention(Mention { 311 did: "did:plc:test123".to_string(), 312 })], 313 }, 314 Facet { 315 index: ByteSlice { 316 byte_start: 16, 317 byte_end: 35, 318 }, 319 features: vec![FacetFeature::Link(Link { 320 uri: "https://example.com".to_string(), 321 })], 322 }, 323 Facet { 324 index: ByteSlice { 325 byte_start: 36, 326 byte_end: 41, 327 }, 328 features: vec![FacetFeature::Tag(Tag { 329 tag: "rust".to_string(), 330 })], 331 }, 332 ]), 333 avatar: None, 334 banner: None, 335 extra: HashMap::new(), 336 }; 337 338 let tags = get_profile_hashtags(&profile); 339 // Should only extract the tag, not the mention or link 340 assert_eq!(tags, vec!["rust"]); 341 } 342}