forked from alemi/upub
feat: way smarter way to represent remote ids
base64 em basically. big commit because touches basically everything!!
This commit is contained in:
parent
27073138ae
commit
af3a3fbbb8
32 changed files with 182 additions and 147 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ pub async fn view(
|
|||
AuthIdentity(auth): AuthIdentity,
|
||||
Query(query): Query<TryFetch>,
|
||||
) -> 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) {
|
||||
let obj = ctx.fetch_activity(&aid).await?;
|
||||
if obj.id != aid {
|
||||
|
|
|
@ -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()),
|
||||
)
|
||||
|
|
|
@ -84,5 +84,5 @@ pub async fn register(
|
|||
registration.banner_url
|
||||
).await?;
|
||||
|
||||
Ok(Json(ctx.uid(registration.username)))
|
||||
Ok(Json(ctx.uid(®istration.username)))
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub async fn get(
|
|||
AuthIdentity(auth): AuthIdentity,
|
||||
) -> crate::Result<JsonLD<serde_json::Value>> {
|
||||
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())
|
||||
|
|
|
@ -14,7 +14,7 @@ pub async fn view(
|
|||
AuthIdentity(auth): AuthIdentity,
|
||||
Query(query): Query<TryFetch>,
|
||||
) -> 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) {
|
||||
let obj = ctx.fetch_object(&oid).await?;
|
||||
// some implementations serve statuses on different urls than their AP id
|
||||
|
|
|
@ -10,7 +10,7 @@ pub async fn get(
|
|||
Query(q): Query<TryFetch>,
|
||||
) -> crate::Result<JsonLD<serde_json::Value>> {
|
||||
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<JsonLD<serde_json::Value>> {
|
||||
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,
|
||||
|
|
|
@ -11,7 +11,7 @@ pub async fn get<const OUTGOING: bool>(
|
|||
) -> crate::Result<JsonLD<serde_json::Value>> {
|
||||
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<const OUTGOING: bool>(
|
|||
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)
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ pub async fn view(
|
|||
Path(id): Path<String>,
|
||||
Query(query): Query<TryFetch>,
|
||||
) -> 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 id.starts_with('@') {
|
||||
if let Some((user, host)) = id.replacen('@', "", 1).split_once('@') {
|
||||
|
|
|
@ -17,11 +17,7 @@ pub async fn page(
|
|||
Query(page): Query<Pagination>,
|
||||
AuthIdentity(auth): AuthIdentity,
|
||||
) -> crate::Result<JsonLD<serde_json::Value>> {
|
||||
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()),
|
||||
|
|
|
@ -105,12 +105,12 @@ pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<Webfinger
|
|||
if user == ctx.domain() && domain == ctx.domain() {
|
||||
return Ok(JsonRD(JsonResourceDescriptor {
|
||||
subject: format!("acct:{user}@{domain}"),
|
||||
aliases: vec![ctx.base()],
|
||||
aliases: vec![ctx.base().to_string()],
|
||||
links: vec![
|
||||
JsonResourceDescriptorLink {
|
||||
rel: "self".to_string(),
|
||||
link_type: Some("application/ld+json".to_string()),
|
||||
href: Some(ctx.base()),
|
||||
href: Some(ctx.base().to_string()),
|
||||
properties: 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(),
|
||||
}));
|
||||
}
|
||||
let uid = ctx.uid(user.to_string());
|
||||
let uid = ctx.uid(user);
|
||||
match model::user::Entity::find_by_id(uid)
|
||||
.one(ctx.db())
|
||||
.await
|
||||
|
|
|
@ -9,7 +9,7 @@ pub async fn view(
|
|||
AuthIdentity(_auth): AuthIdentity,
|
||||
Path(id): Path<String>
|
||||
) -> 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)
|
||||
.one(ctx.db())
|
||||
.await
|
||||
|
|
|
@ -25,7 +25,7 @@ impl Administrable for super::Context {
|
|||
banner_url: Option<String>,
|
||||
) -> 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 {
|
||||
|
|
|
@ -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<String>) -> crate::Result<Vec<String>> {
|
||||
|
|
|
@ -15,8 +15,8 @@ impl apb::server::Outbox for Context {
|
|||
|
||||
async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result<String> {
|
||||
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<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 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<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();
|
||||
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<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();
|
||||
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<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 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<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 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> {
|
||||
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<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 oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
|
||||
self.fetch_object(&oid).await?;
|
||||
|
|
14
uriproxy/Cargo.toml
Normal file
14
uriproxy/Cargo.toml
Normal 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
47
uriproxy/src/lib.rs
Normal 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}")
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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! {
|
||||
<div>
|
||||
|
|
|
@ -14,7 +14,7 @@ pub fn LoginBox(
|
|||
view! {
|
||||
<div>
|
||||
<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 |_| {
|
||||
token_tx.set(None);
|
||||
home_tl.reset(format!("{URL_BASE}/outbox/page"));
|
||||
|
|
|
@ -176,10 +176,10 @@ pub fn Object(object: crate::Object) -> impl IntoView {
|
|||
<td><ActorBanner object=author /></td>
|
||||
<td class="rev" >
|
||||
{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 />
|
||||
<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() />
|
||||
</a>
|
||||
<sup><small><a class="clean ml-s" href={external_url} target="_blank">"↗"</a></small></sup>
|
||||
|
|
|
@ -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 !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<serde_json::Value>, 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<serde_json::Value>, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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! {
|
||||
<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>
|
||||
</a>
|
||||
}
|
||||
|
@ -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! {
|
||||
<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(_) => {
|
||||
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();
|
||||
|
|
|
@ -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<Object> {
|
||||
pub async fn fetch(&self, k: &str, kind: UriClass) -> reqwest::Result<Object> {
|
||||
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<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;
|
||||
|
||||
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 { "" })
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ pub fn DebugPage() -> impl IntoView {
|
|||
<tr>
|
||||
<td>
|
||||
<small><a
|
||||
href={move|| Uri::web(FetchKind::Object, &query.get())}
|
||||
href={move|| Uri::web(U::Object, &query.get())}
|
||||
>obj</a>
|
||||
" "
|
||||
<a
|
||||
href={move|| Uri::web(FetchKind::User, &query.get())}
|
||||
href={move|| Uri::web(U::User, &query.get())}
|
||||
>usr</a></small>
|
||||
</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>
|
||||
|
|
|
@ -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::<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);
|
||||
if let Some(author) = obj.attributed_to().id() {
|
||||
if let Ok(user) = Http::fetch::<serde_json::Value>(
|
||||
&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);
|
||||
}
|
||||
|
|
|
@ -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::<serde_json::Value>(&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::<serde_json::Value>(&object_fetch, auth).await.ok()?)) }
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue