Compare commits
3 commits
ecc277a1f0
...
8386854ed7
Author | SHA1 | Date | |
---|---|---|---|
8386854ed7 | |||
07e537e454 | |||
88915adff7 |
20 changed files with 426 additions and 245 deletions
|
@ -10,6 +10,9 @@ 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");
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
224
upub/core/src/selector.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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::<Event>()
|
||||
.filter(auth.filter_activities())
|
||||
.into_model::<RichActivity>()
|
||||
.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()))
|
||||
}
|
||||
|
||||
|
|
47
upub/routes/src/activitypub/actor/feed.rs
Normal file
47
upub/routes/src/activitypub/actor/feed.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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
|
||||
}
|
|
@ -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(),
|
|
@ -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)
|
|
@ -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(
|
||||
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))
|
|
@ -23,6 +23,10 @@ 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))
|
||||
|
|
29
upub/routes/src/activitypub/feed.rs
Normal file
29
upub/routes/src/activitypub/feed.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
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
|
||||
}
|
|
@ -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<Pagination>,
|
||||
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
||||
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(),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
pub mod user;
|
||||
pub mod actor;
|
||||
pub mod inbox;
|
||||
pub mod outbox;
|
||||
pub mod feed;
|
||||
pub mod object;
|
||||
pub mod activity;
|
||||
pub mod application;
|
||||
|
@ -24,13 +25,14 @@ 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))
|
||||
|
@ -42,17 +44,19 @@ 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::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>))
|
||||
.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>))
|
||||
// activities
|
||||
.route("/activities/:id", get(ap::activity::view))
|
||||
// specific object routes
|
||||
|
|
|
@ -11,8 +11,8 @@ pub async fn get(
|
|||
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
||||
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<JsonLD<serde_json::Value>> {
|
||||
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,
|
||||
|
|
|
@ -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::<Event>()
|
||||
.filter(auth.filter_objects())
|
||||
.into_model::<RichObject>()
|
||||
.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::<String>()
|
||||
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Pagination>,
|
||||
AuthIdentity(auth): AuthIdentity,
|
||||
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
||||
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(),
|
||||
|
|
|
@ -20,7 +20,16 @@ pub enum Identity {
|
|||
}
|
||||
|
||||
impl Identity {
|
||||
pub fn filter_condition(&self) -> Condition {
|
||||
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 {
|
||||
let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
|
||||
match self {
|
||||
Identity::Anonymous => base_cond,
|
||||
|
@ -28,7 +37,6 @@ 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)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<i64>,
|
||||
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::<Event>()
|
||||
.into_model::<RichActivity>()
|
||||
.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| {
|
||||
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<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();
|
||||
|
||||
collection_page(&id, offset, limit, items)
|
||||
|
|
Loading…
Reference in a new issue