diff --git a/Cargo.toml b/Cargo.toml index 2e50e2cc..2d5f0b34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ sea-orm = { version = "0.12", features = ["macros", "sqlx-sqlite", "runtime-toki reqwest = { version = "0.12", features = ["json"] } axum = "0.7" tower-http = { version = "0.5", features = ["cors", "trace"] } -apb = { path = "apb", features = ["unstructured", "orm"] } +apb = { path = "apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters"] } # nodeinfo = "0.0.2" # the version on crates.io doesn't re-export necessary types to build the struct!!! nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs", rev = "e865094804" } # migrations diff --git a/src/model/addressing.rs b/src/model/addressing.rs index f4fc607f..fe855569 100644 --- a/src/model/addressing.rs +++ b/src/model/addressing.rs @@ -1,4 +1,4 @@ -use apb::{ActivityMut, CollectionMut, ObjectMut}; +use apb::{ActivityMut, ObjectMut}; use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, FromQueryResult, Iterable, Order, QueryOrder, QuerySelect, SelectColumns}; use crate::routes::activitypub::jsonld::LD; @@ -98,36 +98,18 @@ impl Event { match self { Event::Activity(x) => x.ap(), Event::DeepActivity { activity, object, liked } => - activity.ap().set_object(apb::Node::object({ - let likes = object.likes; - let mut obj = object.ap() - .set_attachment(attachment); - if let Some(liked) = liked { - obj = obj.set_audience(apb::Node::object( // TODO setting this again ewww... - serde_json::Value::new_object() - .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(likes as u64)) - .set_ordered_items(apb::Node::links(vec![liked])) - )); - } - obj - })), + activity.ap().set_object(apb::Node::object( + object.ap() + .set_attachment(attachment) + .set_liked_by_me(if liked.is_some() { Some(true) } else { None }) + )), Event::StrayObject { object, liked } => serde_json::Value::new_object() .set_activity_type(Some(apb::ActivityType::Activity)) - .set_object(apb::Node::object({ - let likes = object.likes; - let mut obj = object.ap() - .set_attachment(attachment); - if let Some(liked) = liked { - obj = obj.set_audience(apb::Node::object( // TODO setting this again ewww... - serde_json::Value::new_object() - .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(likes as u64)) - .set_ordered_items(apb::Node::links(vec![liked])) - )); - } - obj - })), + .set_object(apb::Node::object( + object.ap() + .set_attachment(attachment) + .set_liked_by_me(if liked.is_some() { Some(true) } else { None }) + )), Event::Tombstone => serde_json::Value::new_object() .set_activity_type(Some(apb::ActivityType::Activity)) .set_object(apb::Node::object( diff --git a/src/model/object.rs b/src/model/object.rs index 1c20ad19..a08b2ce1 100644 --- a/src/model/object.rs +++ b/src/model/object.rs @@ -1,4 +1,4 @@ -use apb::{BaseMut, Object, Collection, CollectionMut, ObjectMut}; +use apb::{BaseMut, Collection, CollectionMut, ObjectMut}; use sea_orm::entity::prelude::*; use crate::routes::activitypub::jsonld::LD; @@ -41,21 +41,12 @@ impl Model { context: object.context().id(), in_reply_to: object.in_reply_to().id(), published: object.published().ok_or(super::FieldError("published"))?, - comments: object.replies() - .get() + comments: object.replies().get() + .map_or(0, |x| x.total_items().unwrap_or(0)) as i64, + likes: object.likes().get() + .map_or(0, |x| x.total_items().unwrap_or(0)) as i64, + shares: object.shares().get() .map_or(0, |x| x.total_items().unwrap_or(0)) as i64, - likes: object.audience() - .get() - .map_or(0, |x| - x.as_collection() - .map_or(0, |x| x.total_items().unwrap_or(0)) - ) as i64, - shares: object.generator() - .get() - .map_or(0, |x| - x.as_collection() - .map_or(0, |x| x.total_items().unwrap_or(0)) - ) as i64, to: object.to().into(), bto: object.bto().into(), cc: object.cc().into(), @@ -81,12 +72,12 @@ impl Model { .set_cc(apb::Node::links(self.cc.0.clone())) .set_bcc(apb::Node::Empty) .set_sensitive(Some(self.sensitive)) - .set_generator(apb::Node::object( + .set_shares(apb::Node::object( serde_json::Value::new_object() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.shares as u64)) )) - .set_audience(apb::Node::object( + .set_likes(apb::Node::object( serde_json::Value::new_object() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.likes as u64)) diff --git a/src/model/user.rs b/src/model/user.rs index c724495f..58e9cc0f 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -use apb::{Actor, ActorMut, ActorType, BaseMut, Collection, CollectionMut, DocumentMut, Object, ObjectMut, PublicKey, PublicKeyMut}; +use apb::{Actor, ActorMut, ActorType, BaseMut, DocumentMut, Object, ObjectMut, PublicKey, PublicKeyMut}; use crate::routes::activitypub::jsonld::LD; @@ -59,9 +59,9 @@ impl Model { following: object.following().id(), created: object.published().unwrap_or(chrono::Utc::now()), updated: chrono::Utc::now(), - following_count: object.generator().get().map_or(0, |f| f.as_collection().map_or(0, |f| f.total_items().unwrap_or(0))) as i64, - followers_count: object.audience().get().map_or(0, |f| f.as_collection().map_or(0, |f| f.total_items().unwrap_or(0))) as i64, - statuses_count: object.replies().get().map_or(0, |o| o.total_items().unwrap_or(0)) as i64, + following_count: object.following_count().unwrap_or(0) as i64, + followers_count: object.followers_count().unwrap_or(0) as i64, + statuses_count: object.statuses_count().unwrap_or(0) as i64, public_key: object.public_key().get().ok_or(super::FieldError("publicKey"))?.public_key_pem().to_string(), private_key: None, // there's no way to transport privkey over AP json, must come from DB }) @@ -85,24 +85,9 @@ impl Model { ))) .set_published(Some(self.created)) .set_preferred_username(Some(&self.preferred_username)) - .set_replies(apb::Node::object( - serde_json::Value::new_object() - .set_id(self.outbox.as_deref()) - .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(self.statuses_count as u64)) - )) - .set_audience(apb::Node::object( - serde_json::Value::new_object() - .set_id(self.followers.as_deref()) - .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(self.followers_count as u64)) - )) - .set_generator(apb::Node::object( - serde_json::Value::new_object() - .set_id(self.following.as_deref()) - .set_collection_type(Some(apb::CollectionType::OrderedCollection)) - .set_total_items(Some(self.following_count as u64)) - )) + .set_statuses_count(Some(self.statuses_count as u64)) + .set_followers_count(Some(self.followers_count as u64)) + .set_following_count(Some(self.following_count as u64)) .set_inbox(apb::Node::maybe_link(self.inbox)) .set_outbox(apb::Node::maybe_link(self.outbox)) .set_following(apb::Node::maybe_link(self.following)) diff --git a/src/routes/activitypub/user/mod.rs b/src/routes/activitypub/user/mod.rs index 127297f1..aa7b4ab7 100644 --- a/src/routes/activitypub/user/mod.rs +++ b/src/routes/activitypub/user/mod.rs @@ -7,7 +7,7 @@ pub mod following; use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; -use apb::{ActorMut, CollectionMut, Node, Object, ObjectMut}; +use apb::{ActorMut, Node, ObjectMut}; use crate::{errors::UpubError, model::{self, user}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url}; use super::{jsonld::LD, JsonLD, TryFetch}; @@ -27,6 +27,37 @@ pub async fn view( if auth.is_local() && query.fetch && !ctx.is_local(&uid) { ctx.fetch_user(&uid).await?; } + + let (followed_by_me, following_me) = match auth.my_id() { + None => (None, None), + Some(my_id) => { + // TODO these two queries are fast because of indexes but still are 2 subqueries for each + // user GET, not even parallelized... should really add these as joins on the main query, so + // that it's one roundtrip only + let followed_by_me = model::relation::Entity::find() + .filter(model::relation::Column::Follower.eq(my_id)) + .filter(model::relation::Column::Following.eq(&uid)) + .select_only() + .select_column(model::relation::Column::Follower) + .into_tuple::() + .one(ctx.db()) + .await? + .map(|_| true); + + let following_me = model::relation::Entity::find() + .filter(model::relation::Column::Following.eq(my_id)) + .filter(model::relation::Column::Follower.eq(&uid)) + .select_only() + .select_column(model::relation::Column::Follower) + .into_tuple::() + .one(ctx.db()) + .await? + .map(|_| true); + + (followed_by_me, following_me) + }, + }; + match user::Entity::find_by_id(&uid) .find_also_related(model::config::Entity) .one(ctx.db()).await? @@ -37,39 +68,9 @@ pub async fn view( .set_inbox(Node::link(url!(ctx, "/users/{id}/inbox"))) .set_outbox(Node::link(url!(ctx, "/users/{id}/outbox"))) .set_following(Node::link(url!(ctx, "/users/{id}/following"))) - .set_followers(Node::link(url!(ctx, "/users/{id}/followers"))); - - // TODO maybe this thing could be made as a single join, to avoid triple db roundtrip for - // each fetch made by local users? it's indexed and fast but still... - if let Some(my_id) = auth.my_id() { - if !auth.is(&uid) { - let followed_by_me = model::relation::Entity::find() - .filter(model::relation::Column::Follower.eq(my_id)) - .filter(model::relation::Column::Following.eq(&uid)) - .select_only() - .select_column(model::relation::Column::Follower) - .into_tuple::() - .all(ctx.db()) - .await?; - - user - .audience() - .update(|x| x.set_ordered_items(apb::Node::links(followed_by_me))); - - let following_me = model::relation::Entity::find() - .filter(model::relation::Column::Following.eq(my_id)) - .filter(model::relation::Column::Follower.eq(&uid)) - .select_only() - .select_column(model::relation::Column::Following) - .into_tuple::() - .all(ctx.db()) - .await?; - - user - .generator() - .update(|x| x.set_ordered_items(apb::Node::links(following_me))); - } - } + .set_followers(Node::link(url!(ctx, "/users/{id}/followers"))) + .set_following_me(following_me) + .set_followed_by_me(followed_by_me); if !auth.is(&uid) && !cfg.show_followers_count { user = user.set_audience(apb::Node::Empty); @@ -81,44 +82,13 @@ pub async fn view( Ok(JsonLD(user.ld_context())) }, - // remote user TODDO doesn't work? - Some((user_model, None)) => { - let user = user_model.ap(); - - // TODO maybe this thing could be made as a single join, to avoid triple db roundtrip for - // each fetch made by local users? it's indexed and fast but still... - if let Some(my_id) = auth.my_id() { - if !auth.is(&uid) { - let followed_by_me = model::relation::Entity::find() - .filter(model::relation::Column::Follower.eq(my_id)) - .filter(model::relation::Column::Following.eq(&uid)) - .select_only() - .select_column(model::relation::Column::Follower) - .into_tuple::() - .all(ctx.db()) - .await?; - - user - .audience() - .update(|x| x.set_ordered_items(apb::Node::links(followed_by_me))); - - let following_me = model::relation::Entity::find() - .filter(model::relation::Column::Following.eq(my_id)) - .filter(model::relation::Column::Follower.eq(&uid)) - .select_only() - .select_column(model::relation::Column::Following) - .into_tuple::() - .all(ctx.db()) - .await?; - - user - .generator() - .update(|x| x.set_ordered_items(apb::Node::links(following_me))); - } - } - - Ok(JsonLD(user.ld_context())) - }, + // remote user + Some((user_model, None)) => Ok(JsonLD( + user_model.ap() + .set_following_me(following_me) + .set_followed_by_me(followed_by_me) + .ld_context() + )), None => Err(UpubError::not_found()), } }