lib

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

decode.rs (9148B)


      1 #[cfg(not(feature = "std"))]
      2 use alloc::{
      3     string::{String, ToString},
      4     vec::Vec,
      5 };
      6 
      7 use radroots_events::{
      8     RadrootsNostrEvent,
      9     farm::RadrootsFarmRef,
     10     kinds::{KIND_FARM, KIND_POST},
     11     post::RadrootsPost,
     12     social::{RadrootsSocialFarmAnchor, RadrootsSocialMediaMetadata, RadrootsSocialTarget},
     13     tags::{TAG_A, TAG_IMETA, TAG_Q, TAG_T},
     14 };
     15 
     16 use crate::error::EventParseError;
     17 use crate::field_helpers::{parse_address_tag, tag_values, validate_lowercase_hex_64_tag};
     18 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent};
     19 use crate::social_helpers::{location_from_tags, parse_dimensions_tag};
     20 
     21 const DEFAULT_KIND: u32 = KIND_POST;
     22 
     23 pub fn post_from_content(kind: u32, content: &str) -> Result<RadrootsPost, EventParseError> {
     24     if kind != DEFAULT_KIND {
     25         return Err(EventParseError::InvalidKind {
     26             expected: "1",
     27             got: kind,
     28         });
     29     }
     30     if content.trim().is_empty() {
     31         return Err(EventParseError::InvalidTag("content"));
     32     }
     33     Ok(RadrootsPost {
     34         content: content.to_string(),
     35         farm: None,
     36         address_refs: None,
     37         location: None,
     38         topics: None,
     39         quote_refs: None,
     40         media: None,
     41     })
     42 }
     43 
     44 pub fn post_from_event(
     45     kind: u32,
     46     tags: &[Vec<String>],
     47     content: &str,
     48 ) -> Result<RadrootsPost, EventParseError> {
     49     let mut post = post_from_content(kind, content)?;
     50     post.farm = farm_anchor_from_tags(tags)?;
     51     post.address_refs = address_refs_from_tags(tags)?;
     52     post.location = location_from_tags(tags);
     53     post.topics = non_empty_vec(tag_values(tags, TAG_T)?);
     54     post.quote_refs = quote_refs_from_tags(tags)?;
     55     post.media = media_from_tags(tags)?;
     56     Ok(post)
     57 }
     58 
     59 pub fn data_from_event(
     60     id: String,
     61     author: String,
     62     published_at: u32,
     63     kind: u32,
     64     content: String,
     65     tags: Vec<Vec<String>>,
     66 ) -> Result<RadrootsParsedData<RadrootsPost>, EventParseError> {
     67     let post = post_from_event(kind, &tags, &content)?;
     68     Ok(RadrootsParsedData::new(
     69         id,
     70         author,
     71         published_at,
     72         kind,
     73         post,
     74     ))
     75 }
     76 
     77 pub fn parsed_from_event(
     78     id: String,
     79     author: String,
     80     published_at: u32,
     81     kind: u32,
     82     content: String,
     83     tags: Vec<Vec<String>>,
     84     sig: String,
     85 ) -> Result<RadrootsParsedEvent<RadrootsPost>, EventParseError> {
     86     let data = data_from_event(
     87         id.clone(),
     88         author.clone(),
     89         published_at,
     90         kind,
     91         content.clone(),
     92         tags.clone(),
     93     )?;
     94     Ok(RadrootsParsedEvent {
     95         event: RadrootsNostrEvent {
     96             id,
     97             author,
     98             created_at: published_at,
     99             kind,
    100             content,
    101             tags,
    102             sig,
    103         },
    104         data,
    105     })
    106 }
    107 
    108 fn farm_anchor_from_tags(
    109     tags: &[Vec<String>],
    110 ) -> Result<Option<RadrootsSocialFarmAnchor>, EventParseError> {
    111     for tag in tags
    112         .iter()
    113         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_A))
    114     {
    115         let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_A))?;
    116         let address = parse_address_tag(value, TAG_A)?;
    117         if address.kind == KIND_FARM {
    118             let relays = if tag.len() > 2 {
    119                 Some(tag[2..].to_vec())
    120             } else {
    121                 None
    122             };
    123             return Ok(Some(RadrootsSocialFarmAnchor {
    124                 farm: RadrootsFarmRef {
    125                     pubkey: address.pubkey,
    126                     d_tag: address.d_tag,
    127                 },
    128                 relays,
    129             }));
    130         }
    131     }
    132     Ok(None)
    133 }
    134 
    135 fn address_refs_from_tags(
    136     tags: &[Vec<String>],
    137 ) -> Result<Option<Vec<RadrootsSocialTarget>>, EventParseError> {
    138     let mut refs = Vec::new();
    139     for tag in tags
    140         .iter()
    141         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_A))
    142     {
    143         let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_A))?;
    144         let address = parse_address_tag(value, TAG_A)?;
    145         if address.kind == KIND_FARM {
    146             continue;
    147         }
    148         let relays = if tag.len() > 2 {
    149             Some(tag[2..].to_vec())
    150         } else {
    151             None
    152         };
    153         refs.push(RadrootsSocialTarget::Address {
    154             address: value.clone(),
    155             author: Some(address.pubkey),
    156             event_kind: Some(address.kind),
    157             relays,
    158         });
    159     }
    160     Ok(non_empty_vec(refs))
    161 }
    162 
    163 fn quote_refs_from_tags(
    164     tags: &[Vec<String>],
    165 ) -> Result<Option<Vec<RadrootsSocialTarget>>, EventParseError> {
    166     let mut refs = Vec::new();
    167     for tag in tags
    168         .iter()
    169         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_Q))
    170     {
    171         let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_Q))?;
    172         let relays = if tag.len() > 2 {
    173             Some(tag[2..].to_vec())
    174         } else {
    175             None
    176         };
    177         match parse_address_tag(value, TAG_Q) {
    178             Ok(address) => refs.push(RadrootsSocialTarget::Address {
    179                 address: value.clone(),
    180                 author: Some(address.pubkey),
    181                 event_kind: Some(address.kind),
    182                 relays,
    183             }),
    184             Err(_) => {
    185                 validate_lowercase_hex_64_tag(value, TAG_Q)?;
    186                 refs.push(RadrootsSocialTarget::Event {
    187                     id: value.clone(),
    188                     author: None,
    189                     event_kind: None,
    190                     relays,
    191                 });
    192             }
    193         }
    194     }
    195     Ok(non_empty_vec(refs))
    196 }
    197 
    198 fn media_from_tags(
    199     tags: &[Vec<String>],
    200 ) -> Result<Option<Vec<RadrootsSocialMediaMetadata>>, EventParseError> {
    201     let mut media = Vec::new();
    202     for tag in tags
    203         .iter()
    204         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_IMETA))
    205     {
    206         if tag.len() < 2 {
    207             return Err(EventParseError::InvalidTag(TAG_IMETA));
    208         }
    209         let raw = tag[1..].to_vec();
    210         if raw.iter().any(|value| value.trim().is_empty()) {
    211             return Err(EventParseError::InvalidTag(TAG_IMETA));
    212         }
    213         let mut item = RadrootsSocialMediaMetadata {
    214             imeta: Some(vec![raw.clone()]),
    215             ..RadrootsSocialMediaMetadata::default()
    216         };
    217         for entry in raw {
    218             parse_imeta_entry(&mut item, &entry)?;
    219         }
    220         media.push(item);
    221     }
    222     Ok(non_empty_vec(media))
    223 }
    224 
    225 fn parse_imeta_entry(
    226     item: &mut RadrootsSocialMediaMetadata,
    227     entry: &str,
    228 ) -> Result<(), EventParseError> {
    229     let Some((key, value)) = entry.split_once(' ') else {
    230         return Err(EventParseError::InvalidTag(TAG_IMETA));
    231     };
    232     if value.trim().is_empty() {
    233         return Err(EventParseError::InvalidTag(TAG_IMETA));
    234     }
    235     match key {
    236         "url" => item.url = Some(value.to_string()),
    237         "m" => item.mime_type = Some(value.to_string()),
    238         "x" => item.sha256 = Some(value.to_string()),
    239         "ox" => item.original_sha256 = Some(value.to_string()),
    240         "size" => {
    241             item.size = Some(
    242                 value
    243                     .parse::<u64>()
    244                     .map_err(|err| EventParseError::InvalidNumber(TAG_IMETA, err))?,
    245             );
    246         }
    247         "dim" => item.dimensions = Some(parse_dimensions_tag(value, TAG_IMETA)?),
    248         "blurhash" => item.blurhash = Some(value.to_string()),
    249         "image" => item.image = Some(value.to_string()),
    250         "summary" => item.summary = Some(value.to_string()),
    251         "alt" => item.alt = Some(value.to_string()),
    252         "fallback" => item.fallback = Some(value.to_string()),
    253         "magnet" => item.magnet = Some(value.to_string()),
    254         "i" => push_repeated_value(&mut item.content_hashes, value),
    255         "service" => push_repeated_value(&mut item.services, value),
    256         "thumb" => {}
    257         _ => {}
    258     }
    259     Ok(())
    260 }
    261 
    262 fn push_repeated_value(values: &mut Option<Vec<String>>, value: &str) {
    263     values.get_or_insert_with(Vec::new).push(value.to_string());
    264 }
    265 
    266 fn non_empty_vec<T>(values: Vec<T>) -> Option<Vec<T>> {
    267     if values.is_empty() {
    268         None
    269     } else {
    270         Some(values)
    271     }
    272 }
    273 
    274 #[cfg(test)]
    275 mod tests {
    276     use super::*;
    277 
    278     #[test]
    279     fn post_decode_accepts_address_ref_without_relays_and_unknown_imeta_keys() {
    280         let author = "a".repeat(64);
    281         let post = post_from_event(
    282             DEFAULT_KIND,
    283             &[
    284                 vec![
    285                     TAG_A.to_string(),
    286                     format!("30023:{author}:AAAAAAAAAAAAAAAAAAAAAA"),
    287                 ],
    288                 vec![
    289                     TAG_IMETA.to_string(),
    290                     "url https://media.example.invalid/a.jpg".to_string(),
    291                     "custom value".to_string(),
    292                 ],
    293             ],
    294             "fresh carrots",
    295         )
    296         .expect("post");
    297 
    298         let refs = post.address_refs.expect("address refs");
    299         assert!(matches!(
    300             &refs[0],
    301             RadrootsSocialTarget::Address { relays: None, .. }
    302         ));
    303         let media = post.media.expect("media");
    304         assert_eq!(
    305             media[0].url.as_deref(),
    306             Some("https://media.example.invalid/a.jpg")
    307         );
    308     }
    309 }