diff --git a/Cargo.lock b/Cargo.lock index 63ce31d..e8d3f0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,6 +4489,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "uriproxy", "uuid", ] @@ -4515,9 +4516,17 @@ dependencies = [ "serde_json", "thiserror", "tracing", + "uriproxy", "web-sys", ] +[[package]] +name = "uriproxy" +version = "0.1.0" +dependencies = [ + "base64 0.22.0", +] + [[package]] name = "url" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 51887a2..a1cf73c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["apb", "web", "mdhtml"] +members = ["apb", "web", "mdhtml", "uriproxy"] [package] name = "upub" @@ -28,6 +28,7 @@ serde_default = "0.1" serde-inline-default = "0.2" toml = "0.8" mdhtml = { path = "mdhtml", features = ["markdown"] } +uriproxy = { path = "uriproxy" } jrd = "0.1" tracing = "0.1" tracing-subscriber = "0.3" diff --git a/src/cli/relay.rs b/src/cli/relay.rs index 10c565a..850a84f 100644 --- a/src/cli/relay.rs +++ b/src/cli/relay.rs @@ -1,12 +1,12 @@ use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder}; pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) -> crate::Result<()> { - let aid = ctx.aid(uuid::Uuid::new_v4().to_string()); + let aid = ctx.aid(&uuid::Uuid::new_v4().to_string()); let mut activity_model = crate::model::activity::Model { id: aid.clone(), activity_type: apb::ActivityType::Follow, - actor: ctx.base(), + actor: ctx.base().to_string(), object: Some(actor.clone()), target: None, published: chrono::Utc::now(), @@ -32,7 +32,7 @@ pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) -> crate::model::activity::Entity::insert(activity_model.into_active_model()) .exec(ctx.db()).await?; - ctx.dispatch(&ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?; + ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?; Ok(()) } diff --git a/src/routes/activitypub/activity.rs b/src/routes/activitypub/activity.rs index fedfb1b..bc15a35 100644 --- a/src/routes/activitypub/activity.rs +++ b/src/routes/activitypub/activity.rs @@ -10,7 +10,7 @@ pub async fn view( AuthIdentity(auth): AuthIdentity, Query(query): Query, ) -> crate::Result> { - let aid = ctx.uri("activities", id); + let aid = ctx.aid(&id); if auth.is_local() && query.fetch && !ctx.is_local(&aid) { let obj = ctx.fetch_activity(&aid).await?; if obj.id != aid { diff --git a/src/routes/activitypub/application.rs b/src/routes/activitypub/application.rs index ab62214..1050c83 100644 --- a/src/routes/activitypub/application.rs +++ b/src/routes/activitypub/application.rs @@ -58,7 +58,7 @@ pub async fn proxy_get( Method::GET, &query.id, None, - &ctx.base(), + ctx.base(), &ctx.app().private_key, &format!("{}+proxy", ctx.domain()), ) @@ -82,7 +82,7 @@ pub async fn proxy_form( Method::GET, &query.id, None, - &ctx.base(), + ctx.base(), &ctx.app().private_key, &format!("{}+proxy", ctx.domain()), ) diff --git a/src/routes/activitypub/auth.rs b/src/routes/activitypub/auth.rs index 2f5239e..acb01b5 100644 --- a/src/routes/activitypub/auth.rs +++ b/src/routes/activitypub/auth.rs @@ -84,5 +84,5 @@ pub async fn register( registration.banner_url ).await?; - Ok(Json(ctx.uid(registration.username))) + Ok(Json(ctx.uid(®istration.username))) } diff --git a/src/routes/activitypub/context.rs b/src/routes/activitypub/context.rs index c24c83b..7278018 100644 --- a/src/routes/activitypub/context.rs +++ b/src/routes/activitypub/context.rs @@ -9,7 +9,7 @@ pub async fn get( AuthIdentity(auth): AuthIdentity, ) -> crate::Result> { let local_context_id = url!(ctx, "/context/{id}"); - let context = ctx.uri("context", id); + let context = ctx.context_id(&id); let count = model::addressing::Entity::find_addressed(auth.my_id()) .filter(auth.filter_condition()) diff --git a/src/routes/activitypub/object/mod.rs b/src/routes/activitypub/object/mod.rs index 8ad7ab7..7796887 100644 --- a/src/routes/activitypub/object/mod.rs +++ b/src/routes/activitypub/object/mod.rs @@ -14,7 +14,7 @@ pub async fn view( AuthIdentity(auth): AuthIdentity, Query(query): Query, ) -> crate::Result> { - let oid = ctx.uri("objects", id); + let oid = ctx.oid(&id); if auth.is_local() && query.fetch && !ctx.is_local(&oid) { let obj = ctx.fetch_object(&oid).await?; // some implementations serve statuses on different urls than their AP id diff --git a/src/routes/activitypub/object/replies.rs b/src/routes/activitypub/object/replies.rs index 35d08cc..a01ba47 100644 --- a/src/routes/activitypub/object/replies.rs +++ b/src/routes/activitypub/object/replies.rs @@ -10,7 +10,7 @@ pub async fn get( Query(q): Query, ) -> crate::Result> { let replies_id = url!(ctx, "/objects/{id}/replies"); - let oid = ctx.uri("objects", id); + let oid = ctx.oid(&id); if auth.is_local() && q.fetch { ctx.fetch_thread(&oid).await?; @@ -32,7 +32,7 @@ pub async fn page( AuthIdentity(auth): AuthIdentity, ) -> crate::Result> { let page_id = url!(ctx, "/objects/{id}/replies/page"); - let oid = ctx.uri("objects", id); + let oid = ctx.oid(&id); crate::server::builders::paginate( page_id, diff --git a/src/routes/activitypub/user/following.rs b/src/routes/activitypub/user/following.rs index 1519e55..d552eb5 100644 --- a/src/routes/activitypub/user/following.rs +++ b/src/routes/activitypub/user/following.rs @@ -11,7 +11,7 @@ pub async fn get( ) -> crate::Result> { let follow___ = if OUTGOING { "following" } else { "followers" }; let count = model::relation::Entity::find() - .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(id.clone()))) + .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id))) .count(ctx.db()).await.unwrap_or_else(|e| { tracing::error!("failed counting {follow___} for {id}: {e}"); 0 @@ -30,7 +30,7 @@ pub async fn page( let offset = page.offset.unwrap_or(0); let following = model::relation::Entity::find() - .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(id.clone()))) + .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id))) .select_only() .select_column(if OUTGOING { Following } else { Follower }) .limit(limit) diff --git a/src/routes/activitypub/user/inbox.rs b/src/routes/activitypub/user/inbox.rs index 628ac36..1d02f09 100644 --- a/src/routes/activitypub/user/inbox.rs +++ b/src/routes/activitypub/user/inbox.rs @@ -11,7 +11,7 @@ pub async fn get( match auth { Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()), Identity::Remote(_) => Err(StatusCode::FORBIDDEN.into()), - Identity::Local(user) => if ctx.uid(id.clone()) == user { + Identity::Local(user) => if ctx.uid(&id) == user { crate::server::builders::collection(&url!(ctx, "/users/{id}/inbox"), None) } else { Err(StatusCode::FORBIDDEN.into()) @@ -29,7 +29,7 @@ pub async fn page( // local inbox is only for local users return Err(UpubError::forbidden()); }; - if uid != &ctx.uid(id.clone()) { + if uid != &ctx.uid(&id) { return Err(UpubError::forbidden()); } diff --git a/src/routes/activitypub/user/mod.rs b/src/routes/activitypub/user/mod.rs index 5879c81..5819f24 100644 --- a/src/routes/activitypub/user/mod.rs +++ b/src/routes/activitypub/user/mod.rs @@ -19,7 +19,7 @@ pub async fn view( Path(id): Path, Query(query): Query, ) -> crate::Result> { - let mut uid = ctx.uri("users", id.clone()); + let mut uid = ctx.uid(&id); if auth.is_local() { if id.starts_with('@') { if let Some((user, host)) = id.replacen('@', "", 1).split_once('@') { diff --git a/src/routes/activitypub/user/outbox.rs b/src/routes/activitypub/user/outbox.rs index 2e132e9..d1be16f 100644 --- a/src/routes/activitypub/user/outbox.rs +++ b/src/routes/activitypub/user/outbox.rs @@ -17,11 +17,7 @@ pub async fn page( Query(page): Query, AuthIdentity(auth): AuthIdentity, ) -> crate::Result> { - let uid = if id.starts_with('+') { - format!("https://{}", id.replacen('+', "", 1).replace('@', "/")) - } else { - ctx.uid(id.clone()) - }; + let uid = ctx.uid(&id); crate::server::builders::paginate( url!(ctx, "/users/{id}/outbox/page"), Condition::all() @@ -47,7 +43,7 @@ pub async fn post( match auth { Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()), Identity::Remote(_) => Err(StatusCode::NOT_IMPLEMENTED.into()), - Identity::Local(uid) => if ctx.uid(id.clone()) == uid { + Identity::Local(uid) => if ctx.uid(&id) == uid { tracing::debug!("processing new local activity: {}", serde_json::to_string(&activity).unwrap_or_default()); match activity.base_type() { None => Err(StatusCode::BAD_REQUEST.into()), diff --git a/src/routes/activitypub/well_known.rs b/src/routes/activitypub/well_known.rs index 2489c8b..8fe8a41 100644 --- a/src/routes/activitypub/well_known.rs +++ b/src/routes/activitypub/well_known.rs @@ -105,12 +105,12 @@ pub async fn webfinger(State(ctx): State, Query(query): Query, Query(query): Query ) -> Result, StatusCode> { - match model::user::Entity::find_by_id(ctx.uid(id)) + match model::user::Entity::find_by_id(ctx.uid(&id)) .find_also_related(model::config::Entity) .one(ctx.db()) .await diff --git a/src/server/admin.rs b/src/server/admin.rs index d20fdd3..0ee1dd7 100644 --- a/src/server/admin.rs +++ b/src/server/admin.rs @@ -25,7 +25,7 @@ impl Administrable for super::Context { banner_url: Option, ) -> crate::Result<()> { let key = openssl::rsa::Rsa::generate(2048).unwrap(); - let ap_id = self.uid(username.clone()); + let ap_id = self.uid(&username); let db = self.db(); let domain = self.domain().to_string(); let user_model = crate::model::user::Model { diff --git a/src/server/context.rs b/src/server/context.rs index e0ec0c4..bbad080 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -4,6 +4,7 @@ use openssl::rsa::Rsa; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set}; use crate::{config::Config, model, server::fetcher::Fetcher}; +use uriproxy::UriClass; use super::dispatcher::Dispatcher; @@ -15,6 +16,7 @@ struct ContextInner { config: Config, domain: String, protocol: String, + base_url: String, dispatcher: Dispatcher, // TODO keep these pre-parsed app: model::application::Model, @@ -72,6 +74,7 @@ impl Context { .await?; Ok(Context(Arc::new(ContextInner { + base_url: format!("{}{}", protocol, domain), db, domain, protocol, app, dispatcher, config, relays: BTreeSet::from_iter(relays.into_iter()), }))) @@ -97,53 +100,38 @@ impl Context { &self.0.protocol } - pub fn base(&self) -> String { - format!("{}{}", self.0.protocol, self.0.domain) - } - - pub fn uri(&self, entity: &str, id: String) -> String { - if id.starts_with("http") { // ready-to-use id - id - } else if id.starts_with('+') { // compacted id - // TODO theres already 2 edge cases, i really need to get rid of this - id - .replace('@', "/") - .replace("///", "/@/") // omg wordpress PLEASE AAAAAAAAAAAAAAAAAAAA - .replace("//", "/@") // oops my method sucks!! TODO - .replacen('+', "https://", 1) - .replace(' ', "%20") // omg wordpress - } else { // bare local id - format!("{}{}/{}/{}", self.0.protocol, self.0.domain, entity, id) - } - } - - /// get bare id, usually an uuid but unspecified - pub fn id(&self, uri: &str) -> String { - if uri.starts_with(&self.0.domain) { - uri.split('/').last().unwrap_or("").to_string() - } else { - uri - .replace("https://", "+") - .replace("http://", "+") - .replace('/', "@") - } + pub fn base(&self) -> &str { + &self.0.base_url } /// get full user id uri - pub fn uid(&self, id: String) -> String { - self.uri("users", id) + pub fn uid(&self, id: &str) -> String { + uriproxy::uri(self.base(), UriClass::User, id) } /// get full object id uri - pub fn oid(&self, id: String) -> String { - self.uri("objects", id) + pub fn oid(&self, id: &str) -> String { + uriproxy::uri(self.base(), UriClass::Object, id) } /// get full activity id uri - pub fn aid(&self, id: String) -> String { - self.uri("activities", id) + pub fn aid(&self, id: &str) -> String { + uriproxy::uri(self.base(), UriClass::Activity, id) } + // TODO remove this!! + pub fn context_id(&self, id: &str) -> String { + uriproxy::uri(self.base(), UriClass::Context, id) + } + + /// get bare id, which is uuid for local stuff and ~{uri|base64} for remote stuff + pub fn id(&self, full_id: &str) -> String { + if self.is_local(full_id) { + uriproxy::decompose_id(full_id) + } else { + uriproxy::compact_id(full_id) + } + } pub fn server(id: &str) -> String { id @@ -156,8 +144,7 @@ impl Context { } pub fn is_local(&self, id: &str) -> bool { - // TODO consider precalculating once this format! - id.starts_with(&format!("{}{}", self.0.protocol, self.0.domain)) + id.starts_with(self.base()) } pub async fn expand_addressing(&self, targets: Vec) -> crate::Result> { diff --git a/src/server/outbox.rs b/src/server/outbox.rs index 5251c10..1aba864 100644 --- a/src/server/outbox.rs +++ b/src/server/outbox.rs @@ -15,8 +15,8 @@ impl apb::server::Outbox for Context { async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result { let raw_oid = uuid::Uuid::new_v4().to_string(); - let oid = self.oid(raw_oid.clone()); - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let oid = self.oid(&raw_oid); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = object.addressed(); let object_model = self.insert_object( object @@ -24,7 +24,7 @@ impl apb::server::Outbox for Context { .set_attributed_to(Node::link(uid.clone())) .set_published(Some(chrono::Utc::now())) .set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))), - Some(self.base()), + Some(self.base().to_string()), ).await?; let activity_model = model::activity::Model { @@ -54,8 +54,8 @@ impl apb::server::Outbox for Context { }; let raw_oid = uuid::Uuid::new_v4().to_string(); - let oid = self.oid(raw_oid.clone()); - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let oid = self.oid(&raw_oid); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); self.insert_object( @@ -67,7 +67,7 @@ impl apb::server::Outbox for Context { .set_bto(activity.bto()) .set_cc(activity.cc()) .set_bcc(activity.bcc()), - Some(self.base()), + Some(self.base().to_string()), ).await?; let activity_model = model::activity::Model::new( @@ -86,7 +86,7 @@ impl apb::server::Outbox for Context { } async fn like(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; self.fetch_object(&oid).await?; @@ -118,7 +118,7 @@ impl apb::server::Outbox for Context { } async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); if activity.object().id().is_none() { return Err(UpubError::bad_request()); @@ -139,7 +139,7 @@ impl apb::server::Outbox for Context { } async fn accept(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); if activity.object().id().is_none() { return Err(UpubError::bad_request()); @@ -192,7 +192,7 @@ impl apb::server::Outbox for Context { } async fn undo(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); let old_aid = activity.object().id().ok_or_else(UpubError::bad_request)?; let old_activity = model::activity::Entity::find_by_id(old_aid) @@ -235,7 +235,7 @@ impl apb::server::Outbox for Context { } async fn delete(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; let object = model::object::Entity::find_by_id(&oid) @@ -275,7 +275,7 @@ impl apb::server::Outbox for Context { } async fn update(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let object_node = activity.object().extract().ok_or_else(UpubError::bad_request)?; match object_node.object_type() { @@ -364,7 +364,7 @@ impl apb::server::Outbox for Context { } async fn announce(&self, uid: String, activity: serde_json::Value) -> crate::Result { - let aid = self.aid(uuid::Uuid::new_v4().to_string()); + let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let activity_targets = activity.addressed(); let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; self.fetch_object(&oid).await?; diff --git a/uriproxy/Cargo.toml b/uriproxy/Cargo.toml new file mode 100644 index 0000000..5747179 --- /dev/null +++ b/uriproxy/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "uriproxy" +version = "0.1.0" +edition = "2021" +authors = [ "alemi " ] +description = "internal upub crate to handle remote uris" +license = "MIT" +keywords = ["upub", "uri", "base64"] +repository = "https://moonlit.technology/alemi/upub" + +[lib] + +[dependencies] +base64 = "0.22" diff --git a/uriproxy/src/lib.rs b/uriproxy/src/lib.rs new file mode 100644 index 0000000..41dc53f --- /dev/null +++ b/uriproxy/src/lib.rs @@ -0,0 +1,47 @@ +use base64::Engine; + +#[derive(Clone, Copy)] +pub enum UriClass { + User, + Object, + Activity, + Context, +} + +impl AsRef for UriClass { + fn as_ref(&self) -> &str { + match self { + Self::User => "users", + Self::Object => "objects", + Self::Activity => "activities", + Self::Context => "context", + } + } +} + +/// unpack uri in id if valid, otherwise compose full uri with "{base}/{entity}/{id}" +pub fn uri(base: &str, entity: UriClass, id: &str) -> String { + if id.starts_with('~') { // ready-to-use base64-encoded id + if let Ok(bytes) = base64::prelude::BASE64_STANDARD.decode(id) { + if let Ok(uri) = std::str::from_utf8(&bytes) { + return uri.to_string(); + } + } + } + format!("{}/{}/{}", base, entity.as_ref(), id) +} + +/// decompose local id constructed by uri() fn +pub fn decompose_id(full_id: &str) -> String { + full_id // https://example.org/users/test/followers/page?offset=42 + .split('/') // ['https:', '', 'example.org', 'users', 'test', 'followers', 'page?offset=42' ] + .nth(4) // 'test' + .unwrap_or("") + .to_string() +} + +/// encode with base64 remote url and prefix it with ~ +pub fn compact_id(uri: &str) -> String { + let encoded = base64::prelude::BASE64_STANDARD.encode(uri.as_bytes()); + format!("~{encoded}") +} diff --git a/web/Cargo.toml b/web/Cargo.toml index db71b72..74ac725 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -27,6 +27,7 @@ leptos_router = { version = "0.6", features = ["csr"] } leptos-use = { version = "0.10", features = ["serde"] } reqwest = { version = "0.12", features = ["json"] } apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activitypub-counters", "litepub"] } +uriproxy = { path = "../uriproxy/" } futures = "0.3.30" lazy_static = "1.4" chrono = { version = "0.4", features = ["serde"] } diff --git a/web/src/components/activity.rs b/web/src/components/activity.rs index e1298c2..bccf1f8 100644 --- a/web/src/components/activity.rs +++ b/web/src/components/activity.rs @@ -11,9 +11,9 @@ pub fn ActivityLine(activity: crate::Object) -> impl IntoView { let actor = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone()).into()); let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); let href = match kind { - apb::ActivityType::Follow => Uri::web(FetchKind::User, &object_id), + apb::ActivityType::Follow => Uri::web(U::User, &object_id), // TODO for update check what's being updated - _ => Uri::web(FetchKind::Object, &object_id), + _ => Uri::web(U::Object, &object_id), }; view! {
diff --git a/web/src/components/login.rs b/web/src/components/login.rs index d3320eb..88a84ae 100644 --- a/web/src/components/login.rs +++ b/web/src/components/login.rs @@ -14,7 +14,7 @@ pub fn LoginBox( view! {
- "hi "{move || auth.username() } + "hi "{move || auth.username() } impl IntoView { {object.in_reply_to().id().map(|reply| view! { - reply + reply })} - + "↗" diff --git a/web/src/components/timeline.rs b/web/src/components/timeline.rs index 5761639..a05863d 100644 --- a/web/src/components/timeline.rs +++ b/web/src/components/timeline.rs @@ -187,8 +187,8 @@ async fn process_activities(activities: Vec, auth: Auth) -> V if let Some(object_id) = activity.object().id() { if !gonna_fetch.contains(&object_id) { let fetch_kind = match activity_type { - apb::ActivityType::Follow => FetchKind::User, - _ => FetchKind::Object, + apb::ActivityType::Follow => U::User, + _ => U::Object, }; gonna_fetch.insert(object_id.clone()); sub_tasks.push(Box::pin(fetch_and_update_with_user(fetch_kind, object_id, auth))); @@ -211,20 +211,20 @@ async fn process_activities(activities: Vec, auth: Auth) -> V if let Some(uid) = activity.attributed_to().id() { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { gonna_fetch.insert(uid.clone()); - sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, uid, auth))); + sub_tasks.push(Box::pin(fetch_and_update(U::User, uid, auth))); } } if let Some(uid) = activity.actor().id() { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { gonna_fetch.insert(uid.clone()); - sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, uid, auth))); + sub_tasks.push(Box::pin(fetch_and_update(U::User, uid, auth))); } } } for user in actors_seen { - sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, user, auth))); + sub_tasks.push(Box::pin(fetch_and_update(U::User, user, auth))); } futures::future::join_all(sub_tasks).await; @@ -232,22 +232,22 @@ async fn process_activities(activities: Vec, auth: Auth) -> V out } -async fn fetch_and_update(kind: FetchKind, id: String, auth: Auth) { +async fn fetch_and_update(kind: U, id: String, auth: Auth) { match Http::fetch(&Uri::api(kind, &id, false), auth).await { Ok(data) => CACHE.put(id, Arc::new(data)), Err(e) => console_warn(&format!("could not fetch '{id}': {e}")), } } -async fn fetch_and_update_with_user(kind: FetchKind, id: String, auth: Auth) { - fetch_and_update(kind.clone(), id.clone(), auth).await; +async fn fetch_and_update_with_user(kind: U, id: String, auth: Auth) { + fetch_and_update(kind, id.clone(), auth).await; if let Some(obj) = CACHE.get(&id) { if let Some(actor_id) = match kind { - FetchKind::Object => obj.attributed_to().id(), - FetchKind::Activity => obj.actor().id(), - FetchKind::User | FetchKind::Context => None, + U::Object => obj.attributed_to().id(), + U::Activity => obj.actor().id(), + U::User | U::Context => None, } { - fetch_and_update(FetchKind::User, actor_id, auth).await; + fetch_and_update(U::User, actor_id, auth).await; } } } diff --git a/web/src/components/user.rs b/web/src/components/user.rs index 12a2b9a..0c11d4b 100644 --- a/web/src/components/user.rs +++ b/web/src/components/user.rs @@ -10,7 +10,7 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView { let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); let avatar = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); view! { - + {username}@{domain} } @@ -20,11 +20,11 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView { pub fn ActorBanner(object: crate::Object) -> impl IntoView { match object.as_ref() { serde_json::Value::String(id) => view! { - + }, serde_json::Value::Object(_) => { let uid = object.id().unwrap_or_default().to_string(); - let uri = Uri::web(FetchKind::User, &uid); + let uri = Uri::web(U::User, &uid); let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); let display_name = object.name().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string(); diff --git a/web/src/lib.rs b/web/src/lib.rs index c636b3c..b869bab 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -17,6 +17,7 @@ pub const DEFAULT_AVATAR_URL: &str = "https://cdn.alemi.dev/social/gradient.png" pub const NAME: &str = "μ"; use std::sync::Arc; +use uriproxy::UriClass; @@ -42,7 +43,7 @@ impl ObjectCache { self.0.insert(k, v); } - pub async fn fetch(&self, k: &str, kind: FetchKind) -> reqwest::Result { + pub async fn fetch(&self, k: &str, kind: UriClass) -> reqwest::Result { match self.get(k) { Some(x) => Ok(x), None => { @@ -58,25 +59,6 @@ impl ObjectCache { } -#[derive(Debug, Clone)] -pub enum FetchKind { - User, - Object, - Activity, - Context, -} - -impl AsRef for FetchKind { - fn as_ref(&self) -> &str { - match self { - Self::User => "users", - Self::Object => "objects", - Self::Activity => "activities", - Self::Context => "context", - } - } -} - pub struct Http; impl Http { @@ -121,28 +103,24 @@ impl Http { pub struct Uri; impl Uri { - pub fn full(kind: FetchKind, id: &str) -> String { - let kind = kind.as_ref(); - if id.starts_with('+') { - id.replace('+', "https://").replace('@', "/") - } else { - format!("{URL_BASE}/{kind}/{id}") - } + pub fn full(kind: UriClass, id: &str) -> String { + uriproxy::uri(URL_BASE, kind, id) } pub fn pretty(url: &str) -> String { + let bare = url.replace("https://", ""); if url.len() < 50 { - url.replace("https://", "") + bare } else { - format!("{}..", url.replace("https://", "").get(..50).unwrap_or_default()) + format!("{}..", bare.get(..50).unwrap_or_default()) }.replace('/', "\u{200B}/\u{200B}") } pub fn short(url: &str) -> String { if url.starts_with(URL_BASE) { - url.split('/').last().unwrap_or_default().to_string() + uriproxy::decompose_id(url) } else { - url.replace("https://", "+").replace('/', "@") + uriproxy::compact_id(url) } } @@ -154,7 +132,7 @@ impl Uri { /// - https://other.domain.net/unexpected/path/root /// - +other.domain.net@users@root /// - root - pub fn web(kind: FetchKind, url: &str) -> String { + pub fn web(kind: UriClass, url: &str) -> String { let kind = kind.as_ref(); format!("/web/{kind}/{}", Self::short(url)) } @@ -167,7 +145,7 @@ impl Uri { /// - https://other.domain.net/unexpected/path/root /// - +other.domain.net@users@root /// - root - pub fn api(kind: FetchKind, url: &str, fetch: bool) -> String { + pub fn api(kind: UriClass, url: &str, fetch: bool) -> String { let kind = kind.as_ref(); format!("{URL_BASE}/{kind}/{}{}", Self::short(url), if fetch { "?fetch=true" } else { "" }) } diff --git a/web/src/page/debug.rs b/web/src/page/debug.rs index 4c4e726..92ba146 100644 --- a/web/src/page/debug.rs +++ b/web/src/page/debug.rs @@ -33,11 +33,11 @@ pub fn DebugPage() -> impl IntoView { obj " " usr diff --git a/web/src/page/object.rs b/web/src/page/object.rs index 8e6cbac..1ccc1f2 100644 --- a/web/src/page/object.rs +++ b/web/src/page/object.rs @@ -21,19 +21,19 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView { } let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |oid| { async move { - match CACHE.get(&Uri::full(FetchKind::Object, &oid)) { + match CACHE.get(&Uri::full(U::Object, &oid)) { Some(x) => Some(x.clone()), None => { - let obj = Http::fetch::(&Uri::api(FetchKind::Object, &oid, true), auth).await.ok()?; + let obj = Http::fetch::(&Uri::api(U::Object, &oid, true), auth).await.ok()?; let obj = Arc::new(obj); if let Some(author) = obj.attributed_to().id() { if let Ok(user) = Http::fetch::( - &Uri::api(FetchKind::User, &author, true), auth + &Uri::api(U::User, &author, true), auth ).await { - CACHE.put(Uri::full(FetchKind::User, &author), Arc::new(user)); + CACHE.put(Uri::full(U::User, &author), Arc::new(user)); } } - CACHE.put(Uri::full(FetchKind::Object, &oid), obj.clone()); + CACHE.put(Uri::full(U::Object, &oid), obj.clone()); Some(obj) } } @@ -66,7 +66,7 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView { }, Some(Some(o)) => { let object = o.clone(); - let tl_url = format!("{}/page", Uri::api(FetchKind::Context, &o.context().id().unwrap_or_default(), false)); + let tl_url = format!("{}/page", Uri::api(U::Context, &o.context().id().unwrap_or_default(), false)); if !tl.next.get().starts_with(&tl_url) { tl.reset(tl_url); } diff --git a/web/src/page/search.rs b/web/src/page/search.rs index aa6fe5b..cf86996 100644 --- a/web/src/page/search.rs +++ b/web/src/page/search.rs @@ -11,7 +11,7 @@ pub fn SearchPage() -> impl IntoView { let user = create_local_resource( move || use_query_map().get().get("q").cloned().unwrap_or_default(), move |q| { - let user_fetch = Uri::api(FetchKind::User, &q, true); + let user_fetch = Uri::api(U::User, &q, true); async move { Some(Arc::new(Http::fetch::(&user_fetch, auth).await.ok()?)) } } ); @@ -19,7 +19,7 @@ pub fn SearchPage() -> impl IntoView { let object = create_local_resource( move || use_query_map().get().get("q").cloned().unwrap_or_default(), move |q| { - let object_fetch = Uri::api(FetchKind::Object, &q, true); + let object_fetch = Uri::api(U::Object, &q, true); async move { Some(Arc::new(Http::fetch::(&object_fetch, auth).await.ok()?)) } } ); diff --git a/web/src/page/user.rs b/web/src/page/user.rs index ae59fc5..f84460a 100644 --- a/web/src/page/user.rs +++ b/web/src/page/user.rs @@ -36,12 +36,12 @@ pub fn UserPage(tl: Timeline) -> impl IntoView { } let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |id| { async move { - match CACHE.get(&Uri::full(FetchKind::User, &id)) { + match CACHE.get(&Uri::full(U::User, &id)) { Some(x) => Some(x.clone()), None => { - let user : serde_json::Value = Http::fetch(&Uri::api(FetchKind::User, &id, true), auth).await.ok()?; + let user : serde_json::Value = Http::fetch(&Uri::api(U::User, &id, true), auth).await.ok()?; let user = Arc::new(user); - CACHE.put(Uri::full(FetchKind::User, &id), user.clone()); + CACHE.put(Uri::full(U::User, &id), user.clone()); Some(user) }, } @@ -89,7 +89,7 @@ pub fn UserPage(tl: Timeline) -> impl IntoView { let following = object.following_count().unwrap_or(0); let followers = object.followers_count().unwrap_or(0); let statuses = object.statuses_count().unwrap_or(0); - let tl_url = format!("{}/outbox/page", Uri::api(FetchKind::User, &id.clone(), false)); + let tl_url = format!("{}/outbox/page", Uri::api(U::User, &id.clone(), false)); if !tl.next.get().starts_with(&tl_url) { tl.reset(tl_url); } diff --git a/web/src/prelude.rs b/web/src/prelude.rs index 3b0f844..36fb1e5 100644 --- a/web/src/prelude.rs +++ b/web/src/prelude.rs @@ -1,7 +1,9 @@ pub use crate::{ - Http, Uri, FetchKind, + Http, Uri, CACHE, URL_BASE, auth::{Auth, AuthToken}, page::*, components::*, }; + +pub use uriproxy::UriClass as U;