From 3581dc54e1947ece135240072c433a596ecd8aef Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 15 Jan 2025 01:09:11 +0100 Subject: [PATCH] feat: likes and shares collections on objects --- upub/core/src/model/object.rs | 9 ++- upub/routes/src/activitypub/mod.rs | 4 ++ upub/routes/src/activitypub/object/likes.rs | 60 ++++++++++++++++++++ upub/routes/src/activitypub/object/mod.rs | 2 + upub/routes/src/activitypub/object/shares.rs | 60 ++++++++++++++++++++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 upub/routes/src/activitypub/object/likes.rs create mode 100644 upub/routes/src/activitypub/object/shares.rs diff --git a/upub/core/src/model/object.rs b/upub/core/src/model/object.rs index 977bc89..bc9f54a 100644 --- a/upub/core/src/model/object.rs +++ b/upub/core/src/model/object.rs @@ -162,6 +162,7 @@ impl Entity { impl crate::ext::IntoActivityPub for Model { fn into_activity_pub_json(self, ctx: &crate::Context) -> serde_json::Value { + let is_local = ctx.is_local(&self.id); apb::new() .set_object_type(Some(self.object_type)) .set_attributed_to(apb::Node::maybe_link(self.attributed_to)) @@ -188,18 +189,22 @@ impl crate::ext::IntoActivityPub for Model { .set_sensitive(Some(self.sensitive)) .set_shares(apb::Node::object( apb::new() + .set_id(if is_local { Some(format!("{}/shares", self.id)) } else { None }) + .set_first(if is_local { apb::Node::link(format!("{}/shares/page", self.id)) } else { apb::Node::Empty }) .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.announces as u64)) )) .set_likes(apb::Node::object( apb::new() + .set_id(if is_local { Some(format!("{}/likes", self.id)) } else { None }) + .set_first(if is_local { apb::Node::link(format!("{}/likes/page", self.id)) } else { apb::Node::Empty }) .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.likes as u64)) )) .set_replies(apb::Node::object( apb::new() - .set_id(if ctx.is_local(&self.id) { Some(format!("{}/replies", self.id)) } else { None }) - .set_first( if ctx.is_local(&self.id) { apb::Node::link(format!("{}/replies/page", self.id)) } else { apb::Node::Empty }) + .set_id(if is_local { Some(format!("{}/replies", self.id)) } else { None }) + .set_first( if is_local { apb::Node::link(format!("{}/replies/page", self.id)) } else { apb::Node::Empty }) .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.replies as u64)) )) diff --git a/upub/routes/src/activitypub/mod.rs b/upub/routes/src/activitypub/mod.rs index 2f72e4a..618ab28 100644 --- a/upub/routes/src/activitypub/mod.rs +++ b/upub/routes/src/activitypub/mod.rs @@ -72,6 +72,10 @@ impl ActivityPubRouter for Router { .route("/objects/{id}/replies/page", get(ap::object::replies::page)) .route("/objects/{id}/context", get(ap::object::context::get)) .route("/objects/{id}/context/page", get(ap::object::context::page)) + .route("/objects/{id}/likes", get(ap::object::likes::get)) + .route("/objects/{id}/likes/page", get(ap::object::likes::page)) + .route("/objects/{id}/shares", get(ap::object::shares::get)) + .route("/objects/{id}/shares/page", get(ap::object::shares::page)) // file routes .route("/file", post(ap::file::upload)) .route("/file/{id}", get(ap::file::download)) diff --git a/upub/routes/src/activitypub/object/likes.rs b/upub/routes/src/activitypub/object/likes.rs new file mode 100644 index 0000000..50efc1c --- /dev/null +++ b/upub/routes/src/activitypub/object/likes.rs @@ -0,0 +1,60 @@ +use apb::{BaseMut, CollectionMut, LD}; +use axum::extract::{Path, Query, State}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait}; +use upub::{selector::{RichActivity, RichFillable}, Context}; + +use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity}; + +pub async fn get( + State(ctx): State, + Path(id): Path, +) -> crate::ApiResult> { + let oid = ctx.oid(&id); + + let object = upub::model::object::Entity::find_by_ap_id(&oid) + .one(ctx.db()) + .await? + .ok_or_else(crate::ApiError::not_found)?; + + Ok(JsonLD( + apb::new() + .set_id(Some(upub::url!(ctx, "/objects/{id}/likes"))) + .set_collection_type(Some(apb::CollectionType::Collection)) + .set_total_items(Some(object.likes as u64)) + .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/likes/page"))) + .ld_context() + )) +} + +pub async fn page( + State(ctx): State, + Path(id): Path, + Query(page): Query, + AuthIdentity(auth): AuthIdentity, +) -> crate::ApiResult> { + let oid = ctx.oid(&id); + let internal = upub::model::object::Entity::ap_to_internal(&oid, ctx.db()) + .await? + .ok_or_else(crate::ApiError::not_found)?; + + let (limit, offset) = page.pagination(); + let items = upub::model::like::Entity::find() + .distinct() + .join(sea_orm::JoinType::InnerJoin, upub::model::like::Relation::Activities.def()) + .join(sea_orm::JoinType::InnerJoin, upub::model::activity::Relation::Addressing.def()) + .filter(auth.filter_activities()) + .filter(upub::model::like::Column::Object.eq(internal)) + .order_by_desc(upub::model::like::Column::Published) + .limit(limit) + .offset(offset) + .into_model::() + .all(ctx.db()) + .await? + .load_batched_models(ctx.db()) + .await? + .into_iter() + .map(|item| ctx.ap(item)) + .collect(); + + crate::builders::collection_page(&upub::url!(ctx, "/objects/{id}/likes/page"), page, apb::Node::array(items)) +} diff --git a/upub/routes/src/activitypub/object/mod.rs b/upub/routes/src/activitypub/object/mod.rs index 92a403f..c3ec58b 100644 --- a/upub/routes/src/activitypub/object/mod.rs +++ b/upub/routes/src/activitypub/object/mod.rs @@ -1,5 +1,7 @@ pub mod replies; pub mod context; +pub mod likes; +pub mod shares; use apb::LD; use axum::extract::{Path, Query, State}; diff --git a/upub/routes/src/activitypub/object/shares.rs b/upub/routes/src/activitypub/object/shares.rs new file mode 100644 index 0000000..db65e4b --- /dev/null +++ b/upub/routes/src/activitypub/object/shares.rs @@ -0,0 +1,60 @@ +use apb::{BaseMut, CollectionMut, LD}; +use axum::extract::{Path, Query, State}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait}; +use upub::{selector::{RichActivity, RichFillable}, Context}; + +use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity}; + +pub async fn get( + State(ctx): State, + Path(id): Path, +) -> crate::ApiResult> { + let oid = ctx.oid(&id); + + let object = upub::model::object::Entity::find_by_ap_id(&oid) + .one(ctx.db()) + .await? + .ok_or_else(crate::ApiError::not_found)?; + + Ok(JsonLD( + apb::new() + .set_id(Some(upub::url!(ctx, "/objects/{id}/shares"))) + .set_collection_type(Some(apb::CollectionType::Collection)) + .set_total_items(Some(object.announces as u64)) + .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/shares/page"))) + .ld_context() + )) +} + +pub async fn page( + State(ctx): State, + Path(id): Path, + Query(page): Query, + AuthIdentity(auth): AuthIdentity, +) -> crate::ApiResult> { + let oid = ctx.oid(&id); + let internal = upub::model::object::Entity::ap_to_internal(&oid, ctx.db()) + .await? + .ok_or_else(crate::ApiError::not_found)?; + + let (limit, offset) = page.pagination(); + let items = upub::model::announce::Entity::find() + .distinct() + .join(sea_orm::JoinType::InnerJoin, upub::model::announce::Relation::Activities.def()) + .join(sea_orm::JoinType::InnerJoin, upub::model::activity::Relation::Addressing.def()) + .filter(auth.filter_activities()) + .filter(upub::model::announce::Column::Object.eq(internal)) + .order_by_desc(upub::model::announce::Column::Published) + .limit(limit) + .offset(offset) + .into_model::() + .all(ctx.db()) + .await? + .load_batched_models(ctx.db()) + .await? + .into_iter() + .map(|item| ctx.ap(item)) + .collect(); + + crate::builders::collection_page(&upub::url!(ctx, "/objects/{id}/shares/page"), page, apb::Node::array(items)) +}