The smokesignal.events web application
at main 149 lines 4.3 kB view raw
1use crate::http::errors::UrlError; 2use atproto_record::lexicon::community::lexicon::calendar::event::NSID; 3 4pub(crate) type QueryParam<'a> = (&'a str, &'a str); 5pub(crate) type QueryParams<'a> = Vec<QueryParam<'a>>; 6 7pub(crate) fn stringify(query: QueryParams) -> String { 8 query.iter().fold(String::new(), |acc, &tuple| { 9 acc + tuple.0 + "=" + tuple.1 + "&" 10 }) 11} 12 13struct URLBuilder { 14 host: String, 15 path: String, 16 params: Vec<(String, String)>, 17} 18 19pub(crate) fn build_url(host: &str, path: &str, params: Vec<Option<(&str, &str)>>) -> String { 20 let mut url_builder = URLBuilder::new(host); 21 url_builder.path(path); 22 23 for (key, value) in params.iter().filter_map(|x| *x) { 24 url_builder.param(key, value); 25 } 26 27 url_builder.build() 28} 29 30impl URLBuilder { 31 fn new(host: &str) -> URLBuilder { 32 let host = if host.starts_with("https://") { 33 host.to_string() 34 } else { 35 format!("https://{}", host) 36 }; 37 38 let host = if let Some(trimmed) = host.strip_suffix('/') { 39 trimmed.to_string() 40 } else { 41 host 42 }; 43 44 URLBuilder { 45 host: host.to_string(), 46 params: vec![], 47 path: "/".to_string(), 48 } 49 } 50 51 fn param(&mut self, key: &str, value: &str) -> &mut Self { 52 self.params 53 .push((key.to_owned(), urlencoding::encode(value).to_string())); 54 self 55 } 56 57 fn path(&mut self, path: &str) -> &mut Self { 58 path.clone_into(&mut self.path); 59 self 60 } 61 62 fn build(self) -> String { 63 let mut url_params = String::new(); 64 65 if !self.params.is_empty() { 66 url_params.push('?'); 67 68 let qs_args = self.params.iter().map(|(k, v)| (&**k, &**v)).collect(); 69 url_params.push_str(stringify(qs_args).as_str()); 70 } 71 72 format!("{}{}{}", self.host, self.path, url_params) 73 } 74} 75 76pub(crate) fn url_from_aturi(external_base: &str, aturi: &str) -> Result<String, UrlError> { 77 let aturi = aturi.strip_prefix("at://").unwrap_or(aturi); 78 let parts = aturi.split("/").collect::<Vec<_>>(); 79 if parts.len() == 3 && parts[1] == NSID { 80 let path = format!("/{}/{}", parts[0], parts[2]); 81 return Ok(build_url(external_base, &path, vec![])); 82 } 83 Err(UrlError::UnsupportedCollection) 84} 85 86fn find_char_bytes_len(ch: &char) -> i32 { 87 let mut b = [0; 4]; 88 ch.encode_utf8(&mut b); 89 let mut clen = 0; 90 for a in b.iter() { 91 clen += match a { 92 0 => 0, 93 _ => 1, 94 } 95 } 96 clen 97} 98 99pub(crate) fn truncate_text(text: &str, tlen: usize, suffix: Option<String>) -> String { 100 if text.len() <= tlen { 101 return text.to_string(); 102 } 103 104 let c = text.chars().nth(tlen); 105 let ret = match c { 106 Some(s) => match char::is_whitespace(s) { 107 true => text.split_at(tlen).0, 108 false => { 109 let chars: Vec<_> = text.chars().collect(); 110 let truncated = chars.split_at(tlen); 111 let mut first_len = 0; 112 for ch in truncated.0.iter() { 113 first_len += find_char_bytes_len(ch); 114 } 115 116 let mut prev_ws = first_len - 1; 117 for ch in truncated.0.iter().rev() { 118 if char::is_whitespace(*ch) { 119 break; 120 } 121 prev_ws -= find_char_bytes_len(ch); 122 } 123 124 let mut next_ws = first_len + 1; 125 for ch in truncated.1.iter() { 126 let mut b = [0; 4]; 127 ch.encode_utf8(&mut b); 128 if char::is_whitespace(*ch) { 129 break; 130 } 131 next_ws += find_char_bytes_len(ch); 132 } 133 134 match next_ws > prev_ws && prev_ws > 0 { 135 true => text.split_at(prev_ws as usize).0, 136 false => text.split_at(next_ws as usize).1, 137 } 138 } 139 }, 140 None => text, 141 }; 142 143 if ret.len() < text.len() 144 && let Some(suffix) = suffix 145 { 146 return format!("{} {}", ret, suffix.clone()); 147 } 148 ret.to_string() 149}