forked from
smokesignal.events/smokesignal
The smokesignal.events web application
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}