lib

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

decode.rs (8230B)


      1 #[cfg(not(feature = "std"))]
      2 use alloc::{
      3     string::{String, ToString},
      4     vec::Vec,
      5 };
      6 
      7 use radroots_events::{
      8     RadrootsNostrEvent,
      9     comment::RadrootsComment,
     10     kinds::{KIND_COMMENT, KIND_POST},
     11     social::RadrootsSocialTarget,
     12     tags::{TAG_E_PREV, TAG_E_ROOT},
     13 };
     14 
     15 use crate::error::EventParseError;
     16 use crate::field_helpers::{parse_address_tag, validate_lowercase_hex_64_tag};
     17 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent};
     18 
     19 const DEFAULT_KIND: u32 = KIND_COMMENT;
     20 
     21 pub fn comment_from_tags(
     22     kind: u32,
     23     tags: &[Vec<String>],
     24     content: &str,
     25 ) -> Result<RadrootsComment, EventParseError> {
     26     if kind != DEFAULT_KIND {
     27         return Err(EventParseError::InvalidKind {
     28             expected: "1111",
     29             got: kind,
     30         });
     31     }
     32     if content.trim().is_empty() {
     33         return Err(EventParseError::InvalidTag("content"));
     34     }
     35     if tags
     36         .iter()
     37         .any(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_ROOT))
     38     {
     39         return Err(EventParseError::InvalidTag(TAG_E_ROOT));
     40     }
     41     if tags
     42         .iter()
     43         .any(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_PREV))
     44     {
     45         return Err(EventParseError::InvalidTag(TAG_E_PREV));
     46     }
     47 
     48     let root = parse_comment_target(tags, CommentTargetTags::root())?;
     49     let parent = parse_comment_target(tags, CommentTargetTags::parent())?;
     50 
     51     Ok(RadrootsComment {
     52         root,
     53         parent,
     54         content: content.to_string(),
     55     })
     56 }
     57 
     58 struct CommentTargetTags {
     59     event: &'static str,
     60     address: &'static str,
     61     external: &'static str,
     62     author: &'static str,
     63     kind: &'static str,
     64 }
     65 
     66 impl CommentTargetTags {
     67     fn root() -> Self {
     68         Self {
     69             event: "E",
     70             address: "A",
     71             external: "I",
     72             author: "P",
     73             kind: "K",
     74         }
     75     }
     76 
     77     fn parent() -> Self {
     78         Self {
     79             event: "e",
     80             address: "a",
     81             external: "i",
     82             author: "p",
     83             kind: "k",
     84         }
     85     }
     86 }
     87 
     88 fn parse_comment_target(
     89     tags: &[Vec<String>],
     90     keys: CommentTargetTags,
     91 ) -> Result<RadrootsSocialTarget, EventParseError> {
     92     let event_tag = find_tag(tags, keys.event);
     93     let address_tag = find_tag(tags, keys.address);
     94     let external_tag = find_tag(tags, keys.external);
     95     let target_count = usize::from(event_tag.is_some())
     96         + usize::from(address_tag.is_some())
     97         + usize::from(external_tag.is_some());
     98     if target_count > 1 {
     99         return Err(EventParseError::InvalidTag(keys.event));
    100     }
    101 
    102     match (event_tag, address_tag, external_tag) {
    103         (Some(tag), None, None) => {
    104             let id = tag
    105                 .get(1)
    106                 .cloned()
    107                 .ok_or(EventParseError::InvalidTag(keys.event))?;
    108             validate_lowercase_hex_64_tag(&id, keys.event)?;
    109             let kind = required_numeric_kind(tags, keys.kind)?;
    110             validate_comment_target_kind(kind, keys.kind)?;
    111             let author = required_author(tags, keys.author)?;
    112             let relays = if tag.len() > 2 {
    113                 Some(tag[2..].to_vec())
    114             } else {
    115                 None
    116             };
    117             Ok(RadrootsSocialTarget::Event {
    118                 id,
    119                 author: Some(author),
    120                 event_kind: Some(kind),
    121                 relays,
    122             })
    123         }
    124         (None, Some(tag), None) => {
    125             let value = tag
    126                 .get(1)
    127                 .cloned()
    128                 .ok_or(EventParseError::InvalidTag(keys.address))?;
    129             let address = parse_address_tag(&value, keys.address)?;
    130             let kind = required_numeric_kind(tags, keys.kind)?;
    131             validate_comment_target_kind(kind, keys.kind)?;
    132             if kind != address.kind {
    133                 return Err(EventParseError::InvalidTag(keys.kind));
    134             }
    135             let author = required_author(tags, keys.author)?;
    136             if author != address.pubkey {
    137                 return Err(EventParseError::InvalidTag(keys.author));
    138             }
    139             let relays = if tag.len() > 2 {
    140                 Some(tag[2..].to_vec())
    141             } else {
    142                 None
    143             };
    144             Ok(RadrootsSocialTarget::Address {
    145                 address: value,
    146                 author: Some(author),
    147                 event_kind: Some(kind),
    148                 relays,
    149             })
    150         }
    151         (None, None, Some(tag)) => {
    152             let id = tag
    153                 .get(1)
    154                 .cloned()
    155                 .ok_or(EventParseError::InvalidTag(keys.external))?;
    156             if id.trim().is_empty() {
    157                 return Err(EventParseError::InvalidTag(keys.external));
    158             }
    159             let external_kind = required_kind_value(tags, keys.kind)?;
    160             if external_kind == "1" {
    161                 return Err(EventParseError::InvalidTag(keys.kind));
    162             }
    163             let hint = tag.get(2).filter(|value| !value.trim().is_empty()).cloned();
    164             Ok(RadrootsSocialTarget::External {
    165                 id,
    166                 external_kind,
    167                 hint,
    168             })
    169         }
    170         _ => Err(EventParseError::MissingTag(keys.event)),
    171     }
    172 }
    173 
    174 fn validate_comment_target_kind(kind: u32, key: &'static str) -> Result<(), EventParseError> {
    175     if kind == KIND_POST {
    176         Err(EventParseError::InvalidTag(key))
    177     } else {
    178         Ok(())
    179     }
    180 }
    181 
    182 fn find_tag<'a>(tags: &'a [Vec<String>], key: &'static str) -> Option<&'a Vec<String>> {
    183     tags.iter()
    184         .find(|tag| tag.first().map(|value| value.as_str()) == Some(key))
    185 }
    186 
    187 fn required_author(tags: &[Vec<String>], key: &'static str) -> Result<String, EventParseError> {
    188     let value = find_tag(tags, key)
    189         .and_then(|tag| tag.get(1))
    190         .cloned()
    191         .ok_or(EventParseError::MissingTag(key))?;
    192     if value.trim().is_empty() {
    193         return Err(EventParseError::InvalidTag(key));
    194     }
    195     Ok(value)
    196 }
    197 
    198 fn required_kind_value(tags: &[Vec<String>], key: &'static str) -> Result<String, EventParseError> {
    199     let value = find_tag(tags, key)
    200         .and_then(|tag| tag.get(1))
    201         .cloned()
    202         .ok_or(EventParseError::MissingTag(key))?;
    203     if value.trim().is_empty() {
    204         return Err(EventParseError::InvalidTag(key));
    205     }
    206     Ok(value)
    207 }
    208 
    209 fn required_numeric_kind(tags: &[Vec<String>], key: &'static str) -> Result<u32, EventParseError> {
    210     required_kind_value(tags, key)?
    211         .parse::<u32>()
    212         .map_err(|err| EventParseError::InvalidNumber(key, err))
    213 }
    214 
    215 pub fn data_from_event(
    216     id: String,
    217     author: String,
    218     published_at: u32,
    219     kind: u32,
    220     content: String,
    221     tags: Vec<Vec<String>>,
    222 ) -> Result<RadrootsParsedData<RadrootsComment>, EventParseError> {
    223     let comment = comment_from_tags(kind, &tags, &content)?;
    224     Ok(RadrootsParsedData::new(
    225         id,
    226         author,
    227         published_at,
    228         kind,
    229         comment,
    230     ))
    231 }
    232 
    233 pub fn parsed_from_event(
    234     id: String,
    235     author: String,
    236     published_at: u32,
    237     kind: u32,
    238     content: String,
    239     tags: Vec<Vec<String>>,
    240     sig: String,
    241 ) -> Result<RadrootsParsedEvent<RadrootsComment>, EventParseError> {
    242     let data = data_from_event(
    243         id.clone(),
    244         author.clone(),
    245         published_at,
    246         kind,
    247         content.clone(),
    248         tags.clone(),
    249     )?;
    250     Ok(RadrootsParsedEvent {
    251         event: RadrootsNostrEvent {
    252             id,
    253             author,
    254             created_at: published_at,
    255             kind,
    256             content,
    257             tags,
    258             sig,
    259         },
    260         data,
    261     })
    262 }
    263 
    264 #[cfg(test)]
    265 mod tests {
    266     use super::*;
    267 
    268     #[test]
    269     fn comment_decode_rejects_non_numeric_target_kind() {
    270         let err = comment_from_tags(
    271             DEFAULT_KIND,
    272             &[
    273                 vec!["E".to_string(), "a".repeat(64)],
    274                 vec!["P".to_string(), "b".repeat(64)],
    275                 vec!["K".to_string(), "kind".to_string()],
    276                 vec!["i".to_string(), "external-id".to_string()],
    277                 vec!["k".to_string(), "web".to_string()],
    278             ],
    279             "content",
    280         )
    281         .expect_err("non-numeric kind");
    282 
    283         assert!(matches!(err, EventParseError::InvalidNumber("K", _)));
    284     }
    285 }