diff --git a/upub/core/src/model/addressing.rs b/upub/core/src/model/addressing.rs index b6708e37..810bf7cb 100644 --- a/upub/core/src/model/addressing.rs +++ b/upub/core/src/model/addressing.rs @@ -1,5 +1,4 @@ -use apb::{ActivityMut, ObjectMut}; -use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, FromQueryResult, Iterable, Order, QueryOrder, QuerySelect, SelectColumns}; +use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "addressing")] @@ -74,116 +73,3 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} - - - -#[allow(clippy::large_enum_variant)] // tombstone is an outlier, not the norm! this is a beefy enum -#[derive(Debug, Clone)] -pub enum Event { - Tombstone, - Activity(crate::model::activity::Model), - StrayObject { - object: crate::model::object::Model, - liked: Option, - }, - DeepActivity { - activity: crate::model::activity::Model, - object: crate::model::object::Model, - liked: Option, - } -} - - -impl Event { - pub fn internal(&self) -> i64 { - match self { - Event::Tombstone => 0, - Event::Activity(x) => x.internal, - Event::StrayObject { object, liked: _ } => object.internal, - Event::DeepActivity { activity: _, liked: _, object } => object.internal, - } - } - - pub fn ap(self, attachment: Option>) -> serde_json::Value { - let attachment = match attachment { - None => apb::Node::Empty, - Some(vec) => apb::Node::array( - vec.into_iter().map(|x| x.ap()).collect() - ), - }; - match self { - Event::Activity(x) => x.ap(), - Event::DeepActivity { activity, object, liked } => - 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 } => apb::new() - .set_activity_type(Some(apb::ActivityType::Activity)) - .set_object(apb::Node::object( - object.ap() - .set_attachment(attachment) - .set_liked_by_me(if liked.is_some() { Some(true) } else { None }) - )), - Event::Tombstone => apb::new() - .set_activity_type(Some(apb::ActivityType::Activity)) - .set_object(apb::Node::object( - apb::new() - .set_object_type(Some(apb::ObjectType::Tombstone)) - )), - } - } -} - -impl FromQueryResult for Event { - fn from_query_result(res: &sea_orm::QueryResult, _pre: &str) -> Result { - let activity = crate::model::activity::Model::from_query_result(res, crate::model::activity::Entity.table_name()).ok(); - let object = crate::model::object::Model::from_query_result(res, crate::model::object::Entity.table_name()).ok(); - let liked = res.try_get(crate::model::like::Entity.table_name(), &crate::model::like::Column::Actor.to_string()).ok(); - match (activity, object) { - (Some(activity), Some(object)) => Ok(Self::DeepActivity { activity, object, liked }), - (Some(activity), None) => Ok(Self::Activity(activity)), - (None, Some(object)) => Ok(Self::StrayObject { object, liked }), - (None, None) => Ok(Self::Tombstone), - } - } -} - - -impl Entity { - pub fn find_addressed(uid: Option) -> Select { - let mut select = Entity::find() - // .distinct() - .select_only() - .join(sea_orm::JoinType::LeftJoin, Relation::Objects.def()) - .join(sea_orm::JoinType::LeftJoin, Relation::Activities.def()) - .filter( - // TODO ghetto double inner join because i want to filter out tombstones - Condition::any() - .add(crate::model::activity::Column::Id.is_not_null()) - .add(crate::model::object::Column::Id.is_not_null()) - ) - .order_by(Column::Published, Order::Desc); - - if let Some(uid) = uid { - select = select - .join( - sea_orm::JoinType::LeftJoin, - crate::model::object::Relation::Likes.def() - .on_condition(move |_l, _r| crate::model::like::Column::Actor.eq(uid).into_condition()), - ) - .select_column_as(crate::model::like::Column::Actor, format!("{}{}", crate::model::like::Entity.table_name(), crate::model::like::Column::Actor.to_string())); - } - - for col in crate::model::object::Column::iter() { - select = select.select_column_as(col, format!("{}{}", crate::model::object::Entity.table_name(), col.to_string())); - } - - for col in crate::model::activity::Column::iter() { - select = select.select_column_as(col, format!("{}{}", crate::model::activity::Entity.table_name(), col.to_string())); - } - - select - } -} diff --git a/upub/core/src/model/attachment.rs b/upub/core/src/model/attachment.rs index 811d7a8d..cabea9cc 100644 --- a/upub/core/src/model/attachment.rs +++ b/upub/core/src/model/attachment.rs @@ -1,8 +1,6 @@ use apb::{DocumentMut, DocumentType, ObjectMut}; use sea_orm::entity::prelude::*; -use super::addressing::Event; - #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "attachments")] pub struct Model { @@ -47,50 +45,3 @@ impl Model { .set_published(Some(self.published)) } } - -#[async_trait::async_trait] -pub trait BatchFillable { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr>; -} - -#[async_trait::async_trait] -impl BatchFillable for &[Event] { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { - let objects : Vec = self - .iter() - .filter_map(|x| match x { - Event::Tombstone => None, - Event::Activity(_) => None, - Event::StrayObject { object, liked: _ } => Some(object.clone()), - Event::DeepActivity { activity: _, liked: _, object } => Some(object.clone()), - }) - .collect(); - - let attachments = objects.load_many(Entity, db).await?; - - let mut out : std::collections::BTreeMap> = std::collections::BTreeMap::new(); - for attach in attachments.into_iter().flatten() { - match out.entry(attach.object) { - std::collections::btree_map::Entry::Vacant(a) => { a.insert(vec![attach]); }, - std::collections::btree_map::Entry::Occupied(mut e) => { e.get_mut().push(attach); }, - } - } - - Ok(out) - } -} - -#[async_trait::async_trait] -impl BatchFillable for Vec { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { - self.as_slice().load_attachments_batch(db).await - } -} - -#[async_trait::async_trait] -impl BatchFillable for Event { - async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result>, DbErr> { - let x = vec![self.clone()]; // TODO wasteful clone and vec![] but ehhh convenient - x.load_attachments_batch(db).await - } -} diff --git a/upub/core/src/selector.rs b/upub/core/src/selector.rs new file mode 100644 index 00000000..96841e95 --- /dev/null +++ b/upub/core/src/selector.rs @@ -0,0 +1,224 @@ +use apb::{ActivityMut, ObjectMut}; +use sea_orm::{sea_query::IntoCondition, ColumnTrait, Condition, ConnectionTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, LoaderTrait, ModelTrait, Order, QueryFilter, QueryOrder, QueryResult, QuerySelect, RelationTrait, Select, SelectColumns}; + +use crate::model; + +pub struct Query; + +impl Query { + pub fn activities(my_id: Option) -> Select { + let mut select = model::addressing::Entity::find() + // .distinct() + .join(sea_orm::JoinType::InnerJoin, model::addressing::Relation::Activities.def()) + .join(sea_orm::JoinType::LeftJoin, model::addressing::Relation::Objects.def()) + .filter( + // TODO ghetto double inner join because i want to filter out tombstones + Condition::any() + .add(model::activity::Column::Id.is_not_null()) + .add(model::object::Column::Id.is_not_null()) + ) + .order_by(model::addressing::Column::Published, Order::Desc) + .select_only(); + + for col in model::activity::Column::iter() { + select = select.select_column_as(col, format!("{}{}", model::activity::Entity.table_name(), col.to_string())); + } + + for col in model::object::Column::iter() { + select = select.select_column_as(col, format!("{}{}", model::object::Entity.table_name(), col.to_string())); + } + + if let Some(uid) = my_id { + select = select + .join( + sea_orm::JoinType::LeftJoin, + model::object::Relation::Likes.def() + .on_condition(move |_l, _r| model::like::Column::Actor.eq(uid).into_condition()), + ) + .select_column_as(model::like::Column::Actor, format!("{}{}", model::like::Entity.table_name(), model::like::Column::Actor.to_string())); + } + + select + } + + pub fn objects(my_id: Option) -> Select { + let mut select = model::addressing::Entity::find() + // .distinct() + .join(sea_orm::JoinType::InnerJoin, model::addressing::Relation::Objects.def()) + .order_by(model::addressing::Column::Published, Order::Desc) + .select_only(); + + for col in model::object::Column::iter() { + select = select.select_column_as(col, format!("{}{}", model::object::Entity.table_name(), col.to_string())); + } + + if let Some(uid) = my_id { + select = select + .join( + sea_orm::JoinType::LeftJoin, + model::object::Relation::Likes.def() + .on_condition(move |_l, _r| model::like::Column::Actor.eq(uid).into_condition()), + ) + .select_column_as(model::like::Column::Actor, format!("{}{}", model::like::Entity.table_name(), model::like::Column::Actor.to_string())); + } + + select + } +} + + + +#[derive(Debug, Clone)] +pub struct EmbeddedActivity { + pub activity: model::activity::Model, + pub object: model::object::Model, + pub liked: Option, +} + +pub struct RichActivity { + pub activity: model::activity::Model, + pub object: Option, + pub liked: Option, + pub attachments: Option>, +} + +impl FromQueryResult for RichActivity { + fn from_query_result(res: &QueryResult, _pre: &str) -> Result { + Ok(RichActivity { + activity: model::activity::Model::from_query_result(res, model::activity::Entity.table_name())?, + object: model::object::Model::from_query_result(res, model::activity::Entity.table_name()).ok(), + liked: res.try_get(model::like::Entity.table_name(), &model::like::Column::Actor.to_string()).ok(), + attachments: None, + }) + } +} + +impl RichActivity { + pub fn ap(self) -> serde_json::Value { + self.activity.ap() + .set_object(apb::Node::maybe_object( + self.object.map(|x| x.ap() + .set_liked_by_me(if self.liked.is_some() { Some(true) } else { None }) + .set_attachment(match self.attachments { + None => apb::Node::Empty, + Some(vec) => apb::Node::array( + vec.into_iter().map(|x| x.ap()).collect() + ), + }) + ) + )) + } +} + +pub struct RichObject { + pub object: model::object::Model, + pub liked: Option, + pub attachments: Option>, +} + +impl FromQueryResult for RichObject { + fn from_query_result(res: &QueryResult, _pre: &str) -> Result { + Ok(RichObject { + object: model::object::Model::from_query_result(res, model::activity::Entity.table_name())?, + liked: res.try_get(model::like::Entity.table_name(), &model::like::Column::Actor.to_string()).ok(), + attachments: None, + }) + } +} + +impl RichObject { + pub fn ap(self) -> serde_json::Value { + self.object.ap() + .set_liked_by_me(if self.liked.is_some() { Some(true) } else { None }) + } +} + +#[async_trait::async_trait] +pub trait BatchFillable: Sized { + async fn with_attachments(self, tx: &impl ConnectionTrait) -> Result; +} + +#[async_trait::async_trait] +impl BatchFillable for Vec { + // TODO 3 iterations... can we make it in less passes? + async fn with_attachments(mut self, tx: &impl ConnectionTrait) -> Result { + let objects : Vec = self + .iter() + .filter_map(|x| x.object.as_ref()) + .map(|o| o.clone()) + .collect(); + + let attachments = objects.load_many(model::attachment::Entity, tx).await?; + + let mut out : std::collections::BTreeMap> = std::collections::BTreeMap::new(); + for attach in attachments.into_iter().flatten() { + match out.entry(attach.object) { + std::collections::btree_map::Entry::Vacant(a) => { a.insert(vec![attach]); }, + std::collections::btree_map::Entry::Occupied(mut e) => { e.get_mut().push(attach); }, + } + } + + for activity in self.iter_mut() { + if let Some(ref object) = activity.object { + activity.attachments = out.remove(&object.internal); + } + } + + Ok(self) + } +} + +#[async_trait::async_trait] +impl BatchFillable for Vec { + // TODO 3 iterations... can we make it in less passes? + async fn with_attachments(mut self, db: &impl ConnectionTrait) -> Result { + let objects : Vec = self + .iter() + .map(|o| o.object.clone()) + .collect(); + + let attachments = objects.load_many(model::attachment::Entity, db).await?; + + let mut out : std::collections::BTreeMap> = std::collections::BTreeMap::new(); + for attach in attachments.into_iter().flatten() { + match out.entry(attach.object) { + std::collections::btree_map::Entry::Vacant(a) => { a.insert(vec![attach]); }, + std::collections::btree_map::Entry::Occupied(mut e) => { e.get_mut().push(attach); }, + } + } + + for obj in self.iter_mut() { + obj.attachments = out.remove(&obj.object.internal); + } + + Ok(self) + } +} + +#[async_trait::async_trait] +impl BatchFillable for RichActivity { + async fn with_attachments(mut self, tx: &impl ConnectionTrait) -> Result { + if let Some(ref obj) = self.object { + self.attachments = Some( + obj.find_related(model::attachment::Entity) + .all(tx) + .await? + ); + } + + Ok(self) + } +} + +#[async_trait::async_trait] +impl BatchFillable for RichObject { + async fn with_attachments(mut self, tx: &impl ConnectionTrait) -> Result { + self.attachments = Some( + self.object.find_related(model::attachment::Entity) + .all(tx) + .await? + ); + + Ok(self) + } +} diff --git a/upub/routes/src/activitypub/activity.rs b/upub/routes/src/activitypub/activity.rs index 3efd69f9..66f29b6f 100644 --- a/upub/routes/src/activitypub/activity.rs +++ b/upub/routes/src/activitypub/activity.rs @@ -1,6 +1,6 @@ use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, QueryFilter, TransactionTrait}; -use upub::{model::{self, addressing::Event, attachment::BatchFillable}, traits::Fetcher, Context}; +use upub::{model, selector::{BatchFillable, RichActivity}, traits::Fetcher, Context}; use apb::LD; use crate::{builders::JsonLD, AuthIdentity}; @@ -23,17 +23,16 @@ pub async fn view( } } - let row = model::addressing::Entity::find_addressed(auth.my_id()) + let row = upub::Query::activities(auth.my_id()) .filter(model::activity::Column::Id.eq(&aid)) - .filter(auth.filter_condition()) - .into_model::() + .filter(auth.filter_activities()) + .into_model::() .one(ctx.db()) .await? - .ok_or_else(crate::ApiError::not_found)?; + .ok_or_else(crate::ApiError::not_found)? + .with_attachments(ctx.db()) + .await?; - let mut attachments = row.load_attachments_batch(ctx.db()).await?; - let attach = attachments.remove(&row.internal()); - - Ok(JsonLD(row.ap(attach).ld_context())) + Ok(JsonLD(row.ap().ld_context())) } diff --git a/upub/routes/src/activitypub/user/following.rs b/upub/routes/src/activitypub/actor/following.rs similarity index 100% rename from upub/routes/src/activitypub/user/following.rs rename to upub/routes/src/activitypub/actor/following.rs diff --git a/upub/routes/src/activitypub/user/inbox.rs b/upub/routes/src/activitypub/actor/inbox.rs similarity index 84% rename from upub/routes/src/activitypub/user/inbox.rs rename to upub/routes/src/activitypub/actor/inbox.rs index 6ae79918..5c5974c6 100644 --- a/upub/routes/src/activitypub/user/inbox.rs +++ b/upub/routes/src/activitypub/actor/inbox.rs @@ -1,7 +1,7 @@ use axum::{http::StatusCode, extract::{Path, Query, State}, Json}; +use sea_orm::{sea_query::IntoCondition, ColumnTrait}; -use sea_orm::{ColumnTrait, Condition}; -use upub::{model, Context}; +use upub::Context; use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity, Identity}; @@ -35,12 +35,9 @@ pub async fn page( return Err(crate::ApiError::forbidden()); } - crate::builders::paginate( + crate::builders::paginate_activities( upub::url!(ctx, "/actors/{id}/inbox/page"), - Condition::any() - .add(model::addressing::Column::Actor.eq(*internal)) - .add(model::object::Column::AttributedTo.eq(uid)) - .add(model::activity::Column::Actor.eq(uid)), + upub::model::addressing::Column::Actor.eq(*internal).into_condition(), ctx.db(), page, auth.my_id(), diff --git a/upub/routes/src/activitypub/user/mod.rs b/upub/routes/src/activitypub/actor/mod.rs similarity index 97% rename from upub/routes/src/activitypub/user/mod.rs rename to upub/routes/src/activitypub/actor/mod.rs index 91a0daf9..50088a00 100644 --- a/upub/routes/src/activitypub/user/mod.rs +++ b/upub/routes/src/activitypub/actor/mod.rs @@ -4,6 +4,8 @@ pub mod outbox; pub mod following; +pub mod feed; + use axum::extract::{Path, Query, State}; use apb::{LD, ActorMut, EndpointsMut, Node, ObjectMut}; @@ -58,6 +60,7 @@ pub async fn view( let mut user = user_model.ap() .set_inbox(Node::link(upub::url!(ctx, "/actors/{id}/inbox"))) .set_outbox(Node::link(upub::url!(ctx, "/actors/{id}/outbox"))) + .set_streams(Node::link(upub::url!(ctx, "/actors/{id}/feed"))) .set_following(Node::link(upub::url!(ctx, "/actors/{id}/following"))) .set_followers(Node::link(upub::url!(ctx, "/actors/{id}/followers"))) .set_following_me(following_me) diff --git a/upub/routes/src/activitypub/user/outbox.rs b/upub/routes/src/activitypub/actor/outbox.rs similarity index 96% rename from upub/routes/src/activitypub/user/outbox.rs rename to upub/routes/src/activitypub/actor/outbox.rs index 5abb87d0..8a3e3c74 100644 --- a/upub/routes/src/activitypub/user/outbox.rs +++ b/upub/routes/src/activitypub/actor/outbox.rs @@ -19,10 +19,10 @@ pub async fn page( AuthIdentity(auth): AuthIdentity, ) -> crate::ApiResult> { let uid = ctx.uid(&id); - crate::builders::paginate( + crate::builders::paginate_activities( upub::url!(ctx, "/actors/{id}/outbox/page"), Condition::all() - .add(auth.filter_condition()) + .add(auth.filter_activities()) .add( Condition::any() .add(model::activity::Column::Actor.eq(&uid)) diff --git a/upub/routes/src/activitypub/inbox.rs b/upub/routes/src/activitypub/inbox.rs index 1f97be13..9cb59741 100644 --- a/upub/routes/src/activitypub/inbox.rs +++ b/upub/routes/src/activitypub/inbox.rs @@ -1,6 +1,6 @@ use apb::{Activity, ActivityType, Base}; use axum::{extract::{Query, State}, http::StatusCode, Json}; -use sea_orm::{sea_query::IntoCondition, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait}; +use sea_orm::{ActiveValue::{NotSet, Set}, ColumnTrait, Condition, EntityTrait}; use upub::{model::job::JobType, Context}; use crate::{AuthIdentity, Identity, builders::JsonLD}; @@ -19,10 +19,9 @@ pub async fn page( AuthIdentity(auth): AuthIdentity, Query(page): Query, ) -> crate::ApiResult> { - crate::builders::paginate( + crate::builders::paginate_activities( upub::url!(ctx, "/inbox/page"), - upub::model::addressing::Column::Actor.is_null() - .into_condition(), + auth.filter_activities(), ctx.db(), page, auth.my_id(), diff --git a/upub/routes/src/activitypub/object/context.rs b/upub/routes/src/activitypub/object/context.rs index bfbdaa31..62d654c2 100644 --- a/upub/routes/src/activitypub/object/context.rs +++ b/upub/routes/src/activitypub/object/context.rs @@ -11,8 +11,8 @@ pub async fn get( ) -> crate::ApiResult> { let context = ctx.oid(&id); - let count = model::addressing::Entity::find_addressed(auth.my_id()) - .filter(auth.filter_condition()) + let count = upub::Query::objects(auth.my_id()) + .filter(auth.filter_objects()) .filter(model::object::Column::Context.eq(&context)) .count(ctx.db()) .await?; @@ -28,10 +28,10 @@ pub async fn page( ) -> crate::ApiResult> { let context = ctx.oid(&id); - crate::builders::paginate( + crate::builders::paginate_objects( upub::url!(ctx, "/objects/{id}/context/page"), Condition::all() - .add(auth.filter_condition()) + .add(auth.filter_objects()) .add(model::object::Column::Context.eq(context)), ctx.db(), page, diff --git a/upub/routes/src/activitypub/object/mod.rs b/upub/routes/src/activitypub/object/mod.rs index d7d7cc8e..406e6d15 100644 --- a/upub/routes/src/activitypub/object/mod.rs +++ b/upub/routes/src/activitypub/object/mod.rs @@ -1,10 +1,10 @@ pub mod replies; pub mod context; -use apb::{CollectionMut, ObjectMut, LD}; +use apb::{BaseMut, CollectionMut, ObjectMut, LD}; use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns, TransactionTrait}; -use upub::{model::{self, addressing::Event}, traits::Fetcher, Context}; +use upub::{model, selector::RichObject, traits::Fetcher, Context}; use crate::{builders::JsonLD, AuthIdentity}; @@ -27,22 +27,15 @@ pub async fn view( } } - let item = model::addressing::Entity::find_addressed(auth.my_id()) + let item = upub::Query::objects(auth.my_id()) .filter(model::object::Column::Id.eq(&oid)) - .filter(auth.filter_condition()) - .into_model::() + .filter(auth.filter_objects()) + .into_model::() .one(ctx.db()) .await? .ok_or_else(crate::ApiError::not_found)?; - let object = match item { - Event::Tombstone => return Err(crate::ApiError::not_found()), - Event::Activity(_) => return Err(crate::ApiError::not_found()), - Event::StrayObject { liked: _, object } => object, - Event::DeepActivity { activity: _, liked: _, object } => object, - }; - - let attachments = object.find_related(model::attachment::Entity) + let attachments = item.object.find_related(model::attachment::Entity) .all(ctx.db()) .await? .into_iter() @@ -52,9 +45,9 @@ pub async fn view( let mut replies = apb::Node::Empty; if ctx.cfg().security.show_reply_ids { - let replies_ids = model::addressing::Entity::find_addressed(None) + let replies_ids = upub::Query::objects(auth.my_id()) .filter(model::object::Column::InReplyTo.eq(oid)) - .filter(auth.filter_condition()) + .filter(auth.filter_objects()) .select_only() .select_column(model::object::Column::Id) .into_tuple::() @@ -63,16 +56,16 @@ pub async fn view( replies = apb::Node::object( apb::new() - // .set_id(Some(&upub::url!(ctx, "/objects/{id}/replies"))) - // .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/replies/page"))) + .set_id(Some(&upub::url!(ctx, "/objects/{id}/replies"))) + .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/replies/page"))) .set_collection_type(Some(apb::CollectionType::Collection)) - .set_total_items(Some(object.replies as u64)) + .set_total_items(Some(item.object.replies as u64)) .set_items(apb::Node::links(replies_ids)) ); } Ok(JsonLD( - object.ap() + item.ap() .set_attachment(apb::Node::array(attachments)) .set_replies(replies) .ld_context() diff --git a/upub/routes/src/activitypub/object/replies.rs b/upub/routes/src/activitypub/object/replies.rs index 6a033aea..6740e9b4 100644 --- a/upub/routes/src/activitypub/object/replies.rs +++ b/upub/routes/src/activitypub/object/replies.rs @@ -17,8 +17,8 @@ pub async fn get( // ctx.fetch_thread(&oid).await?; // } - let count = model::addressing::Entity::find_addressed(auth.my_id()) - .filter(auth.filter_condition()) + let count = upub::Query::objects(auth.my_id()) + .filter(auth.filter_objects()) .filter(model::object::Column::InReplyTo.eq(oid)) .count(ctx.db()) .await?; @@ -35,10 +35,10 @@ pub async fn page( let page_id = upub::url!(ctx, "/objects/{id}/replies/page"); let oid = ctx.oid(&id); - crate::builders::paginate( + crate::builders::paginate_objects( page_id, Condition::all() - .add(auth.filter_condition()) + .add(auth.filter_objects()) .add(model::object::Column::InReplyTo.eq(oid)), ctx.db(), page, diff --git a/upub/routes/src/activitypub/outbox.rs b/upub/routes/src/activitypub/outbox.rs index 4ce68b0b..95dda804 100644 --- a/upub/routes/src/activitypub/outbox.rs +++ b/upub/routes/src/activitypub/outbox.rs @@ -1,5 +1,5 @@ use axum::{extract::{Query, State}, http::StatusCode, Json}; -use sea_orm::{ColumnTrait, Condition}; +use sea_orm::{sea_query::IntoCondition, ColumnTrait}; use upub::Context; use crate::{activitypub::{CreationResult, Pagination}, AuthIdentity, builders::JsonLD}; @@ -13,11 +13,9 @@ pub async fn page( Query(page): Query, AuthIdentity(auth): AuthIdentity, ) -> crate::ApiResult> { - crate::builders::paginate( + crate::builders::paginate_activities( upub::url!(ctx, "/outbox/page"), - Condition::all() - .add(auth.filter_condition()) - .add(upub::model::actor::Column::Domain.eq(ctx.domain().to_string())), + upub::model::actor::Column::Domain.eq(ctx.domain().to_string()).into_condition(), ctx.db(), page, auth.my_id(), diff --git a/upub/routes/src/builders.rs b/upub/routes/src/builders.rs index 95227789..04ef7599 100644 --- a/upub/routes/src/builders.rs +++ b/upub/routes/src/builders.rs @@ -1,14 +1,14 @@ use apb::{BaseMut, CollectionMut, CollectionPageMut, LD}; -use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect, RelationTrait}; +use sea_orm::{Condition, ConnectionTrait, QueryFilter, QuerySelect, RelationTrait}; use axum::response::{IntoResponse, Response}; +use upub::selector::{BatchFillable, RichActivity, RichObject}; -use upub::model::{addressing::Event, attachment::BatchFillable}; use crate::activitypub::Pagination; -pub async fn paginate( +pub async fn paginate_activities( id: String, filter: Condition, - db: &DatabaseConnection, + db: &impl ConnectionTrait, page: Pagination, my_id: Option, with_users: bool, // TODO ewww too many arguments for this weird function... @@ -16,7 +16,7 @@ pub async fn paginate( let limit = page.batch.unwrap_or(20).min(50); let offset = page.offset.unwrap_or(0); - let mut select = upub::model::addressing::Entity::find_addressed(my_id); + let mut select = upub::Query::activities(my_id); if with_users { select = select @@ -28,18 +28,54 @@ pub async fn paginate( // TODO also limit to only local activities .limit(limit) .offset(offset) - .into_model::() + .into_model::() .all(db) + .await? + .with_attachments(db) .await?; - let mut attachments = items.load_attachments_batch(db).await?; - let items : Vec = items .into_iter() - .map(|item| { - let attach = attachments.remove(&item.internal()); - item.ap(attach) - }) + .map(|item| item.ap()) + .collect(); + + collection_page(&id, offset, limit, items) +} + +// TODO can we merge these two??? there are basically only two differences + +pub async fn paginate_objects( + id: String, + filter: Condition, + db: &impl ConnectionTrait, + page: Pagination, + my_id: Option, + with_users: bool, // TODO ewww too many arguments for this weird function... +) -> crate::ApiResult> { + let limit = page.batch.unwrap_or(20).min(50); + let offset = page.offset.unwrap_or(0); + + let mut select = upub::Query::objects(my_id); // <--- difference one + + if with_users { + select = select + .join(sea_orm::JoinType::InnerJoin, upub::model::activity::Relation::Actors.def()); + } + + let items = select + .filter(filter) + // TODO also limit to only local activities + .limit(limit) + .offset(offset) + .into_model::() // <--- difference two + .all(db) + .await? + .with_attachments(db) + .await?; + + let items : Vec = items + .into_iter() + .map(|item| item.ap()) .collect(); collection_page(&id, offset, limit, items)