fix: improved query utils, separated obj/activity

This commit is contained in:
əlemi 2024-06-08 02:17:31 +02:00
parent ecc277a1f0
commit 88915adff7
Signed by: alemi
GPG key ID: A4895B84D311642C
14 changed files with 316 additions and 230 deletions

View file

@ -1,5 +1,4 @@
use apb::{ActivityMut, ObjectMut}; use sea_orm::entity::prelude::*;
use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, FromQueryResult, Iterable, Order, QueryOrder, QuerySelect, SelectColumns};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "addressing")] #[sea_orm(table_name = "addressing")]
@ -74,116 +73,3 @@ impl Related<super::object::Entity> for Entity {
} }
impl ActiveModelBehavior for ActiveModel {} 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<i64>,
},
DeepActivity {
activity: crate::model::activity::Model,
object: crate::model::object::Model,
liked: Option<i64>,
}
}
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<Vec<crate::model::attachment::Model>>) -> 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<Self, sea_orm::DbErr> {
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<i64>) -> Select<Entity> {
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
}
}

View file

@ -1,8 +1,6 @@
use apb::{DocumentMut, DocumentType, ObjectMut}; use apb::{DocumentMut, DocumentType, ObjectMut};
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use super::addressing::Event;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "attachments")] #[sea_orm(table_name = "attachments")]
pub struct Model { pub struct Model {
@ -47,50 +45,3 @@ impl Model {
.set_published(Some(self.published)) .set_published(Some(self.published))
} }
} }
#[async_trait::async_trait]
pub trait BatchFillable {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, DbErr>;
}
#[async_trait::async_trait]
impl BatchFillable for &[Event] {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, DbErr> {
let objects : Vec<crate::model::object::Model> = 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<i64, Vec<Model>> = 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<Event> {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, 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<std::collections::BTreeMap<i64, Vec<Model>>, DbErr> {
let x = vec![self.clone()]; // TODO wasteful clone and vec![] but ehhh convenient
x.load_attachments_batch(db).await
}
}

224
upub/core/src/selector.rs Normal file
View file

@ -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<i64>) -> Select<model::addressing::Entity> {
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<i64>) -> Select<model::addressing::Entity> {
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<i64>,
}
pub struct RichActivity {
pub activity: model::activity::Model,
pub object: Option<model::object::Model>,
pub liked: Option<i64>,
pub attachments: Option<Vec<model::attachment::Model>>,
}
impl FromQueryResult for RichActivity {
fn from_query_result(res: &QueryResult, _pre: &str) -> Result<Self, DbErr> {
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<i64>,
pub attachments: Option<Vec<model::attachment::Model>>,
}
impl FromQueryResult for RichObject {
fn from_query_result(res: &QueryResult, _pre: &str) -> Result<Self, DbErr> {
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<Self, DbErr>;
}
#[async_trait::async_trait]
impl BatchFillable for Vec<RichActivity> {
// TODO 3 iterations... can we make it in less passes?
async fn with_attachments(mut self, tx: &impl ConnectionTrait) -> Result<Self, DbErr> {
let objects : Vec<model::object::Model> = 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<i64, Vec<model::attachment::Model>> = 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<RichObject> {
// TODO 3 iterations... can we make it in less passes?
async fn with_attachments(mut self, db: &impl ConnectionTrait) -> Result<Self, DbErr> {
let objects : Vec<model::object::Model> = self
.iter()
.map(|o| o.object.clone())
.collect();
let attachments = objects.load_many(model::attachment::Entity, db).await?;
let mut out : std::collections::BTreeMap<i64, Vec<model::attachment::Model>> = 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<Self, DbErr> {
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, DbErr> {
self.attachments = Some(
self.object.find_related(model::attachment::Entity)
.all(tx)
.await?
);
Ok(self)
}
}

View file

@ -1,6 +1,6 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, QueryFilter, TransactionTrait}; 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 apb::LD;
use crate::{builders::JsonLD, AuthIdentity}; 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(model::activity::Column::Id.eq(&aid))
.filter(auth.filter_condition()) .filter(auth.filter_activities())
.into_model::<Event>() .into_model::<RichActivity>()
.one(ctx.db()) .one(ctx.db())
.await? .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?; Ok(JsonLD(row.ap().ld_context()))
let attach = attachments.remove(&row.internal());
Ok(JsonLD(row.ap(attach).ld_context()))
} }

View file

@ -1,7 +1,7 @@
use axum::{http::StatusCode, extract::{Path, Query, State}, Json}; use axum::{http::StatusCode, extract::{Path, Query, State}, Json};
use sea_orm::{sea_query::IntoCondition, ColumnTrait};
use sea_orm::{ColumnTrait, Condition}; use upub::Context;
use upub::{model, Context};
use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity, Identity}; use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity, Identity};
@ -35,12 +35,9 @@ pub async fn page(
return Err(crate::ApiError::forbidden()); return Err(crate::ApiError::forbidden());
} }
crate::builders::paginate( crate::builders::paginate_activities(
upub::url!(ctx, "/actors/{id}/inbox/page"), upub::url!(ctx, "/actors/{id}/inbox/page"),
Condition::any() upub::model::addressing::Column::Actor.eq(*internal).into_condition(),
.add(model::addressing::Column::Actor.eq(*internal))
.add(model::object::Column::AttributedTo.eq(uid))
.add(model::activity::Column::Actor.eq(uid)),
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),

View file

@ -4,6 +4,8 @@ pub mod outbox;
pub mod following; pub mod following;
pub mod feed;
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use apb::{LD, ActorMut, EndpointsMut, Node, ObjectMut}; use apb::{LD, ActorMut, EndpointsMut, Node, ObjectMut};
@ -58,6 +60,7 @@ pub async fn view(
let mut user = user_model.ap() let mut user = user_model.ap()
.set_inbox(Node::link(upub::url!(ctx, "/actors/{id}/inbox"))) .set_inbox(Node::link(upub::url!(ctx, "/actors/{id}/inbox")))
.set_outbox(Node::link(upub::url!(ctx, "/actors/{id}/outbox"))) .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_following(Node::link(upub::url!(ctx, "/actors/{id}/following")))
.set_followers(Node::link(upub::url!(ctx, "/actors/{id}/followers"))) .set_followers(Node::link(upub::url!(ctx, "/actors/{id}/followers")))
.set_following_me(following_me) .set_following_me(following_me)

View file

@ -19,10 +19,10 @@ pub async fn page(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::ApiResult<JsonLD<serde_json::Value>> { ) -> crate::ApiResult<JsonLD<serde_json::Value>> {
let uid = ctx.uid(&id); let uid = ctx.uid(&id);
crate::builders::paginate( crate::builders::paginate_activities(
upub::url!(ctx, "/actors/{id}/outbox/page"), upub::url!(ctx, "/actors/{id}/outbox/page"),
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_activities())
.add( .add(
Condition::any() Condition::any()
.add(model::activity::Column::Actor.eq(&uid)) .add(model::activity::Column::Actor.eq(&uid))

View file

@ -1,6 +1,6 @@
use apb::{Activity, ActivityType, Base}; use apb::{Activity, ActivityType, Base};
use axum::{extract::{Query, State}, http::StatusCode, Json}; 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 upub::{model::job::JobType, Context};
use crate::{AuthIdentity, Identity, builders::JsonLD}; use crate::{AuthIdentity, Identity, builders::JsonLD};
@ -19,10 +19,9 @@ pub async fn page(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
) -> crate::ApiResult<JsonLD<serde_json::Value>> { ) -> crate::ApiResult<JsonLD<serde_json::Value>> {
crate::builders::paginate( crate::builders::paginate_activities(
upub::url!(ctx, "/inbox/page"), upub::url!(ctx, "/inbox/page"),
upub::model::addressing::Column::Actor.is_null() auth.filter_activities(),
.into_condition(),
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),

View file

@ -11,8 +11,8 @@ pub async fn get(
) -> crate::ApiResult<JsonLD<serde_json::Value>> { ) -> crate::ApiResult<JsonLD<serde_json::Value>> {
let context = ctx.oid(&id); let context = ctx.oid(&id);
let count = model::addressing::Entity::find_addressed(auth.my_id()) let count = upub::Query::objects(auth.my_id())
.filter(auth.filter_condition()) .filter(auth.filter_objects())
.filter(model::object::Column::Context.eq(&context)) .filter(model::object::Column::Context.eq(&context))
.count(ctx.db()) .count(ctx.db())
.await?; .await?;
@ -28,10 +28,10 @@ pub async fn page(
) -> crate::ApiResult<JsonLD<serde_json::Value>> { ) -> crate::ApiResult<JsonLD<serde_json::Value>> {
let context = ctx.oid(&id); let context = ctx.oid(&id);
crate::builders::paginate( crate::builders::paginate_objects(
upub::url!(ctx, "/objects/{id}/context/page"), upub::url!(ctx, "/objects/{id}/context/page"),
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_objects())
.add(model::object::Column::Context.eq(context)), .add(model::object::Column::Context.eq(context)),
ctx.db(), ctx.db(),
page, page,

View file

@ -1,10 +1,10 @@
pub mod replies; pub mod replies;
pub mod context; pub mod context;
use apb::{CollectionMut, ObjectMut, LD}; use apb::{BaseMut, CollectionMut, ObjectMut, LD};
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns, TransactionTrait}; 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}; 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(model::object::Column::Id.eq(&oid))
.filter(auth.filter_condition()) .filter(auth.filter_objects())
.into_model::<Event>() .into_model::<RichObject>()
.one(ctx.db()) .one(ctx.db())
.await? .await?
.ok_or_else(crate::ApiError::not_found)?; .ok_or_else(crate::ApiError::not_found)?;
let object = match item { let attachments = item.object.find_related(model::attachment::Entity)
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)
.all(ctx.db()) .all(ctx.db())
.await? .await?
.into_iter() .into_iter()
@ -52,9 +45,9 @@ pub async fn view(
let mut replies = apb::Node::Empty; let mut replies = apb::Node::Empty;
if ctx.cfg().security.show_reply_ids { 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(model::object::Column::InReplyTo.eq(oid))
.filter(auth.filter_condition()) .filter(auth.filter_objects())
.select_only() .select_only()
.select_column(model::object::Column::Id) .select_column(model::object::Column::Id)
.into_tuple::<String>() .into_tuple::<String>()
@ -63,16 +56,16 @@ pub async fn view(
replies = apb::Node::object( replies = apb::Node::object(
apb::new() apb::new()
// .set_id(Some(&upub::url!(ctx, "/objects/{id}/replies"))) .set_id(Some(&upub::url!(ctx, "/objects/{id}/replies")))
// .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/replies/page"))) .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/replies/page")))
.set_collection_type(Some(apb::CollectionType::Collection)) .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)) .set_items(apb::Node::links(replies_ids))
); );
} }
Ok(JsonLD( Ok(JsonLD(
object.ap() item.ap()
.set_attachment(apb::Node::array(attachments)) .set_attachment(apb::Node::array(attachments))
.set_replies(replies) .set_replies(replies)
.ld_context() .ld_context()

View file

@ -17,8 +17,8 @@ pub async fn get(
// ctx.fetch_thread(&oid).await?; // ctx.fetch_thread(&oid).await?;
// } // }
let count = model::addressing::Entity::find_addressed(auth.my_id()) let count = upub::Query::objects(auth.my_id())
.filter(auth.filter_condition()) .filter(auth.filter_objects())
.filter(model::object::Column::InReplyTo.eq(oid)) .filter(model::object::Column::InReplyTo.eq(oid))
.count(ctx.db()) .count(ctx.db())
.await?; .await?;
@ -35,10 +35,10 @@ pub async fn page(
let page_id = upub::url!(ctx, "/objects/{id}/replies/page"); let page_id = upub::url!(ctx, "/objects/{id}/replies/page");
let oid = ctx.oid(&id); let oid = ctx.oid(&id);
crate::builders::paginate( crate::builders::paginate_objects(
page_id, page_id,
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_objects())
.add(model::object::Column::InReplyTo.eq(oid)), .add(model::object::Column::InReplyTo.eq(oid)),
ctx.db(), ctx.db(),
page, page,

View file

@ -1,5 +1,5 @@
use axum::{extract::{Query, State}, http::StatusCode, Json}; use axum::{extract::{Query, State}, http::StatusCode, Json};
use sea_orm::{ColumnTrait, Condition}; use sea_orm::{sea_query::IntoCondition, ColumnTrait};
use upub::Context; use upub::Context;
use crate::{activitypub::{CreationResult, Pagination}, AuthIdentity, builders::JsonLD}; use crate::{activitypub::{CreationResult, Pagination}, AuthIdentity, builders::JsonLD};
@ -13,11 +13,9 @@ pub async fn page(
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::ApiResult<JsonLD<serde_json::Value>> { ) -> crate::ApiResult<JsonLD<serde_json::Value>> {
crate::builders::paginate( crate::builders::paginate_activities(
upub::url!(ctx, "/outbox/page"), upub::url!(ctx, "/outbox/page"),
Condition::all() upub::model::actor::Column::Domain.eq(ctx.domain().to_string()).into_condition(),
.add(auth.filter_condition())
.add(upub::model::actor::Column::Domain.eq(ctx.domain().to_string())),
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),

View file

@ -1,14 +1,14 @@
use apb::{BaseMut, CollectionMut, CollectionPageMut, LD}; 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 axum::response::{IntoResponse, Response};
use upub::selector::{BatchFillable, RichActivity, RichObject};
use upub::model::{addressing::Event, attachment::BatchFillable};
use crate::activitypub::Pagination; use crate::activitypub::Pagination;
pub async fn paginate( pub async fn paginate_activities(
id: String, id: String,
filter: Condition, filter: Condition,
db: &DatabaseConnection, db: &impl ConnectionTrait,
page: Pagination, page: Pagination,
my_id: Option<i64>, my_id: Option<i64>,
with_users: bool, // TODO ewww too many arguments for this weird function... 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 limit = page.batch.unwrap_or(20).min(50);
let offset = page.offset.unwrap_or(0); 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 { if with_users {
select = select select = select
@ -28,18 +28,54 @@ pub async fn paginate(
// TODO also limit to only local activities // TODO also limit to only local activities
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.into_model::<Event>() .into_model::<RichActivity>()
.all(db) .all(db)
.await?
.with_attachments(db)
.await?; .await?;
let mut attachments = items.load_attachments_batch(db).await?;
let items : Vec<serde_json::Value> = items let items : Vec<serde_json::Value> = items
.into_iter() .into_iter()
.map(|item| { .map(|item| item.ap())
let attach = attachments.remove(&item.internal()); .collect();
item.ap(attach)
}) 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<i64>,
with_users: bool, // TODO ewww too many arguments for this weird function...
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
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::<RichObject>() // <--- difference two
.all(db)
.await?
.with_attachments(db)
.await?;
let items : Vec<serde_json::Value> = items
.into_iter()
.map(|item| item.ap())
.collect(); .collect();
collection_page(&id, offset, limit, items) collection_page(&id, offset, limit, items)