feat: way smarter way to represent remote ids

base64 em basically. big commit because touches basically everything!!
This commit is contained in:
əlemi 2024-05-20 06:25:47 +02:00
parent 27073138ae
commit af3a3fbbb8
Signed by: alemi
GPG key ID: A4895B84D311642C
32 changed files with 182 additions and 147 deletions

9
Cargo.lock generated
View file

@ -4489,6 +4489,7 @@ dependencies = [
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uriproxy",
"uuid", "uuid",
] ]
@ -4515,9 +4516,17 @@ dependencies = [
"serde_json", "serde_json",
"thiserror", "thiserror",
"tracing", "tracing",
"uriproxy",
"web-sys", "web-sys",
] ]
[[package]]
name = "uriproxy"
version = "0.1.0"
dependencies = [
"base64 0.22.0",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.0"

View file

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["apb", "web", "mdhtml"] members = ["apb", "web", "mdhtml", "uriproxy"]
[package] [package]
name = "upub" name = "upub"
@ -28,6 +28,7 @@ serde_default = "0.1"
serde-inline-default = "0.2" serde-inline-default = "0.2"
toml = "0.8" toml = "0.8"
mdhtml = { path = "mdhtml", features = ["markdown"] } mdhtml = { path = "mdhtml", features = ["markdown"] }
uriproxy = { path = "uriproxy" }
jrd = "0.1" jrd = "0.1"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"

View file

@ -1,12 +1,12 @@
use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder}; use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder};
pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) -> crate::Result<()> { 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 { let mut activity_model = crate::model::activity::Model {
id: aid.clone(), id: aid.clone(),
activity_type: apb::ActivityType::Follow, activity_type: apb::ActivityType::Follow,
actor: ctx.base(), actor: ctx.base().to_string(),
object: Some(actor.clone()), object: Some(actor.clone()),
target: None, target: None,
published: chrono::Utc::now(), 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()) crate::model::activity::Entity::insert(activity_model.into_active_model())
.exec(ctx.db()).await?; .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(()) Ok(())
} }

View file

@ -10,7 +10,7 @@ pub async fn view(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(query): Query<TryFetch>, Query(query): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let aid = ctx.uri("activities", id); let aid = ctx.aid(&id);
if auth.is_local() && query.fetch && !ctx.is_local(&aid) { if auth.is_local() && query.fetch && !ctx.is_local(&aid) {
let obj = ctx.fetch_activity(&aid).await?; let obj = ctx.fetch_activity(&aid).await?;
if obj.id != aid { if obj.id != aid {

View file

@ -58,7 +58,7 @@ pub async fn proxy_get(
Method::GET, Method::GET,
&query.id, &query.id,
None, None,
&ctx.base(), ctx.base(),
&ctx.app().private_key, &ctx.app().private_key,
&format!("{}+proxy", ctx.domain()), &format!("{}+proxy", ctx.domain()),
) )
@ -82,7 +82,7 @@ pub async fn proxy_form(
Method::GET, Method::GET,
&query.id, &query.id,
None, None,
&ctx.base(), ctx.base(),
&ctx.app().private_key, &ctx.app().private_key,
&format!("{}+proxy", ctx.domain()), &format!("{}+proxy", ctx.domain()),
) )

View file

@ -84,5 +84,5 @@ pub async fn register(
registration.banner_url registration.banner_url
).await?; ).await?;
Ok(Json(ctx.uid(registration.username))) Ok(Json(ctx.uid(&registration.username)))
} }

View file

@ -9,7 +9,7 @@ pub async fn get(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let local_context_id = url!(ctx, "/context/{id}"); 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()) let count = model::addressing::Entity::find_addressed(auth.my_id())
.filter(auth.filter_condition()) .filter(auth.filter_condition())

View file

@ -14,7 +14,7 @@ pub async fn view(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(query): Query<TryFetch>, Query(query): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let oid = ctx.uri("objects", id); let oid = ctx.oid(&id);
if auth.is_local() && query.fetch && !ctx.is_local(&oid) { if auth.is_local() && query.fetch && !ctx.is_local(&oid) {
let obj = ctx.fetch_object(&oid).await?; let obj = ctx.fetch_object(&oid).await?;
// some implementations serve statuses on different urls than their AP id // some implementations serve statuses on different urls than their AP id

View file

@ -10,7 +10,7 @@ pub async fn get(
Query(q): Query<TryFetch>, Query(q): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let replies_id = url!(ctx, "/objects/{id}/replies"); 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 { if auth.is_local() && q.fetch {
ctx.fetch_thread(&oid).await?; ctx.fetch_thread(&oid).await?;
@ -32,7 +32,7 @@ pub async fn page(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let page_id = url!(ctx, "/objects/{id}/replies/page"); let page_id = url!(ctx, "/objects/{id}/replies/page");
let oid = ctx.uri("objects", id); let oid = ctx.oid(&id);
crate::server::builders::paginate( crate::server::builders::paginate(
page_id, page_id,

View file

@ -11,7 +11,7 @@ pub async fn get<const OUTGOING: bool>(
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let follow___ = if OUTGOING { "following" } else { "followers" }; let follow___ = if OUTGOING { "following" } else { "followers" };
let count = model::relation::Entity::find() 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| { .count(ctx.db()).await.unwrap_or_else(|e| {
tracing::error!("failed counting {follow___} for {id}: {e}"); tracing::error!("failed counting {follow___} for {id}: {e}");
0 0
@ -30,7 +30,7 @@ pub async fn page<const OUTGOING: bool>(
let offset = page.offset.unwrap_or(0); let offset = page.offset.unwrap_or(0);
let following = model::relation::Entity::find() 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_only()
.select_column(if OUTGOING { Following } else { Follower }) .select_column(if OUTGOING { Following } else { Follower })
.limit(limit) .limit(limit)

View file

@ -11,7 +11,7 @@ pub async fn get(
match auth { match auth {
Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()), Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()),
Identity::Remote(_) => 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) crate::server::builders::collection(&url!(ctx, "/users/{id}/inbox"), None)
} else { } else {
Err(StatusCode::FORBIDDEN.into()) Err(StatusCode::FORBIDDEN.into())
@ -29,7 +29,7 @@ pub async fn page(
// local inbox is only for local users // local inbox is only for local users
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
}; };
if uid != &ctx.uid(id.clone()) { if uid != &ctx.uid(&id) {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }

View file

@ -19,7 +19,7 @@ pub async fn view(
Path(id): Path<String>, Path(id): Path<String>,
Query(query): Query<TryFetch>, Query(query): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let mut uid = ctx.uri("users", id.clone()); let mut uid = ctx.uid(&id);
if auth.is_local() { if auth.is_local() {
if id.starts_with('@') { if id.starts_with('@') {
if let Some((user, host)) = id.replacen('@', "", 1).split_once('@') { if let Some((user, host)) = id.replacen('@', "", 1).split_once('@') {

View file

@ -17,11 +17,7 @@ pub async fn page(
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let uid = if id.starts_with('+') { let uid = ctx.uid(&id);
format!("https://{}", id.replacen('+', "", 1).replace('@', "/"))
} else {
ctx.uid(id.clone())
};
crate::server::builders::paginate( crate::server::builders::paginate(
url!(ctx, "/users/{id}/outbox/page"), url!(ctx, "/users/{id}/outbox/page"),
Condition::all() Condition::all()
@ -47,7 +43,7 @@ pub async fn post(
match auth { match auth {
Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()), Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()),
Identity::Remote(_) => Err(StatusCode::NOT_IMPLEMENTED.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()); tracing::debug!("processing new local activity: {}", serde_json::to_string(&activity).unwrap_or_default());
match activity.base_type() { match activity.base_type() {
None => Err(StatusCode::BAD_REQUEST.into()), None => Err(StatusCode::BAD_REQUEST.into()),

View file

@ -105,12 +105,12 @@ pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<Webfinger
if user == ctx.domain() && domain == ctx.domain() { if user == ctx.domain() && domain == ctx.domain() {
return Ok(JsonRD(JsonResourceDescriptor { return Ok(JsonRD(JsonResourceDescriptor {
subject: format!("acct:{user}@{domain}"), subject: format!("acct:{user}@{domain}"),
aliases: vec![ctx.base()], aliases: vec![ctx.base().to_string()],
links: vec![ links: vec![
JsonResourceDescriptorLink { JsonResourceDescriptorLink {
rel: "self".to_string(), rel: "self".to_string(),
link_type: Some("application/ld+json".to_string()), link_type: Some("application/ld+json".to_string()),
href: Some(ctx.base()), href: Some(ctx.base().to_string()),
properties: jrd::Map::default(), properties: jrd::Map::default(),
titles: jrd::Map::default(), titles: jrd::Map::default(),
}, },
@ -119,7 +119,7 @@ pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<Webfinger
properties: jrd::Map::default(), properties: jrd::Map::default(),
})); }));
} }
let uid = ctx.uid(user.to_string()); let uid = ctx.uid(user);
match model::user::Entity::find_by_id(uid) match model::user::Entity::find_by_id(uid)
.one(ctx.db()) .one(ctx.db())
.await .await

View file

@ -9,7 +9,7 @@ pub async fn view(
AuthIdentity(_auth): AuthIdentity, AuthIdentity(_auth): AuthIdentity,
Path(id): Path<String> Path(id): Path<String>
) -> Result<Json<Account>, StatusCode> { ) -> Result<Json<Account>, 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) .find_also_related(model::config::Entity)
.one(ctx.db()) .one(ctx.db())
.await .await

View file

@ -25,7 +25,7 @@ impl Administrable for super::Context {
banner_url: Option<String>, banner_url: Option<String>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let key = openssl::rsa::Rsa::generate(2048).unwrap(); 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 db = self.db();
let domain = self.domain().to_string(); let domain = self.domain().to_string();
let user_model = crate::model::user::Model { let user_model = crate::model::user::Model {

View file

@ -4,6 +4,7 @@ use openssl::rsa::Rsa;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set}; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set};
use crate::{config::Config, model, server::fetcher::Fetcher}; use crate::{config::Config, model, server::fetcher::Fetcher};
use uriproxy::UriClass;
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
@ -15,6 +16,7 @@ struct ContextInner {
config: Config, config: Config,
domain: String, domain: String,
protocol: String, protocol: String,
base_url: String,
dispatcher: Dispatcher, dispatcher: Dispatcher,
// TODO keep these pre-parsed // TODO keep these pre-parsed
app: model::application::Model, app: model::application::Model,
@ -72,6 +74,7 @@ impl Context {
.await?; .await?;
Ok(Context(Arc::new(ContextInner { Ok(Context(Arc::new(ContextInner {
base_url: format!("{}{}", protocol, domain),
db, domain, protocol, app, dispatcher, config, db, domain, protocol, app, dispatcher, config,
relays: BTreeSet::from_iter(relays.into_iter()), relays: BTreeSet::from_iter(relays.into_iter()),
}))) })))
@ -97,53 +100,38 @@ impl Context {
&self.0.protocol &self.0.protocol
} }
pub fn base(&self) -> String { pub fn base(&self) -> &str {
format!("{}{}", self.0.protocol, self.0.domain) &self.0.base_url
}
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('/', "@")
}
} }
/// get full user id uri /// get full user id uri
pub fn uid(&self, id: String) -> String { pub fn uid(&self, id: &str) -> String {
self.uri("users", id) uriproxy::uri(self.base(), UriClass::User, id)
} }
/// get full object id uri /// get full object id uri
pub fn oid(&self, id: String) -> String { pub fn oid(&self, id: &str) -> String {
self.uri("objects", id) uriproxy::uri(self.base(), UriClass::Object, id)
} }
/// get full activity id uri /// get full activity id uri
pub fn aid(&self, id: String) -> String { pub fn aid(&self, id: &str) -> String {
self.uri("activities", id) 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 { pub fn server(id: &str) -> String {
id id
@ -156,8 +144,7 @@ impl Context {
} }
pub fn is_local(&self, id: &str) -> bool { pub fn is_local(&self, id: &str) -> bool {
// TODO consider precalculating once this format! id.starts_with(self.base())
id.starts_with(&format!("{}{}", self.0.protocol, self.0.domain))
} }
pub async fn expand_addressing(&self, targets: Vec<String>) -> crate::Result<Vec<String>> { pub async fn expand_addressing(&self, targets: Vec<String>) -> crate::Result<Vec<String>> {

View file

@ -15,8 +15,8 @@ impl apb::server::Outbox for Context {
async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result<String> { async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result<String> {
let raw_oid = uuid::Uuid::new_v4().to_string(); let raw_oid = uuid::Uuid::new_v4().to_string();
let oid = self.oid(raw_oid.clone()); let oid = self.oid(&raw_oid);
let aid = self.aid(uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = object.addressed(); let activity_targets = object.addressed();
let object_model = self.insert_object( let object_model = self.insert_object(
object object
@ -24,7 +24,7 @@ impl apb::server::Outbox for Context {
.set_attributed_to(Node::link(uid.clone())) .set_attributed_to(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now())) .set_published(Some(chrono::Utc::now()))
.set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))), .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?; ).await?;
let activity_model = model::activity::Model { 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 raw_oid = uuid::Uuid::new_v4().to_string();
let oid = self.oid(raw_oid.clone()); let oid = self.oid(&raw_oid);
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 activity_targets = activity.addressed();
self.insert_object( self.insert_object(
@ -67,7 +67,7 @@ impl apb::server::Outbox for Context {
.set_bto(activity.bto()) .set_bto(activity.bto())
.set_cc(activity.cc()) .set_cc(activity.cc())
.set_bcc(activity.bcc()), .set_bcc(activity.bcc()),
Some(self.base()), Some(self.base().to_string()),
).await?; ).await?;
let activity_model = model::activity::Model::new( 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<String> { async fn like(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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 activity_targets = activity.addressed();
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
self.fetch_object(&oid).await?; 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<String> { async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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 activity_targets = activity.addressed();
if activity.object().id().is_none() { if activity.object().id().is_none() {
return Err(UpubError::bad_request()); 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<String> { async fn accept(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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 activity_targets = activity.addressed();
if activity.object().id().is_none() { if activity.object().id().is_none() {
return Err(UpubError::bad_request()); 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<String> { async fn undo(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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 activity_targets = activity.addressed();
let old_aid = activity.object().id().ok_or_else(UpubError::bad_request)?; let old_aid = activity.object().id().ok_or_else(UpubError::bad_request)?;
let old_activity = model::activity::Entity::find_by_id(old_aid) 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<String> { async fn delete(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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 oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
let object = model::object::Entity::find_by_id(&oid) 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<String> { async fn update(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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)?; let object_node = activity.object().extract().ok_or_else(UpubError::bad_request)?;
match object_node.object_type() { 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<String> { async fn announce(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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 activity_targets = activity.addressed();
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
self.fetch_object(&oid).await?; self.fetch_object(&oid).await?;

14
uriproxy/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "uriproxy"
version = "0.1.0"
edition = "2021"
authors = [ "alemi <me@alemi.dev>" ]
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"

47
uriproxy/src/lib.rs Normal file
View file

@ -0,0 +1,47 @@
use base64::Engine;
#[derive(Clone, Copy)]
pub enum UriClass {
User,
Object,
Activity,
Context,
}
impl AsRef<str> 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}")
}

View file

@ -27,6 +27,7 @@ leptos_router = { version = "0.6", features = ["csr"] }
leptos-use = { version = "0.10", features = ["serde"] } leptos-use = { version = "0.10", features = ["serde"] }
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activitypub-counters", "litepub"] } apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activitypub-counters", "litepub"] }
uriproxy = { path = "../uriproxy/" }
futures = "0.3.30" futures = "0.3.30"
lazy_static = "1.4" lazy_static = "1.4"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }

View file

@ -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 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 kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
let href = match kind { 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 // TODO for update check what's being updated
_ => Uri::web(FetchKind::Object, &object_id), _ => Uri::web(U::Object, &object_id),
}; };
view! { view! {
<div> <div>

View file

@ -14,7 +14,7 @@ pub fn LoginBox(
view! { view! {
<div> <div>
<div class="w-100" class:hidden=move || !auth.present() > <div class="w-100" class:hidden=move || !auth.present() >
"hi "<a href={move || Uri::web(FetchKind::User, &auth.username() )} >{move || auth.username() }</a> "hi "<a href={move || Uri::web(U::User, &auth.username() )} >{move || auth.username() }</a>
<input style="float:right" type="submit" value="logout" on:click=move |_| { <input style="float:right" type="submit" value="logout" on:click=move |_| {
token_tx.set(None); token_tx.set(None);
home_tl.reset(format!("{URL_BASE}/outbox/page")); home_tl.reset(format!("{URL_BASE}/outbox/page"));

View file

@ -176,10 +176,10 @@ pub fn Object(object: crate::Object) -> impl IntoView {
<td><ActorBanner object=author /></td> <td><ActorBanner object=author /></td>
<td class="rev" > <td class="rev" >
{object.in_reply_to().id().map(|reply| view! { {object.in_reply_to().id().map(|reply| view! {
<small><i><a class="clean" href={Uri::web(FetchKind::Object, &reply)} title={reply}>reply</a></i></small> <small><i><a class="clean" href={Uri::web(U::Object, &reply)} title={reply}>reply</a></i></small>
})} })}
<PrivacyMarker addressed=addressed /> <PrivacyMarker addressed=addressed />
<a class="clean hover ml-s" href={Uri::web(FetchKind::Object, object.id().unwrap_or_default())}> <a class="clean hover ml-s" href={Uri::web(U::Object, object.id().unwrap_or_default())}>
<DateTime t=object.published() /> <DateTime t=object.published() />
</a> </a>
<sup><small><a class="clean ml-s" href={external_url} target="_blank">""</a></small></sup> <sup><small><a class="clean ml-s" href={external_url} target="_blank">""</a></small></sup>

View file

@ -187,8 +187,8 @@ async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> V
if let Some(object_id) = activity.object().id() { if let Some(object_id) = activity.object().id() {
if !gonna_fetch.contains(&object_id) { if !gonna_fetch.contains(&object_id) {
let fetch_kind = match activity_type { let fetch_kind = match activity_type {
apb::ActivityType::Follow => FetchKind::User, apb::ActivityType::Follow => U::User,
_ => FetchKind::Object, _ => U::Object,
}; };
gonna_fetch.insert(object_id.clone()); gonna_fetch.insert(object_id.clone());
sub_tasks.push(Box::pin(fetch_and_update_with_user(fetch_kind, object_id, auth))); 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<serde_json::Value>, auth: Auth) -> V
if let Some(uid) = activity.attributed_to().id() { if let Some(uid) = activity.attributed_to().id() {
if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
gonna_fetch.insert(uid.clone()); 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 let Some(uid) = activity.actor().id() {
if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
gonna_fetch.insert(uid.clone()); 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 { 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; futures::future::join_all(sub_tasks).await;
@ -232,22 +232,22 @@ async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> V
out 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 { match Http::fetch(&Uri::api(kind, &id, false), auth).await {
Ok(data) => CACHE.put(id, Arc::new(data)), Ok(data) => CACHE.put(id, Arc::new(data)),
Err(e) => console_warn(&format!("could not fetch '{id}': {e}")), Err(e) => console_warn(&format!("could not fetch '{id}': {e}")),
} }
} }
async fn fetch_and_update_with_user(kind: FetchKind, id: String, auth: Auth) { async fn fetch_and_update_with_user(kind: U, id: String, auth: Auth) {
fetch_and_update(kind.clone(), id.clone(), auth).await; fetch_and_update(kind, id.clone(), auth).await;
if let Some(obj) = CACHE.get(&id) { if let Some(obj) = CACHE.get(&id) {
if let Some(actor_id) = match kind { if let Some(actor_id) = match kind {
FetchKind::Object => obj.attributed_to().id(), U::Object => obj.attributed_to().id(),
FetchKind::Activity => obj.actor().id(), U::Activity => obj.actor().id(),
FetchKind::User | FetchKind::Context => None, U::User | U::Context => None,
} { } {
fetch_and_update(FetchKind::User, actor_id, auth).await; fetch_and_update(U::User, actor_id, auth).await;
} }
} }
} }

View file

@ -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 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()); let avatar = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into());
view! { view! {
<a href={Uri::web(FetchKind::User, &actor_id)} class="clean hover"> <a href={Uri::web(U::User, &actor_id)} class="clean hover">
<img src={avatar} class="avatar-inline mr-s" /><b>{username}</b><small>@{domain}</small> <img src={avatar} class="avatar-inline mr-s" /><b>{username}</b><small>@{domain}</small>
</a> </a>
} }
@ -20,11 +20,11 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView {
pub fn ActorBanner(object: crate::Object) -> impl IntoView { pub fn ActorBanner(object: crate::Object) -> impl IntoView {
match object.as_ref() { match object.as_ref() {
serde_json::Value::String(id) => view! { serde_json::Value::String(id) => view! {
<div><b>?</b>" "<a class="clean hover" href={Uri::web(FetchKind::User, id)}>{Uri::pretty(id)}</a></div> <div><b>?</b>" "<a class="clean hover" href={Uri::web(U::User, id)}>{Uri::pretty(id)}</a></div>
}, },
serde_json::Value::Object(_) => { serde_json::Value::Object(_) => {
let uid = object.id().unwrap_or_default().to_string(); 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 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 display_name = object.name().unwrap_or_default().to_string();
let username = object.preferred_username().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string();

View file

@ -17,6 +17,7 @@ pub const DEFAULT_AVATAR_URL: &str = "https://cdn.alemi.dev/social/gradient.png"
pub const NAME: &str = "μ"; pub const NAME: &str = "μ";
use std::sync::Arc; use std::sync::Arc;
use uriproxy::UriClass;
@ -42,7 +43,7 @@ impl ObjectCache {
self.0.insert(k, v); self.0.insert(k, v);
} }
pub async fn fetch(&self, k: &str, kind: FetchKind) -> reqwest::Result<Object> { pub async fn fetch(&self, k: &str, kind: UriClass) -> reqwest::Result<Object> {
match self.get(k) { match self.get(k) {
Some(x) => Ok(x), Some(x) => Ok(x),
None => { None => {
@ -58,25 +59,6 @@ impl ObjectCache {
} }
#[derive(Debug, Clone)]
pub enum FetchKind {
User,
Object,
Activity,
Context,
}
impl AsRef<str> for FetchKind {
fn as_ref(&self) -> &str {
match self {
Self::User => "users",
Self::Object => "objects",
Self::Activity => "activities",
Self::Context => "context",
}
}
}
pub struct Http; pub struct Http;
impl Http { impl Http {
@ -121,28 +103,24 @@ impl Http {
pub struct Uri; pub struct Uri;
impl Uri { impl Uri {
pub fn full(kind: FetchKind, id: &str) -> String { pub fn full(kind: UriClass, id: &str) -> String {
let kind = kind.as_ref(); uriproxy::uri(URL_BASE, kind, id)
if id.starts_with('+') {
id.replace('+', "https://").replace('@', "/")
} else {
format!("{URL_BASE}/{kind}/{id}")
}
} }
pub fn pretty(url: &str) -> String { pub fn pretty(url: &str) -> String {
let bare = url.replace("https://", "");
if url.len() < 50 { if url.len() < 50 {
url.replace("https://", "") bare
} else { } else {
format!("{}..", url.replace("https://", "").get(..50).unwrap_or_default()) format!("{}..", bare.get(..50).unwrap_or_default())
}.replace('/', "\u{200B}/\u{200B}") }.replace('/', "\u{200B}/\u{200B}")
} }
pub fn short(url: &str) -> String { pub fn short(url: &str) -> String {
if url.starts_with(URL_BASE) { if url.starts_with(URL_BASE) {
url.split('/').last().unwrap_or_default().to_string() uriproxy::decompose_id(url)
} else { } else {
url.replace("https://", "+").replace('/', "@") uriproxy::compact_id(url)
} }
} }
@ -154,7 +132,7 @@ impl Uri {
/// - https://other.domain.net/unexpected/path/root /// - https://other.domain.net/unexpected/path/root
/// - +other.domain.net@users@root /// - +other.domain.net@users@root
/// - root /// - root
pub fn web(kind: FetchKind, url: &str) -> String { pub fn web(kind: UriClass, url: &str) -> String {
let kind = kind.as_ref(); let kind = kind.as_ref();
format!("/web/{kind}/{}", Self::short(url)) format!("/web/{kind}/{}", Self::short(url))
} }
@ -167,7 +145,7 @@ impl Uri {
/// - https://other.domain.net/unexpected/path/root /// - https://other.domain.net/unexpected/path/root
/// - +other.domain.net@users@root /// - +other.domain.net@users@root
/// - 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(); let kind = kind.as_ref();
format!("{URL_BASE}/{kind}/{}{}", Self::short(url), if fetch { "?fetch=true" } else { "" }) format!("{URL_BASE}/{kind}/{}{}", Self::short(url), if fetch { "?fetch=true" } else { "" })
} }

View file

@ -33,11 +33,11 @@ pub fn DebugPage() -> impl IntoView {
<tr> <tr>
<td> <td>
<small><a <small><a
href={move|| Uri::web(FetchKind::Object, &query.get())} href={move|| Uri::web(U::Object, &query.get())}
>obj</a> >obj</a>
" " " "
<a <a
href={move|| Uri::web(FetchKind::User, &query.get())} href={move|| Uri::web(U::User, &query.get())}
>usr</a></small> >usr</a></small>
</td> </td>
<td class="w-100"><input class="w-100" type="text" on:input=move|ev| set_query.set(event_target_value(&ev)) placeholder="AP id" /></td> <td class="w-100"><input class="w-100" type="text" on:input=move|ev| set_query.set(event_target_value(&ev)) placeholder="AP id" /></td>

View file

@ -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| { let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |oid| {
async move { async move {
match CACHE.get(&Uri::full(FetchKind::Object, &oid)) { match CACHE.get(&Uri::full(U::Object, &oid)) {
Some(x) => Some(x.clone()), Some(x) => Some(x.clone()),
None => { None => {
let obj = Http::fetch::<serde_json::Value>(&Uri::api(FetchKind::Object, &oid, true), auth).await.ok()?; let obj = Http::fetch::<serde_json::Value>(&Uri::api(U::Object, &oid, true), auth).await.ok()?;
let obj = Arc::new(obj); let obj = Arc::new(obj);
if let Some(author) = obj.attributed_to().id() { if let Some(author) = obj.attributed_to().id() {
if let Ok(user) = Http::fetch::<serde_json::Value>( if let Ok(user) = Http::fetch::<serde_json::Value>(
&Uri::api(FetchKind::User, &author, true), auth &Uri::api(U::User, &author, true), auth
).await { ).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) Some(obj)
} }
} }
@ -66,7 +66,7 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
}, },
Some(Some(o)) => { Some(Some(o)) => {
let object = o.clone(); 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) { if !tl.next.get().starts_with(&tl_url) {
tl.reset(tl_url); tl.reset(tl_url);
} }

View file

@ -11,7 +11,7 @@ pub fn SearchPage() -> impl IntoView {
let user = create_local_resource( let user = create_local_resource(
move || use_query_map().get().get("q").cloned().unwrap_or_default(), move || use_query_map().get().get("q").cloned().unwrap_or_default(),
move |q| { 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::<serde_json::Value>(&user_fetch, auth).await.ok()?)) } async move { Some(Arc::new(Http::fetch::<serde_json::Value>(&user_fetch, auth).await.ok()?)) }
} }
); );
@ -19,7 +19,7 @@ pub fn SearchPage() -> impl IntoView {
let object = create_local_resource( let object = create_local_resource(
move || use_query_map().get().get("q").cloned().unwrap_or_default(), move || use_query_map().get().get("q").cloned().unwrap_or_default(),
move |q| { 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::<serde_json::Value>(&object_fetch, auth).await.ok()?)) } async move { Some(Arc::new(Http::fetch::<serde_json::Value>(&object_fetch, auth).await.ok()?)) }
} }
); );

View file

@ -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| { let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |id| {
async move { async move {
match CACHE.get(&Uri::full(FetchKind::User, &id)) { match CACHE.get(&Uri::full(U::User, &id)) {
Some(x) => Some(x.clone()), Some(x) => Some(x.clone()),
None => { 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); 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) Some(user)
}, },
} }
@ -89,7 +89,7 @@ pub fn UserPage(tl: Timeline) -> impl IntoView {
let following = object.following_count().unwrap_or(0); let following = object.following_count().unwrap_or(0);
let followers = object.followers_count().unwrap_or(0); let followers = object.followers_count().unwrap_or(0);
let statuses = object.statuses_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) { if !tl.next.get().starts_with(&tl_url) {
tl.reset(tl_url); tl.reset(tl_url);
} }

View file

@ -1,7 +1,9 @@
pub use crate::{ pub use crate::{
Http, Uri, FetchKind, Http, Uri,
CACHE, URL_BASE, CACHE, URL_BASE,
auth::{Auth, AuthToken}, auth::{Auth, AuthToken},
page::*, page::*,
components::*, components::*,
}; };
pub use uriproxy::UriClass as U;