2025-01-21 02:39:43 +01:00
|
|
|
#![recursion_limit = "256"] // oh nooo leptos...
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
mod auth;
|
2024-05-13 19:13:40 +02:00
|
|
|
mod app;
|
2024-04-21 17:43:36 +02:00
|
|
|
mod components;
|
2024-04-17 22:07:47 +02:00
|
|
|
mod page;
|
2024-05-12 00:11:28 +02:00
|
|
|
mod config;
|
2025-01-19 05:35:28 +01:00
|
|
|
mod groups;
|
2024-06-12 06:02:36 +02:00
|
|
|
|
|
|
|
mod actors;
|
2024-07-04 02:14:50 +02:00
|
|
|
mod activities;
|
|
|
|
mod objects;
|
2024-06-12 06:02:36 +02:00
|
|
|
mod timeline;
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2025-01-19 05:28:56 +01:00
|
|
|
use apb::{Activity, Object, Actor, Base};
|
2024-04-17 22:07:47 +02:00
|
|
|
pub use app::App;
|
2024-05-12 00:11:28 +02:00
|
|
|
pub use config::Config;
|
2024-05-13 19:13:40 +02:00
|
|
|
pub use auth::Auth;
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
pub mod prelude;
|
2024-04-15 05:27:53 +02:00
|
|
|
|
2024-06-21 04:27:02 +02:00
|
|
|
pub const URL_BASE: &str = "https://dev.upub.social";
|
2024-04-15 05:00:23 +02:00
|
|
|
pub const URL_PREFIX: &str = "/web";
|
2024-04-24 05:38:09 +02:00
|
|
|
pub const URL_SENSITIVE: &str = "https://cdn.alemi.dev/social/nsfw.png";
|
2024-07-17 23:01:00 +02:00
|
|
|
pub const FALLBACK_IMAGE_URL: &str = "https://cdn.alemi.dev/social/gradient.png";
|
2024-04-17 21:10:20 +02:00
|
|
|
pub const NAME: &str = "μ";
|
2024-05-20 08:18:15 +02:00
|
|
|
pub const DEFAULT_COLOR: &str = "#BF616A";
|
2024-07-17 21:54:15 +02:00
|
|
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
use std::{ops::Deref, sync::Arc};
|
2024-05-20 06:25:47 +02:00
|
|
|
use uriproxy::UriClass;
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2025-01-19 04:51:06 +01:00
|
|
|
pub type Doc = Arc<serde_json::Value>;
|
2024-07-03 04:05:49 +02:00
|
|
|
|
2024-07-03 03:21:11 +02:00
|
|
|
pub mod cache {
|
2024-07-03 04:05:49 +02:00
|
|
|
use super::DashmapCache;
|
2024-07-03 03:21:11 +02:00
|
|
|
lazy_static::lazy_static! {
|
2025-01-19 04:51:06 +01:00
|
|
|
pub static ref OBJECTS: DashmapCache<super::Doc> = DashmapCache::default();
|
2024-07-03 04:05:49 +02:00
|
|
|
pub static ref WEBFINGER: DashmapCache<String> = DashmapCache::default();
|
2025-01-19 05:28:56 +01:00
|
|
|
pub static ref TIMELINES: DashmapCache<(Option<String>, Vec<String>)> = DashmapCache::default();
|
2024-07-03 03:21:11 +02:00
|
|
|
}
|
|
|
|
}
|
2024-04-16 06:34:50 +02:00
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum LookupStatus<T> {
|
2024-11-10 21:29:52 +01:00
|
|
|
Resolving, // TODO use this to avoid fetching twice!
|
2024-07-03 04:05:49 +02:00
|
|
|
Found(T),
|
|
|
|
NotFound,
|
|
|
|
}
|
2024-04-15 22:03:32 +02:00
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
impl<T> LookupStatus<T> {
|
|
|
|
fn inner(&self) -> Option<&T> {
|
|
|
|
if let Self::Found(x) = self {
|
|
|
|
return Some(x);
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
2024-04-15 22:20:33 +02:00
|
|
|
}
|
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
pub trait Cache {
|
|
|
|
type Item;
|
|
|
|
|
|
|
|
fn lookup(&self, key: &str) -> Option<impl Deref<Target = LookupStatus<Self::Item>>>;
|
|
|
|
fn store(&self, key: &str, value: Self::Item) -> Option<Self::Item>;
|
2024-11-21 02:41:19 +01:00
|
|
|
fn invalidate(&self, key: &str);
|
2025-01-20 03:05:27 +01:00
|
|
|
fn clear(&self);
|
2024-07-03 04:05:49 +02:00
|
|
|
|
|
|
|
fn get(&self, key: &str) -> Option<Self::Item> where Self::Item : Clone {
|
|
|
|
Some(self.lookup(key)?.deref().inner()?.clone())
|
|
|
|
}
|
2024-05-01 16:06:46 +02:00
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
fn get_or(&self, key: &str, or: Self::Item) -> Self::Item where Self::Item : Clone {
|
|
|
|
self.get(key).unwrap_or(or)
|
|
|
|
}
|
2024-04-15 22:32:05 +02:00
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
fn get_or_default(&self, key: &str) -> Self::Item where Self::Item : Clone + Default {
|
|
|
|
self.get(key).unwrap_or_default()
|
2024-04-14 06:45:51 +02:00
|
|
|
}
|
2024-07-03 04:05:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Default, Clone)]
|
|
|
|
pub struct DashmapCache<T>(Arc<dashmap::DashMap<String, LookupStatus<T>>>);
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
impl<T> Cache for DashmapCache<T> {
|
|
|
|
type Item = T;
|
|
|
|
|
|
|
|
fn lookup(&self, key: &str) -> Option<impl Deref<Target = LookupStatus<Self::Item>>> {
|
|
|
|
self.0.get(key)
|
2024-04-21 18:56:25 +02:00
|
|
|
}
|
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
fn store(&self, key: &str, value: Self::Item) -> Option<Self::Item> {
|
|
|
|
self.0.insert(key.to_string(), LookupStatus::Found(value))
|
|
|
|
.and_then(|x| if let LookupStatus::Found(x) = x { Some(x) } else { None } )
|
2024-04-16 06:42:02 +02:00
|
|
|
}
|
2024-11-21 02:41:19 +01:00
|
|
|
|
|
|
|
fn invalidate(&self, key: &str) {
|
|
|
|
self.0.remove(key);
|
|
|
|
}
|
2025-01-20 03:05:27 +01:00
|
|
|
|
|
|
|
fn clear(&self) {
|
|
|
|
self.0.clear();
|
|
|
|
}
|
2024-07-03 04:05:49 +02:00
|
|
|
}
|
|
|
|
|
2025-01-19 04:51:06 +01:00
|
|
|
impl DashmapCache<Doc> {
|
2025-01-20 03:05:27 +01:00
|
|
|
pub async fn fetch(&self, key: &str, kind: UriClass, auth: Auth) -> Option<Doc> {
|
2024-11-10 21:29:52 +01:00
|
|
|
let full_key = Uri::full(kind, key);
|
2025-01-20 03:05:27 +01:00
|
|
|
tracing::debug!("resolving {key} -> {full_key}");
|
2024-11-10 21:29:52 +01:00
|
|
|
match self.get(&full_key) {
|
|
|
|
Some(x) => Some(x),
|
|
|
|
None => {
|
2025-01-19 05:28:56 +01:00
|
|
|
match Http::fetch::<serde_json::Value>(&Uri::api(kind, key, true), auth).await {
|
|
|
|
Ok(obj) => {
|
|
|
|
let obj = Arc::new(obj);
|
|
|
|
self.include(obj.clone());
|
|
|
|
Some(obj)
|
|
|
|
},
|
2024-11-10 21:29:52 +01:00
|
|
|
Err(e) => {
|
|
|
|
tracing::error!("failed loading object from backend: {e}");
|
2025-01-19 05:28:56 +01:00
|
|
|
None
|
2024-11-10 21:29:52 +01:00
|
|
|
},
|
2024-12-28 20:18:01 +01:00
|
|
|
}
|
2024-11-10 21:29:52 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2025-01-19 05:28:56 +01:00
|
|
|
|
|
|
|
pub fn include(&self, obj: Doc) {
|
2025-01-20 03:05:27 +01:00
|
|
|
if let Ok(id) = obj.id() {
|
|
|
|
tracing::debug!("storing object {id}: {obj}");
|
|
|
|
cache::OBJECTS.store(&id, obj.clone());
|
|
|
|
if obj.actor_type().is_ok() {
|
|
|
|
if let Ok(url) = obj.url().id() {
|
|
|
|
cache::WEBFINGER.store(&url, id);
|
|
|
|
}
|
2025-01-19 05:28:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Ok(sub_obj) = obj.object().into_inner() {
|
|
|
|
if let Ok(sub_id) = sub_obj.id() {
|
|
|
|
tracing::debug!("storing sub object {sub_id}: {sub_obj}");
|
|
|
|
cache::OBJECTS.store(&sub_id, Arc::new(sub_obj));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-20 03:05:27 +01:00
|
|
|
pub async fn preload(&self, key: String, kind: UriClass, auth: Auth) -> Option<Doc> {
|
|
|
|
let doc = self.fetch(&key, kind, auth).await?;
|
2025-01-19 05:28:56 +01:00
|
|
|
let mut sub_tasks = Vec::new();
|
|
|
|
|
|
|
|
match kind {
|
|
|
|
UriClass::Activity => {
|
|
|
|
if let Ok(actor) = doc.actor().id() {
|
2025-01-20 03:05:27 +01:00
|
|
|
sub_tasks.push(self.preload(actor, UriClass::Actor, auth));
|
2025-01-19 05:28:56 +01:00
|
|
|
}
|
2025-01-20 03:05:27 +01:00
|
|
|
let clazz = match doc.activity_type().unwrap_or(apb::ActivityType::Activity) {
|
|
|
|
// TODO activities like Announce or Update may be multiple things, we can't know before
|
|
|
|
apb::ActivityType::Accept(_) => UriClass::Activity,
|
|
|
|
apb::ActivityType::Reject(_) => UriClass::Activity,
|
|
|
|
apb::ActivityType::Undo => UriClass::Activity,
|
|
|
|
apb::ActivityType::Follow => UriClass::Actor,
|
|
|
|
_ => UriClass::Object,
|
|
|
|
};
|
2025-01-19 05:28:56 +01:00
|
|
|
if let Ok(object) = doc.object().id() {
|
2025-01-20 03:05:27 +01:00
|
|
|
sub_tasks.push(self.preload(object, clazz, auth));
|
2025-01-19 05:28:56 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
UriClass::Object => {
|
|
|
|
if let Ok(actor) = doc.attributed_to().id() {
|
2025-01-20 03:05:27 +01:00
|
|
|
sub_tasks.push(self.preload(actor, UriClass::Actor, auth));
|
2025-01-19 05:28:56 +01:00
|
|
|
}
|
|
|
|
if let Ok(quote) = doc.quote_url().id() {
|
2025-01-20 03:05:27 +01:00
|
|
|
sub_tasks.push(self.preload(quote, UriClass::Object, auth));
|
2025-01-19 05:28:56 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
_ => {},
|
|
|
|
}
|
|
|
|
|
|
|
|
futures::future::join_all(sub_tasks).await;
|
|
|
|
|
|
|
|
Some(doc)
|
|
|
|
}
|
2024-11-10 21:29:52 +01:00
|
|
|
}
|
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
impl DashmapCache<String> {
|
2024-05-23 16:16:27 +02:00
|
|
|
pub async fn blocking_resolve(&self, user: &str, domain: &str) -> Option<String> {
|
2024-07-03 04:05:49 +02:00
|
|
|
if let Some(x) = self.resource(user, domain) { return Some(x); }
|
2024-05-23 16:16:27 +02:00
|
|
|
self.fetch(user, domain).await;
|
2024-07-03 04:05:49 +02:00
|
|
|
self.resource(user, domain)
|
2024-05-23 16:16:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn resolve(&self, user: &str, domain: &str) -> Option<String> {
|
2024-07-03 04:05:49 +02:00
|
|
|
if let Some(x) = self.resource(user, domain) { return Some(x); }
|
2024-05-23 16:16:27 +02:00
|
|
|
let (_self, user, domain) = (self.clone(), user.to_string(), domain.to_string());
|
2025-01-17 02:19:52 +01:00
|
|
|
leptos::task::spawn_local(async move { _self.fetch(&user, &domain).await });
|
2024-05-23 16:16:27 +02:00
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2024-07-03 04:05:49 +02:00
|
|
|
fn resource(&self, user: &str, domain: &str) -> Option<String> {
|
2024-05-23 16:16:27 +02:00
|
|
|
let query = format!("{user}@{domain}");
|
2024-07-03 04:05:49 +02:00
|
|
|
self.get(&query)
|
2024-05-23 16:16:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn fetch(&self, user: &str, domain: &str) {
|
|
|
|
let query = format!("{user}@{domain}");
|
|
|
|
self.0.insert(query.to_string(), LookupStatus::Resolving);
|
|
|
|
match reqwest::get(format!("{URL_BASE}/.well-known/webfinger?resource=acct:{query}")).await {
|
|
|
|
Ok(res) => match res.error_for_status() {
|
|
|
|
Ok(res) => match res.json::<jrd::JsonResourceDescriptor>().await {
|
|
|
|
Ok(doc) => {
|
2024-05-27 05:38:51 +02:00
|
|
|
if let Some(uid) = doc.links.into_iter().find(|x| x.rel == "self").and_then(|x| x.href) {
|
2024-05-23 16:16:27 +02:00
|
|
|
self.0.insert(query, LookupStatus::Found(uid));
|
|
|
|
} else {
|
|
|
|
self.0.insert(query, LookupStatus::NotFound);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Err(e) => {
|
|
|
|
tracing::error!("invalid webfinger response: {e:?}");
|
|
|
|
self.0.remove(&query);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Err(e) => {
|
|
|
|
tracing::error!("could not resolve webfinbger: {e:?}");
|
|
|
|
self.0.insert(query, LookupStatus::NotFound);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Err(e) => {
|
|
|
|
tracing::error!("failed accessing webfinger server: {e:?}");
|
|
|
|
self.0.remove(&query);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-17 02:19:52 +01:00
|
|
|
use leptos_router::params::Params; // TODO can i remove this?
|
2024-07-04 03:14:26 +02:00
|
|
|
#[derive(Clone, leptos::Params, PartialEq)]
|
|
|
|
pub struct IdParam {
|
|
|
|
id: Option<String>,
|
|
|
|
}
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
pub struct Http;
|
2024-04-14 06:45:51 +02:00
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
impl Http {
|
|
|
|
pub async fn request<T: serde::ser::Serialize>(
|
|
|
|
method: reqwest::Method,
|
|
|
|
url: &str,
|
|
|
|
data: Option<&T>,
|
2024-05-01 18:22:25 +02:00
|
|
|
auth: Auth,
|
2024-04-17 22:07:47 +02:00
|
|
|
) -> reqwest::Result<reqwest::Response> {
|
2025-01-17 02:19:52 +01:00
|
|
|
use leptos::prelude::GetUntracked;
|
2024-04-17 22:07:47 +02:00
|
|
|
|
|
|
|
let mut req = reqwest::Client::new()
|
|
|
|
.request(method, url);
|
|
|
|
|
2024-05-03 03:55:26 +02:00
|
|
|
if let Some(auth) = auth.token.get_untracked().filter(|x| !x.is_empty()) {
|
2024-04-17 22:07:47 +02:00
|
|
|
req = req.header("Authorization", format!("Bearer {}", auth));
|
2024-04-15 03:38:16 +02:00
|
|
|
}
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
if let Some(data) = data {
|
|
|
|
req = req.json(data);
|
2024-04-15 22:03:32 +02:00
|
|
|
}
|
|
|
|
|
2024-05-11 17:58:01 +02:00
|
|
|
req.send().await
|
2024-04-15 22:03:32 +02:00
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
pub async fn fetch<T: serde::de::DeserializeOwned>(url: &str, token: Auth) -> reqwest::Result<T> {
|
2024-04-17 22:07:47 +02:00
|
|
|
Self::request::<()>(reqwest::Method::GET, url, None, token)
|
|
|
|
.await?
|
2024-05-11 17:58:01 +02:00
|
|
|
.error_for_status()?
|
2024-04-17 22:07:47 +02:00
|
|
|
.json::<T>()
|
|
|
|
.await
|
2024-04-17 07:29:56 +02:00
|
|
|
}
|
|
|
|
|
2024-05-01 18:22:25 +02:00
|
|
|
pub async fn post<T: serde::ser::Serialize>(url: &str, data: &T, token: Auth) -> reqwest::Result<()> {
|
2024-04-17 22:07:47 +02:00
|
|
|
Self::request(reqwest::Method::POST, url, Some(data), token)
|
2024-05-11 17:58:01 +02:00
|
|
|
.await?
|
|
|
|
.error_for_status()?;
|
2024-04-17 22:07:47 +02:00
|
|
|
Ok(())
|
2024-04-14 06:45:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
pub struct Uri;
|
|
|
|
|
|
|
|
impl Uri {
|
2024-05-20 06:25:47 +02:00
|
|
|
pub fn full(kind: UriClass, id: &str) -> String {
|
|
|
|
uriproxy::uri(URL_BASE, kind, id)
|
2024-04-16 06:34:50 +02:00
|
|
|
}
|
|
|
|
|
2024-07-03 07:30:29 +02:00
|
|
|
pub fn pretty(url: &str, len: usize) -> String {
|
2024-05-20 06:25:47 +02:00
|
|
|
let bare = url.replace("https://", "");
|
2024-07-03 07:30:29 +02:00
|
|
|
if bare.len() < len {
|
2024-05-20 06:25:47 +02:00
|
|
|
bare
|
2024-04-17 22:07:47 +02:00
|
|
|
} else {
|
2024-07-03 07:30:29 +02:00
|
|
|
format!("{}..", bare.get(..len).unwrap_or_default())
|
2024-07-04 03:43:58 +02:00
|
|
|
}
|
|
|
|
//.replace('/', "\u{200B}/\u{200B}")
|
2024-04-17 21:10:20 +02:00
|
|
|
}
|
2024-04-15 03:03:01 +02:00
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
pub fn short(url: &str) -> String {
|
2024-05-20 08:45:41 +02:00
|
|
|
if url.starts_with(URL_BASE) || url.starts_with('/') {
|
2024-06-07 06:14:45 +02:00
|
|
|
uriproxy::decompose(url)
|
2024-05-21 15:28:48 +02:00
|
|
|
} else if url.starts_with("https://") || url.starts_with("http://") {
|
2024-06-07 06:14:45 +02:00
|
|
|
uriproxy::compact(url)
|
2024-05-20 08:45:41 +02:00
|
|
|
} else {
|
|
|
|
url.to_string()
|
2024-04-17 22:07:47 +02:00
|
|
|
}
|
2024-04-15 03:03:01 +02:00
|
|
|
}
|
2024-04-16 08:02:03 +02:00
|
|
|
|
2024-04-17 22:07:47 +02:00
|
|
|
/// convert url id to valid frontend view id:
|
2024-05-21 15:28:48 +02:00
|
|
|
///
|
2024-04-17 22:07:47 +02:00
|
|
|
/// accepts:
|
2024-05-21 15:28:48 +02:00
|
|
|
///
|
2024-05-20 06:25:47 +02:00
|
|
|
pub fn web(kind: UriClass, url: &str) -> String {
|
2024-04-21 18:56:25 +02:00
|
|
|
let kind = kind.as_ref();
|
2024-04-17 22:07:47 +02:00
|
|
|
format!("/web/{kind}/{}", Self::short(url))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// convert url id to valid backend api id
|
2024-05-21 15:28:48 +02:00
|
|
|
///
|
2024-04-17 22:07:47 +02:00
|
|
|
/// accepts:
|
2024-05-21 15:28:48 +02:00
|
|
|
///
|
2024-05-20 06:25:47 +02:00
|
|
|
pub fn api(kind: UriClass, url: &str, fetch: bool) -> String {
|
2024-04-21 18:56:25 +02:00
|
|
|
let kind = kind.as_ref();
|
2024-04-18 05:00:44 +02:00
|
|
|
format!("{URL_BASE}/{kind}/{}{}", Self::short(url), if fetch { "?fetch=true" } else { "" })
|
2024-04-16 08:02:03 +02:00
|
|
|
}
|
2024-08-15 17:30:26 +02:00
|
|
|
|
|
|
|
pub fn domain(full: &str) -> String {
|
|
|
|
full
|
|
|
|
.replacen("https://", "", 1)
|
|
|
|
.replacen("http://", "", 1)
|
|
|
|
.split('/')
|
|
|
|
.next()
|
|
|
|
.unwrap_or_default()
|
|
|
|
.to_string()
|
|
|
|
}
|
2024-04-16 08:02:03 +02:00
|
|
|
}
|