diff --git a/apb/src/node.rs b/apb/src/node.rs index 7b8c9414..f83e2959 100644 --- a/apb/src/node.rs +++ b/apb/src/node.rs @@ -169,6 +169,14 @@ impl Node { ) } + pub fn maybe_array(values: Vec) -> Self { + if values.is_empty() { + Node::Empty + } else { + Node::array(values) + } + } + #[cfg(feature = "fetch")] pub async fn fetch(&mut self) -> reqwest::Result<&mut Self> { if let Node::Link(link) = self { diff --git a/apb/src/types/object/actor.rs b/apb/src/types/object/actor.rs index 8f9017ff..4a697a6d 100644 --- a/apb/src/types/object/actor.rs +++ b/apb/src/types/object/actor.rs @@ -44,7 +44,7 @@ pub trait Actor : Object { #[cfg(feature = "activitypub-fe")] fn followed_by_me(&self) -> Field { Err(FieldErr("followedByMe")) } #[cfg(feature = "activitypub-fe")] - fn feed(&self) -> Node { Node::Empty } + fn notifications(&self) -> Node { Node::Empty } #[cfg(feature = "activitypub-counters")] fn followers_count(&self) -> Field { Err(FieldErr("followersCount")) } @@ -99,7 +99,7 @@ pub trait ActorMut : ObjectMut { #[cfg(feature = "activitypub-fe")] fn set_followed_by_me(self, val: Option) -> Self; #[cfg(feature = "activitypub-fe")] - fn set_feed(self, val: Node) -> Self; + fn set_notifications(self, val: Node) -> Self; #[cfg(feature = "activitypub-counters")] fn set_followers_count(self, val: Option) -> Self; @@ -155,7 +155,7 @@ impl Actor for serde_json::Value { #[cfg(feature = "activitypub-fe")] crate::getter! { followedByMe -> bool } #[cfg(feature = "activitypub-fe")] - crate::getter! { feed -> node Self::Collection } + crate::getter! { notifications -> node Self::Collection } #[cfg(feature = "activitypub-counters")] crate::getter! { followingCount -> u64 } @@ -206,7 +206,7 @@ impl ActorMut for serde_json::Value { #[cfg(feature = "activitypub-fe")] crate::setter! { followedByMe -> bool } #[cfg(feature = "activitypub-fe")] - crate::setter! { feed -> node Self::Collection } + crate::setter! { notifications -> node Self::Collection } #[cfg(feature = "activitypub-counters")] crate::setter! { followingCount -> u64 } diff --git a/upub/core/src/selector/query.rs b/upub/core/src/selector/query.rs index 16df43f1..543ec7c9 100644 --- a/upub/core/src/selector/query.rs +++ b/upub/core/src/selector/query.rs @@ -85,6 +85,26 @@ impl Query { select } + pub fn notifications(user: i64, show_seen: bool) -> Select { + let mut select = + model::notification::Entity::find() + .join(sea_orm::JoinType::InnerJoin, model::notification::Relation::Activities.def()) + .order_by_desc(model::notification::Column::Published) + .filter(model::notification::Column::Actor.eq(user)); + + if !show_seen { + select = select.filter(model::notification::Column::Seen.eq(false)); + } + + select = select.select_only(); + + for column in model::activity::Column::iter() { + select = select.select_column(column); + } + + select + } + pub fn notify(activity: i64, actor: i64) -> Insert { model::notification::Entity::insert( model::notification::ActiveModel { diff --git a/upub/routes/src/activitypub/actor/mod.rs b/upub/routes/src/activitypub/actor/mod.rs index 66fc2719..ca66128f 100644 --- a/upub/routes/src/activitypub/actor/mod.rs +++ b/upub/routes/src/activitypub/actor/mod.rs @@ -1,6 +1,7 @@ pub mod inbox; pub mod outbox; pub mod following; +pub mod notifications; use axum::extract::{Path, Query, State}; @@ -68,7 +69,7 @@ pub async fn view( )); if auth.is(&uid) { - user = user.set_feed(Node::link(upub::url!(ctx, "/actors/{id}/feed"))); + user = user.set_notifications(Node::link(upub::url!(ctx, "/actors/{id}/notifications"))); } if !auth.is(&uid) && !cfg.show_followers_count { diff --git a/upub/routes/src/activitypub/actor/notifications.rs b/upub/routes/src/activitypub/actor/notifications.rs new file mode 100644 index 00000000..567f0c15 --- /dev/null +++ b/upub/routes/src/activitypub/actor/notifications.rs @@ -0,0 +1,57 @@ +use axum::extract::{Path, Query, State}; +use sea_orm::{PaginatorTrait, QuerySelect}; + +use upub::Context; + +use crate::{activitypub::Pagination, builders::JsonLD, AuthIdentity, Identity}; + +pub async fn get( + State(ctx): State, + Path(id): Path, + AuthIdentity(auth): AuthIdentity, +) -> crate::ApiResult> { + let Identity::Local { id: uid, internal } = &auth else { + // notifications are only for local users + return Err(crate::ApiError::forbidden()); + }; + if uid != &ctx.uid(&id) { + return Err(crate::ApiError::forbidden()); + } + + let count = upub::Query::notifications(*internal, false) + .count(ctx.db()) + .await?; + + crate::builders::collection(&upub::url!(ctx, "/actors/{id}/notifications"), Some(count)) +} + +pub async fn page( + State(ctx): State, + Path(id): Path, + AuthIdentity(auth): AuthIdentity, + Query(page): Query, +) -> crate::ApiResult> { + let Identity::Local { id: uid, internal } = &auth else { + // notifications are only for local users + return Err(crate::ApiError::forbidden()); + }; + if uid != &ctx.uid(&id) { + return Err(crate::ApiError::forbidden()); + } + + let limit = page.batch.unwrap_or(20).min(50); + let offset = page.offset.unwrap_or(0); + + let activities = upub::Query::notifications(*internal, false) + .limit(limit) + .offset(offset) + .into_model::() + .all(ctx.db()) + .await? + .into_iter() + .map(|x| x.ap()) + .collect(); + + crate::builders::collection_page(&upub::url!(ctx, "/actors/{id}/notifications/page"), offset, limit, activities) + +} diff --git a/upub/routes/src/activitypub/mod.rs b/upub/routes/src/activitypub/mod.rs index c1a49e4c..5d61854a 100644 --- a/upub/routes/src/activitypub/mod.rs +++ b/upub/routes/src/activitypub/mod.rs @@ -49,6 +49,8 @@ impl ActivityPubRouter for Router { .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/notifications", get(ap::actor::notifications::get)) + .route("/actors/:id/notifications/page", get(ap::actor::notifications::page)) .route("/actors/:id/followers", get(ap::actor::following::get::)) .route("/actors/:id/followers/page", get(ap::actor::following::page::)) .route("/actors/:id/following", get(ap::actor::following::get::))