Compare commits

..

No commits in common. "8386854ed712e7936764f1fe234029dea28c61a7" and "ecc277a1f076532c4bc0cd88378f994086c8578a" have entirely different histories.

20 changed files with 245 additions and 426 deletions

View file

@ -10,9 +10,6 @@ pub use config::Config;
pub mod init;
pub mod ext;
pub mod selector;
pub use selector::Query;
pub use traits::normalize::AP;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View file

@ -1,4 +1,5 @@
use sea_orm::entity::prelude::*;
use apb::{ActivityMut, ObjectMut};
use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, FromQueryResult, Iterable, Order, QueryOrder, QuerySelect, SelectColumns};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "addressing")]
@ -73,3 +74,116 @@ impl Related<super::object::Entity> 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<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,6 +1,8 @@
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 {
@ -45,3 +47,50 @@ impl Model {
.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
}
}

View file

@ -1,224 +0,0 @@
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 sea_orm::{ColumnTrait, QueryFilter, TransactionTrait};
use upub::{model, selector::{BatchFillable, RichActivity}, traits::Fetcher, Context};
use upub::{model::{self, addressing::Event, attachment::BatchFillable}, traits::Fetcher, Context};
use apb::LD;
use crate::{builders::JsonLD, AuthIdentity};
@ -23,16 +23,17 @@ pub async fn view(
}
}
let row = upub::Query::activities(auth.my_id())
let row = model::addressing::Entity::find_addressed(auth.my_id())
.filter(model::activity::Column::Id.eq(&aid))
.filter(auth.filter_activities())
.into_model::<RichActivity>()
.filter(auth.filter_condition())
.into_model::<Event>()
.one(ctx.db())
.await?
.ok_or_else(crate::ApiError::not_found)?
.with_attachments(ctx.db())
.await?;
.ok_or_else(crate::ApiError::not_found)?;
Ok(JsonLD(row.ap().ld_context()))
let mut attachments = row.load_attachments_batch(ctx.db()).await?;
let attach = attachments.remove(&row.internal());
Ok(JsonLD(row.ap(attach).ld_context()))
}

View file

@ -1,47 +0,0 @@
use axum::extract::{Path, Query, State};
use sea_orm::{sea_query::IntoCondition, ColumnTrait};
use upub::Context;
use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity, Identity};
pub async fn get(
State(ctx): State<Context>,
Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity,
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
match auth {
Identity::Anonymous => Err(crate::ApiError::forbidden()),
Identity::Remote { .. } => Err(crate::ApiError::forbidden()),
Identity::Local { id: user, .. } => if ctx.uid(&id) == user {
crate::builders::collection(&upub::url!(ctx, "/actors/{id}/feed"), None)
} else {
Err(crate::ApiError::forbidden())
},
}
}
pub async fn page(
State(ctx): State<Context>,
Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>,
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
let Identity::Local { id: uid, internal } = &auth else {
// local inbox is only for local users
return Err(crate::ApiError::forbidden());
};
if uid != &ctx.uid(&id) {
return Err(crate::ApiError::forbidden());
}
crate::builders::paginate_activities(
upub::url!(ctx, "/actors/{id}/feed/page"),
upub::model::addressing::Column::Actor.eq(*internal).into_condition(),
ctx.db(),
page,
auth.my_id(),
false,
)
.await
}

View file

@ -23,10 +23,6 @@ pub async fn view(
.set_actor_type(Some(apb::ActorType::Application))
.set_name(Some(&ctx.cfg().instance.name))
.set_summary(Some(&ctx.cfg().instance.description))
.set_streams(apb::Node::links(vec![
upub::url!(ctx, "/feed"),
upub::url!(ctx, "/local"),
]))
.set_inbox(apb::Node::link(upub::url!(ctx, "/inbox")))
.set_outbox(apb::Node::link(upub::url!(ctx, "/outbox")))
.set_published(Some(ctx.actor().published))

View file

@ -1,29 +0,0 @@
use axum::extract::{Query, State};
use upub::Context;
use crate::{AuthIdentity, builders::JsonLD};
use super::Pagination;
pub async fn get(
State(ctx): State<Context>,
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
crate::builders::collection(&upub::url!(ctx, "/feed"), None)
}
pub async fn page(
State(ctx): State<Context>,
AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>,
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
crate::builders::paginate_objects(
upub::url!(ctx, "/feed/page"),
auth.filter_objects(),
ctx.db(),
page,
auth.my_id(),
false,
)
.await
}

View file

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

View file

@ -1,7 +1,6 @@
pub mod actor;
pub mod user;
pub mod inbox;
pub mod outbox;
pub mod feed;
pub mod object;
pub mod activity;
pub mod application;
@ -25,14 +24,13 @@ impl ActivityPubRouter for Router<upub::Context> {
.route("/proxy", post(ap::application::proxy_form))
.route("/proxy", get(ap::application::proxy_get))
.route("/proxy/:uri", get(ap::application::proxy_path))
// TODO shared inboxes and instance stream will come later, just use users *boxes for now
.route("/inbox", post(ap::inbox::post))
.route("/inbox", get(ap::inbox::get))
.route("/inbox/page", get(ap::inbox::page))
.route("/outbox", post(ap::outbox::post))
.route("/outbox", get(ap::outbox::get))
.route("/outbox/page", get(ap::outbox::page))
.route("/feed", get(ap::feed::get))
.route("/feed/page", get(ap::feed::page))
// AUTH routes
.route("/auth", put(ap::auth::register))
.route("/auth", post(ap::auth::login))
@ -44,19 +42,17 @@ impl ActivityPubRouter for Router<upub::Context> {
.route("/.well-known/oauth-authorization-server", get(ap::well_known::oauth_authorization_server))
.route("/nodeinfo/:version", get(ap::well_known::nodeinfo))
// actor routes
.route("/actors/:id", get(ap::actor::view))
.route("/actors/:id/inbox", post(ap::actor::inbox::post))
.route("/actors/:id/inbox", get(ap::actor::inbox::get))
.route("/actors/:id/inbox/page", get(ap::actor::inbox::page))
.route("/actors/:id/outbox", post(ap::actor::outbox::post))
.route("/actors/:id/outbox", get(ap::actor::outbox::get))
.route("/actors/:id/outbox/page", get(ap::actor::outbox::page))
.route("/actors/:id/feed", get(ap::actor::feed::get))
.route("/actors/:id/feed/page", get(ap::actor::feed::page))
.route("/actors/:id/followers", get(ap::actor::following::get::<false>))
.route("/actors/:id/followers/page", get(ap::actor::following::page::<false>))
.route("/actors/:id/following", get(ap::actor::following::get::<true>))
.route("/actors/:id/following/page", get(ap::actor::following::page::<true>))
.route("/actors/:id", get(ap::user::view))
.route("/actors/:id/inbox", post(ap::user::inbox::post))
.route("/actors/:id/inbox", get(ap::user::inbox::get))
.route("/actors/:id/inbox/page", get(ap::user::inbox::page))
.route("/actors/:id/outbox", post(ap::user::outbox::post))
.route("/actors/:id/outbox", get(ap::user::outbox::get))
.route("/actors/:id/outbox/page", get(ap::user::outbox::page))
.route("/actors/:id/followers", get(ap::user::following::get::<false>))
.route("/actors/:id/followers/page", get(ap::user::following::page::<false>))
.route("/actors/:id/following", get(ap::user::following::get::<true>))
.route("/actors/:id/following/page", get(ap::user::following::page::<true>))
// activities
.route("/activities/:id", get(ap::activity::view))
// specific object routes

View file

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

View file

@ -1,10 +1,10 @@
pub mod replies;
pub mod context;
use apb::{BaseMut, CollectionMut, ObjectMut, LD};
use apb::{CollectionMut, ObjectMut, LD};
use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns, TransactionTrait};
use upub::{model, selector::RichObject, traits::Fetcher, Context};
use upub::{model::{self, addressing::Event}, traits::Fetcher, Context};
use crate::{builders::JsonLD, AuthIdentity};
@ -27,15 +27,22 @@ pub async fn view(
}
}
let item = upub::Query::objects(auth.my_id())
let item = model::addressing::Entity::find_addressed(auth.my_id())
.filter(model::object::Column::Id.eq(&oid))
.filter(auth.filter_objects())
.into_model::<RichObject>()
.filter(auth.filter_condition())
.into_model::<Event>()
.one(ctx.db())
.await?
.ok_or_else(crate::ApiError::not_found)?;
let attachments = item.object.find_related(model::attachment::Entity)
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)
.all(ctx.db())
.await?
.into_iter()
@ -45,9 +52,9 @@ pub async fn view(
let mut replies = apb::Node::Empty;
if ctx.cfg().security.show_reply_ids {
let replies_ids = upub::Query::objects(auth.my_id())
let replies_ids = model::addressing::Entity::find_addressed(None)
.filter(model::object::Column::InReplyTo.eq(oid))
.filter(auth.filter_objects())
.filter(auth.filter_condition())
.select_only()
.select_column(model::object::Column::Id)
.into_tuple::<String>()
@ -56,16 +63,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(item.object.replies as u64))
.set_total_items(Some(object.replies as u64))
.set_items(apb::Node::links(replies_ids))
);
}
Ok(JsonLD(
item.ap()
object.ap()
.set_attachment(apb::Node::array(attachments))
.set_replies(replies)
.ld_context()

View file

@ -17,8 +17,8 @@ pub async fn get(
// ctx.fetch_thread(&oid).await?;
// }
let count = upub::Query::objects(auth.my_id())
.filter(auth.filter_objects())
let count = model::addressing::Entity::find_addressed(auth.my_id())
.filter(auth.filter_condition())
.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_objects(
crate::builders::paginate(
page_id,
Condition::all()
.add(auth.filter_objects())
.add(auth.filter_condition())
.add(model::object::Column::InReplyTo.eq(oid)),
ctx.db(),
page,

View file

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

View file

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

View file

@ -4,8 +4,6 @@ pub mod outbox;
pub mod following;
pub mod feed;
use axum::extract::{Path, Query, State};
use apb::{LD, ActorMut, EndpointsMut, Node, ObjectMut};
@ -60,7 +58,6 @@ 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)

View file

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

View file

@ -20,16 +20,7 @@ pub enum Identity {
}
impl Identity {
pub fn filter_activities(&self) -> Condition {
let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
match self {
Identity::Anonymous => base_cond,
Identity::Remote { internal, .. } => base_cond.add(upub::model::addressing::Column::Instance.eq(*internal)),
Identity::Local { internal, .. } => base_cond.add(upub::model::addressing::Column::Actor.eq(*internal)),
}
}
pub fn filter_objects(&self) -> Condition {
pub fn filter_condition(&self) -> Condition {
let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
match self {
Identity::Anonymous => base_cond,
@ -37,6 +28,7 @@ impl Identity {
// TODO should we allow all users on same server to see? or just specific user??
Identity::Local { id, internal } => base_cond
.add(upub::model::addressing::Column::Actor.eq(*internal))
.add(upub::model::activity::Column::Actor.eq(id))
.add(upub::model::object::Column::AttributedTo.eq(id)),
}
}

View file

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