encode.rs (6676B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 format, 4 string::{String, ToString}, 5 vec::Vec, 6 }; 7 8 use radroots_events::{ 9 comment::RadrootsComment, 10 kinds::{KIND_COMMENT, KIND_POST}, 11 social::RadrootsSocialTarget, 12 }; 13 14 use crate::error::EventEncodeError; 15 use crate::field_helpers::{ 16 parse_address_tag, validate_lowercase_hex_64, validate_non_empty_field, 17 }; 18 use crate::wire::WireEventParts; 19 20 const DEFAULT_KIND: u32 = KIND_COMMENT; 21 22 pub fn comment_build_tags(comment: &RadrootsComment) -> Result<Vec<Vec<String>>, EventEncodeError> { 23 let mut tags = Vec::with_capacity(8); 24 push_comment_target(&mut tags, &comment.root, CommentTargetTags::root())?; 25 push_comment_target(&mut tags, &comment.parent, CommentTargetTags::parent())?; 26 Ok(tags) 27 } 28 29 pub fn to_wire_parts(comment: &RadrootsComment) -> Result<WireEventParts, EventEncodeError> { 30 to_wire_parts_with_kind(comment, DEFAULT_KIND) 31 } 32 33 pub fn to_wire_parts_with_kind( 34 comment: &RadrootsComment, 35 kind: u32, 36 ) -> Result<WireEventParts, EventEncodeError> { 37 if kind != DEFAULT_KIND { 38 return Err(EventEncodeError::InvalidKind(kind)); 39 } 40 if comment.content.trim().is_empty() { 41 return Err(EventEncodeError::EmptyRequiredField("content")); 42 } 43 let tags = comment_build_tags(comment)?; 44 Ok(WireEventParts { 45 kind, 46 content: comment.content.clone(), 47 tags, 48 }) 49 } 50 51 struct CommentTargetTags { 52 event: &'static str, 53 address: &'static str, 54 external: &'static str, 55 author: &'static str, 56 kind: &'static str, 57 field: &'static str, 58 } 59 60 impl CommentTargetTags { 61 fn root() -> Self { 62 Self { 63 event: "E", 64 address: "A", 65 external: "I", 66 author: "P", 67 kind: "K", 68 field: "root", 69 } 70 } 71 72 fn parent() -> Self { 73 Self { 74 event: "e", 75 address: "a", 76 external: "i", 77 author: "p", 78 kind: "k", 79 field: "parent", 80 } 81 } 82 } 83 84 fn push_comment_target( 85 tags: &mut Vec<Vec<String>>, 86 target: &RadrootsSocialTarget, 87 keys: CommentTargetTags, 88 ) -> Result<(), EventEncodeError> { 89 match target { 90 RadrootsSocialTarget::Event { 91 id, 92 author, 93 event_kind, 94 relays, 95 } => { 96 validate_lowercase_hex_64(id, keys.field)?; 97 let author = author 98 .as_deref() 99 .ok_or(EventEncodeError::EmptyRequiredField(keys.field))?; 100 validate_non_empty_field(author, keys.field)?; 101 let kind = event_kind.ok_or(EventEncodeError::EmptyRequiredField(keys.field))?; 102 validate_comment_target_kind(kind, keys.field)?; 103 let mut event_tag = Vec::with_capacity(2 + relays.as_ref().map_or(0, Vec::len)); 104 event_tag.push(keys.event.to_string()); 105 event_tag.push(id.clone()); 106 if let Some(relays) = relays { 107 event_tag.extend(relays.iter().cloned()); 108 } 109 tags.push(event_tag); 110 tags.push(vec![keys.author.to_string(), author.to_string()]); 111 tags.push(vec![keys.kind.to_string(), kind.to_string()]); 112 } 113 RadrootsSocialTarget::Address { 114 address, 115 author, 116 event_kind, 117 relays, 118 } => { 119 let parsed = parse_address_tag(address, keys.field) 120 .map_err(|_| EventEncodeError::InvalidField(keys.field))?; 121 validate_comment_target_kind(parsed.kind, keys.field)?; 122 if let Some(kind) = event_kind 123 && *kind != parsed.kind 124 { 125 return Err(EventEncodeError::InvalidField(keys.field)); 126 } 127 if let Some(author) = author.as_deref() 128 && author != parsed.pubkey 129 { 130 return Err(EventEncodeError::InvalidField(keys.field)); 131 } 132 let mut address_tag = Vec::with_capacity(2 + relays.as_ref().map_or(0, Vec::len)); 133 address_tag.push(keys.address.to_string()); 134 address_tag.push(format!( 135 "{}:{}:{}", 136 parsed.kind, parsed.pubkey, parsed.d_tag 137 )); 138 if let Some(relays) = relays { 139 address_tag.extend(relays.iter().cloned()); 140 } 141 tags.push(address_tag); 142 tags.push(vec![keys.author.to_string(), parsed.pubkey]); 143 tags.push(vec![keys.kind.to_string(), parsed.kind.to_string()]); 144 } 145 RadrootsSocialTarget::External { 146 id, 147 external_kind, 148 hint, 149 } => { 150 validate_non_empty_field(id, keys.field)?; 151 validate_non_empty_field(external_kind, keys.field)?; 152 if external_kind == "1" { 153 return Err(EventEncodeError::InvalidField(keys.field)); 154 } 155 let mut external_tag = Vec::with_capacity(3); 156 external_tag.push(keys.external.to_string()); 157 external_tag.push(id.clone()); 158 if let Some(hint) = hint.as_deref().filter(|value| !value.trim().is_empty()) { 159 external_tag.push(hint.to_string()); 160 } 161 tags.push(external_tag); 162 tags.push(vec![keys.kind.to_string(), external_kind.clone()]); 163 } 164 } 165 Ok(()) 166 } 167 168 fn validate_comment_target_kind(kind: u32, field: &'static str) -> Result<(), EventEncodeError> { 169 if kind == KIND_POST { 170 Err(EventEncodeError::InvalidField(field)) 171 } else { 172 Ok(()) 173 } 174 } 175 176 #[cfg(test)] 177 mod tests { 178 use super::*; 179 180 #[test] 181 fn comment_targets_encode_without_relays() { 182 let author = "a".repeat(64); 183 let comment = RadrootsComment { 184 root: RadrootsSocialTarget::Event { 185 id: "b".repeat(64), 186 author: Some(author.clone()), 187 event_kind: Some(KIND_COMMENT), 188 relays: None, 189 }, 190 parent: RadrootsSocialTarget::Address { 191 address: format!("{KIND_COMMENT}:{author}:AAAAAAAAAAAAAAAAAAAAAA"), 192 author: Some(author), 193 event_kind: Some(KIND_COMMENT), 194 relays: None, 195 }, 196 content: "looks good".to_string(), 197 }; 198 199 let tags = comment_build_tags(&comment).expect("comment tags"); 200 assert!( 201 tags.iter() 202 .any(|tag| tag == &vec!["E".to_string(), "b".repeat(64)]) 203 ); 204 assert!( 205 tags.iter() 206 .any(|tag| tag.first().map(String::as_str) == Some("a") && tag.len() == 2) 207 ); 208 } 209 }