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",
"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"

View file

@ -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"

View file

@ -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(())
}

View file

@ -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 {

View file

@ -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()),
)

View file

@ -84,5 +84,5 @@ pub async fn register(
registration.banner_url
).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,
) -> 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())

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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());
}

View file

@ -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('@') {

View file

@ -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()),

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() {
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

View file

@ -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

View file

@ -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 {

View file

@ -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>> {

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> {
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
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"] }
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"] }

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 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>

View file

@ -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"));

View file

@ -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>

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 !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;
}
}
}

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 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();

View file

@ -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 { "" })
}

View file

@ -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>

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| {
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);
}

View file

@ -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()?)) }
}
);

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| {
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);
}

View file

@ -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;