The smokesignal.events web application
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}