tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

metadata.rs (10624B)


      1 use std::collections::BTreeSet;
      2 
      3 use crate::{
      4     GroupLimitsConfig,
      5     errors::{GroupError, GroupErrorKind},
      6     event_view::{GroupEventTag, GroupEventView},
      7 };
      8 use tangle_protocol::Kind;
      9 
     10 pub const MAX_METADATA_NAME_BYTES: usize = 128;
     11 pub const MAX_METADATA_PICTURE_BYTES: usize = 2_048;
     12 pub const MAX_METADATA_ABOUT_BYTES: usize = 4_096;
     13 
     14 #[derive(Debug, Clone, PartialEq, Eq)]
     15 pub struct GroupMetadata {
     16     name: Option<String>,
     17     picture: Option<String>,
     18     about: Option<String>,
     19     private: bool,
     20     restricted: bool,
     21     hidden: bool,
     22     closed: bool,
     23     supported_kinds: SupportedKinds,
     24 }
     25 
     26 impl GroupMetadata {
     27     pub fn from_parts(
     28         text: GroupMetadataText,
     29         flags: GroupMetadataFlags,
     30         supported_kinds: SupportedKinds,
     31     ) -> Self {
     32         Self {
     33             name: text.name,
     34             picture: text.picture,
     35             about: text.about,
     36             private: flags.private,
     37             restricted: flags.restricted,
     38             hidden: flags.hidden,
     39             closed: flags.closed,
     40             supported_kinds,
     41         }
     42     }
     43 
     44     pub fn empty() -> Self {
     45         Self {
     46             name: None,
     47             picture: None,
     48             about: None,
     49             private: false,
     50             restricted: false,
     51             hidden: false,
     52             closed: false,
     53             supported_kinds: SupportedKinds::UnspecifiedAll,
     54         }
     55     }
     56 
     57     pub fn name(&self) -> Option<&str> {
     58         self.name.as_deref()
     59     }
     60 
     61     pub fn picture(&self) -> Option<&str> {
     62         self.picture.as_deref()
     63     }
     64 
     65     pub fn about(&self) -> Option<&str> {
     66         self.about.as_deref()
     67     }
     68 
     69     pub fn private(&self) -> bool {
     70         self.private
     71     }
     72 
     73     pub fn restricted(&self) -> bool {
     74         self.restricted
     75     }
     76 
     77     pub fn hidden(&self) -> bool {
     78         self.hidden
     79     }
     80 
     81     pub fn closed(&self) -> bool {
     82         self.closed
     83     }
     84 
     85     pub fn supported_kinds(&self) -> &SupportedKinds {
     86         &self.supported_kinds
     87     }
     88 }
     89 
     90 #[derive(Debug, Clone, PartialEq, Eq)]
     91 pub struct GroupMetadataText {
     92     name: Option<String>,
     93     picture: Option<String>,
     94     about: Option<String>,
     95 }
     96 
     97 impl GroupMetadataText {
     98     pub fn new(name: Option<String>, picture: Option<String>, about: Option<String>) -> Self {
     99         Self {
    100             name,
    101             picture,
    102             about,
    103         }
    104     }
    105 
    106     pub fn empty() -> Self {
    107         Self {
    108             name: None,
    109             picture: None,
    110             about: None,
    111         }
    112     }
    113 }
    114 
    115 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    116 pub struct GroupMetadataFlags {
    117     private: bool,
    118     restricted: bool,
    119     hidden: bool,
    120     closed: bool,
    121 }
    122 
    123 impl GroupMetadataFlags {
    124     pub fn new(private: bool, restricted: bool, hidden: bool, closed: bool) -> Self {
    125         Self {
    126             private,
    127             restricted,
    128             hidden,
    129             closed,
    130         }
    131     }
    132 }
    133 
    134 #[derive(Debug, Clone, PartialEq, Eq)]
    135 pub enum SupportedKinds {
    136     UnspecifiedAll,
    137     None,
    138     Only(BTreeSet<Kind>),
    139 }
    140 
    141 pub fn parse_group_metadata(
    142     event: &(impl GroupEventView + ?Sized),
    143     limits: GroupLimitsConfig,
    144 ) -> Result<GroupMetadata, GroupError> {
    145     let mut builder = MetadataBuilder::default();
    146     event.visit_tags(|tag| {
    147         let Some(name) = tag.first_value() else {
    148             return Ok(());
    149         };
    150         match name {
    151             "name" => builder.name = parse_text_tag(&tag, "name", MAX_METADATA_NAME_BYTES)?,
    152             "picture" => {
    153                 builder.picture = parse_text_tag(&tag, "picture", MAX_METADATA_PICTURE_BYTES)?
    154             }
    155             "about" => builder.about = parse_text_tag(&tag, "about", MAX_METADATA_ABOUT_BYTES)?,
    156             "private" => builder.private = true,
    157             "restricted" => builder.restricted = true,
    158             "hidden" => builder.hidden = true,
    159             "closed" => builder.closed = true,
    160             "supported_kinds" => {
    161                 if builder.supported_kinds.is_some() {
    162                     return Err(GroupError::invalid(
    163                         GroupErrorKind::TooManySupportedKinds,
    164                         "metadata must contain at most one supported_kinds tag",
    165                     ));
    166                 }
    167                 builder.supported_kinds = Some(parse_supported_kinds_tag(&tag, limits)?);
    168             }
    169             _ => {}
    170         }
    171         Ok(())
    172     })?;
    173     Ok(GroupMetadata {
    174         name: builder.name,
    175         picture: builder.picture,
    176         about: builder.about,
    177         private: builder.private,
    178         restricted: builder.restricted,
    179         hidden: builder.hidden,
    180         closed: builder.closed,
    181         supported_kinds: builder
    182             .supported_kinds
    183             .unwrap_or(SupportedKinds::UnspecifiedAll),
    184     })
    185 }
    186 
    187 #[derive(Default)]
    188 struct MetadataBuilder {
    189     name: Option<String>,
    190     picture: Option<String>,
    191     about: Option<String>,
    192     private: bool,
    193     restricted: bool,
    194     hidden: bool,
    195     closed: bool,
    196     supported_kinds: Option<SupportedKinds>,
    197 }
    198 
    199 fn parse_text_tag(
    200     tag: &GroupEventTag<'_>,
    201     field: &'static str,
    202     max_bytes: usize,
    203 ) -> Result<Option<String>, GroupError> {
    204     let value = tag.value(1).map(str::to_owned);
    205     if let Some(value) = &value
    206         && value.len() > max_bytes
    207     {
    208         return Err(GroupError::invalid(
    209             GroupErrorKind::MetadataTooLarge,
    210             format!("metadata {field} must be at most {max_bytes} bytes"),
    211         ));
    212     }
    213     Ok(value)
    214 }
    215 
    216 fn parse_supported_kinds_tag(
    217     tag: &GroupEventTag<'_>,
    218     limits: GroupLimitsConfig,
    219 ) -> Result<SupportedKinds, GroupError> {
    220     let values = tag.values().iter().skip(1).copied().collect::<Vec<_>>();
    221     if values.is_empty() {
    222         return Ok(SupportedKinds::None);
    223     }
    224     let max = usize::from(limits.max_supported_kinds());
    225     if values.len() > max {
    226         return Err(GroupError::invalid(
    227             GroupErrorKind::TooManySupportedKinds,
    228             format!(
    229                 "supported_kinds has {} values, maximum is {max}",
    230                 values.len()
    231             ),
    232         ));
    233     }
    234     let mut kinds = BTreeSet::new();
    235     for value in values {
    236         let raw = value.parse::<u64>().map_err(|_| {
    237             GroupError::invalid(
    238                 GroupErrorKind::UnsupportedGroupKind,
    239                 "supported_kinds values must be unsigned integers",
    240             )
    241         })?;
    242         kinds.insert(Kind::new(raw).map_err(|reason| {
    243             GroupError::invalid(
    244                 GroupErrorKind::UnsupportedGroupKind,
    245                 format!("supported_kinds value is invalid: {reason}"),
    246             )
    247         })?);
    248     }
    249     Ok(SupportedKinds::Only(kinds))
    250 }
    251 
    252 #[cfg(test)]
    253 mod tests {
    254     use std::collections::BTreeSet;
    255 
    256     use super::{SupportedKinds, parse_group_metadata};
    257     use crate::{GroupErrorKind, GroupLimitsConfig};
    258     use tangle_protocol::{
    259         Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent,
    260     };
    261 
    262     #[test]
    263     fn parses_group_metadata_flags_and_fields() {
    264         let metadata = parse_group_metadata(
    265             &event(vec![
    266                 Tag::from_parts("name", &["Farmers"]).expect("name"),
    267                 Tag::from_parts("picture", &["https://radroots.test/group.png"]).expect("picture"),
    268                 Tag::from_parts("about", &["Local harvest coordination"]).expect("about"),
    269                 Tag::from_parts("private", &[]).expect("private"),
    270                 Tag::from_parts("restricted", &[]).expect("restricted"),
    271                 Tag::from_parts("hidden", &[]).expect("hidden"),
    272                 Tag::from_parts("closed", &[]).expect("closed"),
    273                 Tag::from_parts("supported_kinds", &["1", "7"]).expect("supported"),
    274             ]),
    275             GroupLimitsConfig::default(),
    276         )
    277         .expect("metadata");
    278 
    279         assert_eq!(metadata.name(), Some("Farmers"));
    280         assert_eq!(metadata.picture(), Some("https://radroots.test/group.png"));
    281         assert_eq!(metadata.about(), Some("Local harvest coordination"));
    282         assert!(metadata.private());
    283         assert!(metadata.restricted());
    284         assert!(metadata.hidden());
    285         assert!(metadata.closed());
    286         assert_eq!(
    287             metadata.supported_kinds(),
    288             &SupportedKinds::Only(BTreeSet::from([
    289                 Kind::new(1).expect("kind"),
    290                 Kind::new(7).expect("kind")
    291             ]))
    292         );
    293     }
    294 
    295     #[test]
    296     fn supported_kinds_absent_empty_and_list_forms_are_distinct() {
    297         assert_eq!(
    298             parse_group_metadata(&event(Vec::new()), GroupLimitsConfig::default())
    299                 .expect("absent")
    300                 .supported_kinds(),
    301             &SupportedKinds::UnspecifiedAll
    302         );
    303         assert_eq!(
    304             parse_group_metadata(
    305                 &event(vec![
    306                     Tag::from_parts("supported_kinds", &[]).expect("supported")
    307                 ]),
    308                 GroupLimitsConfig::default()
    309             )
    310             .expect("empty")
    311             .supported_kinds(),
    312             &SupportedKinds::None
    313         );
    314         assert!(matches!(
    315             parse_group_metadata(
    316                 &event(vec![Tag::from_parts("supported_kinds", &["1"]).expect("supported")]),
    317                 GroupLimitsConfig::default()
    318             )
    319             .expect("list")
    320             .supported_kinds(),
    321             SupportedKinds::Only(kinds) if kinds.contains(&Kind::new(1).expect("kind"))
    322         ));
    323     }
    324 
    325     #[test]
    326     fn metadata_parser_rejects_oversize_fields_and_kind_limits() {
    327         let error = parse_group_metadata(
    328             &event(vec![
    329                 Tag::from_parts("name", &[&"a".repeat(129)]).expect("name"),
    330             ]),
    331             GroupLimitsConfig::default(),
    332         )
    333         .expect_err("name");
    334         assert_eq!(error.kind(), GroupErrorKind::MetadataTooLarge);
    335 
    336         let limits = GroupLimitsConfig::new(128, 8, 1, 1, 1).expect("limits");
    337         let error = parse_group_metadata(
    338             &event(vec![
    339                 Tag::from_parts("supported_kinds", &["1", "2"]).expect("supported"),
    340             ]),
    341             limits,
    342         )
    343         .expect_err("supported kinds");
    344         assert_eq!(error.kind(), GroupErrorKind::TooManySupportedKinds);
    345     }
    346 
    347     fn event(tags: Vec<Tag>) -> Event {
    348         Event::new(
    349             EventId::new(&"0".repeat(64)).expect("id"),
    350             UnsignedEvent::new(
    351                 PublicKeyHex::new(&"1".repeat(64)).expect("pubkey"),
    352                 UnixTimestamp::new(1),
    353                 Kind::new(1).expect("kind"),
    354                 tags,
    355                 "",
    356             ),
    357             SignatureHex::new(&"2".repeat(128)).expect("sig"),
    358         )
    359     }
    360 }