1mod auth;
2mod app;
3mod components;
4mod page;
5mod config;
6mod groups;
7
8mod actors;
9mod activities;
10mod objects;
11mod timeline;
12mod lists;
13
14use apb::{Activity, Object, Actor, Base};
15pub use app::App;
16pub use config::Config;
17pub use auth::Auth;
18
19pub mod prelude;
20
21pub const URL_BASE: &str = match std::option_env!("UPUB_BASE_URL") { Some(x) => x, None => "" };
22pub const URL_PREFIX: &str = "/web";
23pub const URL_SENSITIVE: &str = "https://cdn.alemi.dev/social/nsfw.png";
24pub const FALLBACK_IMAGE_URL: &str = "https://cdn.alemi.dev/social/gradient.png";
25pub const NAME: &str = "μ";
26pub const DEFAULT_COLOR: &str = "#BF616A";
27pub const VERSION: &str = env!("CARGO_PKG_VERSION");
28
29use std::{ops::Deref, sync::Arc};
30use uriproxy::UriClass;
31
32pub type Doc = Arc<serde_json::Value>;
33
34pub mod cache {
35 use super::DashmapCache;
36 lazy_static::lazy_static! {
37 pub static ref OBJECTS: DashmapCache<super::Doc> = DashmapCache::default();
38 pub static ref WEBFINGER: DashmapCache<String> = DashmapCache::default();
39 pub static ref TIMELINES: DashmapCache<(Option<String>, Vec<String>)> = DashmapCache::default();
40 }
41}
42
43#[derive(Debug)]
44pub enum LookupStatus<T> {
45 Resolving, Found(T),
47 NotFound,
48}
49
50impl<T> LookupStatus<T> {
51 fn inner(&self) -> Option<&T> {
52 if let Self::Found(x) = self {
53 return Some(x);
54 }
55 None
56 }
57}
58
59pub trait Cache {
60 type Item;
61
62 fn lookup(&self, key: &str) -> Option<impl Deref<Target = LookupStatus<Self::Item>>>;
63 fn store(&self, key: &str, value: Self::Item) -> Option<Self::Item>;
64 fn invalidate(&self, key: &str);
65 fn clear(&self);
66
67 fn get(&self, key: &str) -> Option<Self::Item> where Self::Item : Clone {
68 Some(self.lookup(key)?.deref().inner()?.clone())
69 }
70
71 fn get_or(&self, key: &str, or: Self::Item) -> Self::Item where Self::Item : Clone {
72 self.get(key).unwrap_or(or)
73 }
74
75 fn get_or_default(&self, key: &str) -> Self::Item where Self::Item : Clone + Default {
76 self.get(key).unwrap_or_default()
77 }
78}
79
80#[derive(Default, Clone)]
81pub struct DashmapCache<T>(Arc<dashmap::DashMap<String, LookupStatus<T>>>);
82
83impl<T> Cache for DashmapCache<T> {
84 type Item = T;
85
86 fn lookup(&self, key: &str) -> Option<impl Deref<Target = LookupStatus<Self::Item>>> {
87 self.0.get(key)
88 }
89
90 fn store(&self, key: &str, value: Self::Item) -> Option<Self::Item> {
91 self.0.insert(key.to_string(), LookupStatus::Found(value))
92 .and_then(|x| if let LookupStatus::Found(x) = x { Some(x) } else { None } )
93 }
94
95 fn invalidate(&self, key: &str) {
96 self.0.remove(key);
97 }
98
99 fn clear(&self) {
100 self.0.clear();
101 }
102}
103
104impl DashmapCache<Doc> {
105 pub async fn fetch(&self, key: &str, kind: UriClass, auth: Auth) -> Option<Doc> {
106 let full_key = Uri::full(kind, key);
107 tracing::debug!("resolving {key} -> {full_key}");
108 match self.get(&full_key) {
109 Some(x) => Some(x),
110 None => {
111 match Http::fetch::<serde_json::Value>(&Uri::api(kind, key, true), auth).await {
112 Ok(obj) => {
113 let obj = Arc::new(obj);
114 self.include(obj.clone());
115 Some(obj)
116 },
117 Err(e) => {
118 tracing::error!("failed loading object '{kind:?}({key})' from backend: {e}");
119 None
120 },
121 }
122 },
123 }
124 }
125
126 pub fn include(&self, obj: Doc) {
127 if let Ok(id) = obj.id() {
128 tracing::debug!("storing object {id}: {obj}");
129 cache::OBJECTS.store(&id, obj.clone());
130 if obj.actor_type().is_ok() {
131 if let Ok(url) = obj.url().id() {
132 cache::WEBFINGER.store(&url, id);
133 }
134 }
135 }
136 if let Ok(sub_obj) = obj.object().into_inner() {
137 if let Ok(sub_id) = sub_obj.id() {
138 tracing::debug!("storing sub object {sub_id}: {sub_obj}");
139 cache::OBJECTS.store(&sub_id, Arc::new(sub_obj));
140 }
141 }
142 }
143
144 pub async fn preload(&self, key: String, kind: UriClass, auth: Auth) -> Option<Doc> {
145 let doc = self.fetch(&key, kind, auth).await?;
146 let mut sub_tasks = Vec::new();
147
148 match kind {
149 UriClass::Activity => {
150 if let Ok(actor) = doc.actor().id() {
151 sub_tasks.push(self.preload(actor, UriClass::Actor, auth));
152 }
153 if let Ok(actor) = doc.attributed_to().id() {
154 sub_tasks.push(self.preload(actor, UriClass::Actor, auth));
155 }
156 let clazz = match doc.activity_type().unwrap_or(apb::ActivityType::Activity) {
157 apb::ActivityType::Accept(_) => UriClass::Activity,
159 apb::ActivityType::Reject(_) => UriClass::Activity,
160 apb::ActivityType::Undo => UriClass::Activity,
161 apb::ActivityType::Follow => UriClass::Actor,
162 _ => UriClass::Object,
163 };
164 if let Ok(object) = doc.object().id() {
165 sub_tasks.push(self.preload(object, clazz, auth));
166 }
167 },
168 UriClass::Object => {
169 if let Ok(actor) = doc.actor().id() {
170 sub_tasks.push(self.preload(actor, UriClass::Actor, auth));
171 }
172 if let Ok(actor) = doc.attributed_to().id() {
173 sub_tasks.push(self.preload(actor, UriClass::Actor, auth));
174 }
175 if let Ok(quote) = doc.quote_url().id() {
176 sub_tasks.push(self.preload(quote, UriClass::Object, auth));
177 }
178 },
179 _ => {},
180 }
181
182 futures::future::join_all(sub_tasks).await;
183
184 Some(doc)
185 }
186}
187
188impl DashmapCache<String> {
189 pub async fn blocking_resolve(&self, user: &str, domain: &str, auth: Auth) -> Option<String> {
190 if let Some(x) = self.resource(user, domain) { return Some(x); }
191 self.fetch(user, domain, auth).await;
192 self.resource(user, domain)
193 }
194
195 pub fn resolve(&self, user: &str, domain: &str, auth: Auth) -> Option<String> {
196 if let Some(x) = self.resource(user, domain) { return Some(x); }
197 let (_self, user, domain) = (self.clone(), user.to_string(), domain.to_string());
198 leptos::task::spawn_local(async move { _self.fetch(&user, &domain, auth).await });
199 None
200 }
201
202 fn resource(&self, user: &str, domain: &str) -> Option<String> {
203 let query = format!("{user}@{domain}");
204 self.get(&query)
205 }
206
207 async fn fetch(&self, user: &str, domain: &str, auth: Auth) {
208 let query = format!("{user}@{domain}");
209 self.0.insert(query.to_string(), LookupStatus::Resolving);
210 match crate::Http::fetch::<jrd::JsonResourceDescriptor>(&format!("{URL_BASE}/.well-known/webfinger?resource=acct:{query}"), auth).await {
211 Ok(doc) => {
212 if let Some(uid) = doc.links.into_iter().find(|x| x.rel == "self").and_then(|x| x.href) {
213 self.0.insert(query, LookupStatus::Found(uid));
214 } else {
215 self.0.insert(query, LookupStatus::NotFound);
216 }
217 },
218 Err(e) => {
219 tracing::error!("could not resolve webfinbger: {e:?}");
220 self.0.insert(query, LookupStatus::NotFound);
221 },
222 }
223 }
224}
225
226use leptos_router::params::Params; #[derive(Clone, leptos::Params, PartialEq)]
228pub struct IdParam {
229 id: Option<String>,
230}
231
232pub struct Http;
233
234impl Http {
235 pub fn location() -> &'static str {
237 static LOCATION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
238 LOCATION.get_or_init(||
239 web_sys::window()
240 .expect("could not access window element")
241 .location()
242 .origin()
243 .expect("could not access location origin")
244 ).as_str()
245 }
246
247 pub async fn request<T: serde::ser::Serialize>(
248 method: reqwest::Method,
249 url: &str,
250 data: Option<&T>,
251 auth: Auth,
252 ) -> reqwest::Result<reqwest::Response> {
253 tracing::info!("making request to {url}");
254 use leptos::prelude::GetUntracked;
255
256 let mut url = url.to_string();
262 if !url.starts_with("http") {
263 url = format!("{}{url}", Self::location());
264 }
265
266 let mut req = reqwest::Client::new()
267 .request(method, url);
268
269 if let Some(auth) = auth.token.get_untracked().filter(|x| !x.is_empty()) {
270 req = req.header("Authorization", format!("Bearer {}", auth));
271 }
272
273 if let Some(data) = data {
274 req = req.json(data);
275 }
276
277 req.send().await
278 }
279
280 pub async fn fetch<T: serde::de::DeserializeOwned>(url: &str, token: Auth) -> reqwest::Result<T> {
281 Self::request::<()>(reqwest::Method::GET, url, None, token)
282 .await?
283 .error_for_status()?
284 .json::<T>()
285 .await
286 }
287
288 pub async fn post<T: serde::ser::Serialize>(url: &str, data: &T, token: Auth) -> reqwest::Result<()> {
289 Self::request(reqwest::Method::POST, url, Some(data), token)
290 .await?
291 .error_for_status()?;
292 Ok(())
293 }
294}
295
296pub struct Uri;
297
298impl Uri {
299 pub fn full(kind: UriClass, id: &str) -> String {
300 uriproxy::uri(URL_BASE, kind, id)
301 }
302
303 pub fn pretty(url: &str, len: usize) -> String {
304 let bare = url.replace("https://", "");
305 if bare.len() < len {
306 bare
307 } else {
308 format!("{}..", bare.get(..len).unwrap_or_default())
309 }
310 }
312
313 pub fn short(url: &str) -> String {
314 if url.starts_with(Http::location()) || url.starts_with('/') {
315 uriproxy::decompose(url)
316 } else if url.starts_with("https://") || url.starts_with("http://") {
317 uriproxy::compact(url)
318 } else {
319 url.to_string()
320 }
321 }
322
323 pub fn web(kind: UriClass, url: &str) -> String {
328 let kind = kind.as_ref();
329 format!("/web/{kind}/{}", Self::short(url))
330 }
331
332 pub fn api(kind: UriClass, url: &str, fetch: bool) -> String {
337 let kind = kind.as_ref();
338 format!("{URL_BASE}/{kind}/{}{}", Self::short(url), if fetch { "?fetch=true" } else { "" })
339 }
340
341 pub fn domain(full: &str) -> String {
342 full
343 .replacen("https://", "", 1)
344 .replacen("http://", "", 1)
345 .split('/')
346 .next()
347 .unwrap_or_default()
348 .to_string()
349 }
350}
351
352pub trait IconGradient {
353 fn icon_url_and_style(&self) -> (String, String);
354}
355
356impl IconGradient for Doc {
357 fn icon_url_and_style(&self) -> (String, String) {
358 use apb::Shortcuts;
359 match self.icon_url() {
360 Ok(url) => (url, "".to_string()),
361 Err(_e) => {
362 let (from, to) = crate::string_to_hex(&self.id().unwrap_or_default());
363 ("".to_string(), format!("background: radial-gradient({from}, {to}); padding: .1em;"))
364 },
365 }
366 }
367}
368
369fn string_to_hex(inpt: &str) -> (String, String) {
370 use std::hash::{Hash, Hasher};
371
372 let mut hasher = std::hash::DefaultHasher::new();
373 inpt.hash(&mut hasher);
374 let raw = hasher.finish();
375
376 let from = raw as u32;
377 let to = (raw >> 32) as u32;
378
379 let from_str = format!(
380 "#{:06x}", from >> 8
381 );
382
383 let to_str = format!(
384 "#{:06x}", to >> 8
385 );
386 (from_str, to_str)
387}
388