lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }