From ac1b6ad4001d8d217e0b8c16f46a7b28c84c12d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=97=8D+85CD?= <50108258+kwaa@users.noreply.github.com> Date: Fri, 3 May 2024 09:42:18 +0800 Subject: [PATCH] feat: receive `Announce` activity (#25) * feat: received announce migration * feat: received announce schema * feat(apub/like): support announce * refactor(apub/activities): remove useless struct * feat(api_mastodon): implement reblogged_by * chore(api_mastodon): update openapi * refactor(apub): move like_or_announce type to mod * chore(apub): add activities test * fix: format code * docs: update federation --- FEDERATION.md | 2 +- crates/api_apub/src/users/user_inbox.rs | 4 +- .../routes/statuses/status_favourited_by.rs | 4 +- .../routes/statuses/status_reblogged_by.rs | 46 +++++++- .../mastodon/activities/create_note.json | 62 +++++++++++ .../assets/mastodon/activities/follow.json | 7 ++ .../assets/mastodon/activities/like_page.json | 7 ++ .../mastodon/activities/undo_follow.json | 12 ++ .../mastodon/activities/undo_like_page.json | 12 ++ crates/apub/src/activities/activity_lists.rs | 27 ++--- crates/apub/src/activities/like/like.rs | 77 ------------- crates/apub/src/activities/like/mod.rs | 9 -- .../like_or_announce/db_received_announce.rs | 26 +++++ .../db_received_announce_impl.rs | 26 +++++ .../db_received_like.rs | 0 .../db_received_like_impl.rs | 10 +- .../like_or_announce/like_or_announce.rs | 103 ++++++++++++++++++ .../src/activities/like_or_announce/mod.rs | 33 ++++++ .../undo_like_or_announce.rs} | 24 ++-- crates/apub/src/activities/mod.rs | 12 +- crates/apub/tests/activities.rs | 16 +++ crates/db_migration/src/lib.rs | 2 + .../src/m20240501_000002_received_announce.rs | 48 ++++++++ crates/db_schema/src/lib.rs | 1 + crates/db_schema/src/post.rs | 8 ++ crates/db_schema/src/prelude.rs | 1 + crates/db_schema/src/received_announce.rs | 43 ++++++++ crates/db_schema/src/user.rs | 8 ++ docs/src/others/federation.md | 2 +- 29 files changed, 501 insertions(+), 131 deletions(-) create mode 100644 crates/apub/assets/mastodon/activities/create_note.json create mode 100644 crates/apub/assets/mastodon/activities/follow.json create mode 100644 crates/apub/assets/mastodon/activities/like_page.json create mode 100644 crates/apub/assets/mastodon/activities/undo_follow.json create mode 100644 crates/apub/assets/mastodon/activities/undo_like_page.json delete mode 100644 crates/apub/src/activities/like/like.rs delete mode 100644 crates/apub/src/activities/like/mod.rs create mode 100644 crates/apub/src/activities/like_or_announce/db_received_announce.rs create mode 100644 crates/apub/src/activities/like_or_announce/db_received_announce_impl.rs rename crates/apub/src/activities/{like => like_or_announce}/db_received_like.rs (100%) rename crates/apub/src/activities/{like => like_or_announce}/db_received_like_impl.rs (63%) create mode 100644 crates/apub/src/activities/like_or_announce/like_or_announce.rs create mode 100644 crates/apub/src/activities/like_or_announce/mod.rs rename crates/apub/src/activities/{like/undo_like.rs => like_or_announce/undo_like_or_announce.rs} (57%) create mode 100644 crates/apub/tests/activities.rs create mode 100644 crates/db_migration/src/m20240501_000002_received_announce.rs create mode 100644 crates/db_schema/src/received_announce.rs diff --git a/FEDERATION.md b/FEDERATION.md index b34ee929..418f6dfd 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -32,9 +32,9 @@ The following activities and object types are supported: - `Follow(Actor)`, `Undo(Follow)` - `Create(Note)` - `Like(Note)`, `Undo(Like)` +- `Announce(Note)`, `Undo(Announce)` - Activities are implemented in way that is compatible with Mastodon and other popular ActivityPub servers. diff --git a/crates/api_apub/src/users/user_inbox.rs b/crates/api_apub/src/users/user_inbox.rs index da9a196b..7d77f581 100644 --- a/crates/api_apub/src/users/user_inbox.rs +++ b/crates/api_apub/src/users/user_inbox.rs @@ -4,11 +4,11 @@ use activitypub_federation::{ protocol::context::WithContext, }; use axum::{debug_handler, response::IntoResponse}; -use hatsu_apub::{activities::ServiceInboxActivities, actors::ApubUser}; +use hatsu_apub::{activities::UserInboxActivities, actors::ApubUser}; use hatsu_utils::AppData; #[debug_handler] pub async fn handler(data: Data, activity_data: ActivityData) -> impl IntoResponse { - receive_activity::, ApubUser, AppData>(activity_data, &data) + receive_activity::, ApubUser, AppData>(activity_data, &data) .await } diff --git a/crates/api_mastodon/src/routes/statuses/status_favourited_by.rs b/crates/api_mastodon/src/routes/statuses/status_favourited_by.rs index 7c7e0ae6..b1115be6 100644 --- a/crates/api_mastodon/src/routes/statuses/status_favourited_by.rs +++ b/crates/api_mastodon/src/routes/statuses/status_favourited_by.rs @@ -6,9 +6,6 @@ use sea_orm::{EntityTrait, ModelTrait}; use crate::entities::Account; -// (status = NOT_FOUND, description = "Status does not exist or is private", body = AppError) -// { "error": "Record not found" } - /// See who favourited a status /// /// @@ -18,6 +15,7 @@ use crate::entities::Account; path = "/api/v1/statuses/{id}/favourited_by", responses( (status = OK, description = "A list of accounts who favourited the status", body = Vec), + (status = NOT_FOUND, description = "Status does not exist or is private", body = AppError), ), params( ("id" = String, Path, description = "The ID of the Status in the database.") diff --git a/crates/api_mastodon/src/routes/statuses/status_reblogged_by.rs b/crates/api_mastodon/src/routes/statuses/status_reblogged_by.rs index 45c23a71..1682455d 100644 --- a/crates/api_mastodon/src/routes/statuses/status_reblogged_by.rs +++ b/crates/api_mastodon/src/routes/statuses/status_reblogged_by.rs @@ -1,12 +1,11 @@ use activitypub_federation::config::Data; -use axum::{debug_handler, Json}; +use axum::{debug_handler, extract::Path, Json}; +use hatsu_db_schema::prelude::{Post, ReceivedAnnounce}; use hatsu_utils::{AppData, AppError}; +use sea_orm::{EntityTrait, ModelTrait}; use crate::entities::Account; -// (status = NOT_FOUND, description = "Status does not exist or is private", body = AppError) -// { "error": "Record not found" } - /// See who boosted a status /// /// @@ -16,12 +15,47 @@ use crate::entities::Account; path = "/api/v1/statuses/{id}/reblogged_by", responses( (status = OK, description = "A list of accounts that boosted the status", body = Vec), + (status = NOT_FOUND, description = "Status does not exist or is private", body = AppError), ), params( ("id" = String, Path, description = "The ID of the Status in the database.") ) )] #[debug_handler] -pub async fn status_reblogged_by(data: Data) -> Result>, AppError> { - Ok(Json(vec![Account::primary_account(&data).await?])) +pub async fn status_reblogged_by( + Path(base64_url): Path, + data: Data, +) -> Result>, AppError> { + let base64 = base64_simd::URL_SAFE; + + match base64.decode_to_vec(&base64_url) { + Ok(utf8_url) => match String::from_utf8(utf8_url) { + Ok(url) if url.starts_with("https://") => { + let post_url = hatsu_utils::url::generate_post_url(data.domain(), url)?; + + match Post::find_by_id(&post_url.to_string()) + .one(&data.conn) + .await? + { + Some(post) => { + let handles = post + .find_related(ReceivedAnnounce) + .all(&data.conn) + .await + .unwrap() + .into_iter() + .map(|received_like| async { + Account::from_id(received_like.actor, &data).await.unwrap() + }) + .collect::>(); + + Ok(Json(futures::future::join_all(handles).await)) + }, + _ => Err(AppError::not_found("Record", &base64_url)), + } + }, + _ => Err(AppError::not_found("Record", &base64_url)), + }, + _ => Err(AppError::not_found("Record", &base64_url)), + } } diff --git a/crates/apub/assets/mastodon/activities/create_note.json b/crates/apub/assets/mastodon/activities/create_note.json new file mode 100644 index 00000000..67aea397 --- /dev/null +++ b/crates/apub/assets/mastodon/activities/create_note.json @@ -0,0 +1,62 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri" + } + ], + "id": "https://mastodon.madrid/users/felix/statuses/107224289116410645/activity", + "type": "Create", + "actor": "https://mastodon.madrid/users/felix", + "published": "2021-11-05T11:46:50Z", + "to": [ + "https://mastodon.madrid/users/felix/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://mamot.fr/users/retiolus" + ], + "object": { + "id": "https://mastodon.madrid/users/felix/statuses/107224289116410645", + "type": "Note", + "summary": null, + "inReplyTo": "https://mamot.fr/users/retiolus/statuses/107224244380204526", + "published": "2021-11-05T11:46:50Z", + "url": "https://mastodon.madrid/@felix/107224289116410645", + "attributedTo": "https://mastodon.madrid/users/felix", + "to": [ + "https://mastodon.madrid/users/felix/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://mamot.fr/users/retiolus" + ], + "sensitive": false, + "atomUri": "https://mastodon.madrid/users/felix/statuses/107224289116410645", + "inReplyToAtomUri": "https://mamot.fr/users/retiolus/statuses/107224244380204526", + "conversation": "tag:mamot.fr,2021-11-05:objectId=64635960:objectType=Conversation", + "content": "

@retiolus i have never been disappointed by a thinkpad. if you want to save money, get a model from a few years ago, there isnt a huge difference anyway.

", + "contentMap": { + "en": "

@retiolus i have neverbeendisappointed by a thinkpad. if you want to save money, get a model from a few years ago, there isnt a huge difference anyway.

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "https://mamot.fr/users/retiolus", + "name": "@retiolus@mamot.fr" + } + ], + "replies": { + "id": "https://mastodon.madrid/users/felix/statuses/107224289116410645/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://mastodon.madrid/users/felix/statuses/107224289116410645/replies?only_other_accounts=true&page=true", + "partOf": "https://mastodon.madrid/users/felix/statuses/107224289116410645/replies", + "items": [] + } + } + } +} diff --git a/crates/apub/assets/mastodon/activities/follow.json b/crates/apub/assets/mastodon/activities/follow.json new file mode 100644 index 00000000..9f3651e3 --- /dev/null +++ b/crates/apub/assets/mastodon/activities/follow.json @@ -0,0 +1,7 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://masto.asonix.dog/1ea87517-63c5-4118-8831-460ee641b2cf", + "type": "Follow", + "actor": "https://masto.asonix.dog/users/asonix", + "object": "https://ds9.lemmy.ml/c/testcom" +} diff --git a/crates/apub/assets/mastodon/activities/like_page.json b/crates/apub/assets/mastodon/activities/like_page.json new file mode 100644 index 00000000..646f3c5f --- /dev/null +++ b/crates/apub/assets/mastodon/activities/like_page.json @@ -0,0 +1,7 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://mastodon.madrid/users/felix#likes/212340", + "type": "Like", + "actor": "https://mastodon.madrid/users/felix", + "object": "https://ds9.lemmy.ml/post/147" +} diff --git a/crates/apub/assets/mastodon/activities/undo_follow.json b/crates/apub/assets/mastodon/activities/undo_follow.json new file mode 100644 index 00000000..f269cef5 --- /dev/null +++ b/crates/apub/assets/mastodon/activities/undo_follow.json @@ -0,0 +1,12 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://masto.asonix.dog/users/asonix#follows/449/undo", + "type": "Undo", + "actor": "https://masto.asonix.dog/users/asonix", + "object": { + "id": "https://masto.asonix.dog/1ea87517-63c5-4118-8831-460ee641b2cf", + "type": "Follow", + "actor": "https://masto.asonix.dog/users/asonix", + "object": "https://ds9.lemmy.ml/c/testcom" + } +} diff --git a/crates/apub/assets/mastodon/activities/undo_like_page.json b/crates/apub/assets/mastodon/activities/undo_like_page.json new file mode 100644 index 00000000..06436fda --- /dev/null +++ b/crates/apub/assets/mastodon/activities/undo_like_page.json @@ -0,0 +1,12 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://mastodon.madrid/users/felix#likes/212341/undo", + "type": "Undo", + "actor": "https://mastodon.madrid/users/felix", + "object": { + "id": "https://mastodon.madrid/users/felix#likes/212341", + "type": "Like", + "actor": "https://mastodon.madrid/users/felix", + "object": "https://ds9.lemmy.ml/post/147" + } +} diff --git a/crates/apub/src/activities/activity_lists.rs b/crates/apub/src/activities/activity_lists.rs index b33441c0..b7a39d3b 100644 --- a/crates/apub/src/activities/activity_lists.rs +++ b/crates/apub/src/activities/activity_lists.rs @@ -2,28 +2,23 @@ use activitypub_federation::{config::Data, traits::ActivityHandler}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::activities::{AcceptFollow, CreateOrUpdateNote, Follow, Like, UndoFollow, UndoLike}; +use crate::activities::{ + AcceptFollow, + CreateOrUpdateNote, + Follow, + LikeOrAnnounce, + UndoFollow, + UndoLikeOrAnnounce, +}; #[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] #[enum_delegate::implement(ActivityHandler)] -pub enum SharedInboxActivities { +pub enum UserInboxActivities { CreateOrUpdateNote(CreateOrUpdateNote), Follow(Follow), AcceptFollow(AcceptFollow), UndoFollow(UndoFollow), - Like(Like), - UndoLike(UndoLike), -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -#[enum_delegate::implement(ActivityHandler)] -pub enum ServiceInboxActivities { - CreateOrUpdateNote(CreateOrUpdateNote), - Follow(Follow), - AcceptFollow(AcceptFollow), - UndoFollow(UndoFollow), - Like(Like), - UndoLike(UndoLike), + LikeOrAnnounce(LikeOrAnnounce), + UndoLikeOrAnnounce(UndoLikeOrAnnounce), } diff --git a/crates/apub/src/activities/like/like.rs b/crates/apub/src/activities/like/like.rs deleted file mode 100644 index db4b887c..00000000 --- a/crates/apub/src/activities/like/like.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::ops::Deref; - -use activitypub_federation::{ - config::Data, - fetch::object_id::ObjectId, - kinds::activity::LikeType, - traits::ActivityHandler, -}; -use hatsu_db_schema::{prelude::ReceivedLike, received_like}; -use hatsu_utils::{AppData, AppError}; -use sea_orm::{ - ActiveModelTrait, - ColumnTrait, - Condition, - EntityTrait, - IntoActiveModel, - QueryFilter, -}; -use serde::{Deserialize, Serialize}; -use url::Url; - -use crate::{activities::ApubReceivedLike, actors::ApubUser, objects::ApubPost}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Like { - #[serde(rename = "type")] - pub(crate) kind: LikeType, - pub(crate) id: Url, - pub(crate) actor: ObjectId, - pub(crate) object: ObjectId, -} - -/// receive only -#[async_trait::async_trait] -impl ActivityHandler for Like { - type DataType = AppData; - type Error = AppError; - - fn id(&self) -> &Url { - &self.id - } - - fn actor(&self) -> &Url { - self.actor.inner() - } - - async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { - // TODO - Ok(()) - } - - async fn receive(self, data: &Data) -> Result<(), Self::Error> { - let actor = self.actor.dereference(data).await?; - let object = self.object.dereference_local(data).await?; - - if ReceivedLike::find() - .filter( - Condition::all() - .add(received_like::Column::Actor.eq(&actor.id)) - .add(received_like::Column::Object.eq(&object.id)), - ) - .one(&data.conn) - .await? - .is_none() - { - ApubReceivedLike::from_json(&self)? - .deref() - .clone() - .into_active_model() - .insert(&data.conn) - .await?; - } - - Ok(()) - } -} diff --git a/crates/apub/src/activities/like/mod.rs b/crates/apub/src/activities/like/mod.rs deleted file mode 100644 index f0140ad9..00000000 --- a/crates/apub/src/activities/like/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod db_received_like; -mod db_received_like_impl; - -mod like; -mod undo_like; - -pub use db_received_like::ApubReceivedLike; -pub use like::Like; -pub use undo_like::UndoLike; diff --git a/crates/apub/src/activities/like_or_announce/db_received_announce.rs b/crates/apub/src/activities/like_or_announce/db_received_announce.rs new file mode 100644 index 00000000..fedf0aa8 --- /dev/null +++ b/crates/apub/src/activities/like_or_announce/db_received_announce.rs @@ -0,0 +1,26 @@ +use std::ops::Deref; + +use hatsu_db_schema::received_announce::Model as DbReceivedAnnounce; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ApubReceivedAnnounce(pub(crate) DbReceivedAnnounce); + +impl AsRef for ApubReceivedAnnounce { + fn as_ref(&self) -> &DbReceivedAnnounce { + &self.0 + } +} + +impl Deref for ApubReceivedAnnounce { + type Target = DbReceivedAnnounce; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for ApubReceivedAnnounce { + fn from(u: DbReceivedAnnounce) -> Self { + Self(u) + } +} diff --git a/crates/apub/src/activities/like_or_announce/db_received_announce_impl.rs b/crates/apub/src/activities/like_or_announce/db_received_announce_impl.rs new file mode 100644 index 00000000..42439882 --- /dev/null +++ b/crates/apub/src/activities/like_or_announce/db_received_announce_impl.rs @@ -0,0 +1,26 @@ +use activitypub_federation::kinds::activity::AnnounceType; +use hatsu_db_schema::received_announce::Model as DbReceivedAnnounce; +use hatsu_utils::AppError; +use url::Url; + +use crate::activities::{ApubReceivedAnnounce, LikeOrAnnounce, LikeOrAnnounceType}; + +impl ApubReceivedAnnounce { + pub fn into_json(self) -> Result { + Ok(LikeOrAnnounce { + kind: LikeOrAnnounceType::AnnounceType(AnnounceType::Announce), + id: Url::parse(&self.id)?, + actor: Url::parse(&self.actor)?.into(), + object: Url::parse(&self.object)?.into(), + }) + } + + pub fn from_json(json: &LikeOrAnnounce) -> Result { + Ok(DbReceivedAnnounce { + id: json.id.to_string(), + actor: json.actor.to_string(), + object: json.object.to_string(), + } + .into()) + } +} diff --git a/crates/apub/src/activities/like/db_received_like.rs b/crates/apub/src/activities/like_or_announce/db_received_like.rs similarity index 100% rename from crates/apub/src/activities/like/db_received_like.rs rename to crates/apub/src/activities/like_or_announce/db_received_like.rs diff --git a/crates/apub/src/activities/like/db_received_like_impl.rs b/crates/apub/src/activities/like_or_announce/db_received_like_impl.rs similarity index 63% rename from crates/apub/src/activities/like/db_received_like_impl.rs rename to crates/apub/src/activities/like_or_announce/db_received_like_impl.rs index 9d74ebfd..4c0716aa 100644 --- a/crates/apub/src/activities/like/db_received_like_impl.rs +++ b/crates/apub/src/activities/like_or_announce/db_received_like_impl.rs @@ -3,19 +3,19 @@ use hatsu_db_schema::received_like::Model as DbReceivedLike; use hatsu_utils::AppError; use url::Url; -use crate::activities::{ApubReceivedLike, Like}; +use crate::activities::{ApubReceivedLike, LikeOrAnnounce, LikeOrAnnounceType}; impl ApubReceivedLike { - pub fn into_json(self) -> Result { - Ok(Like { - kind: LikeType::Like, + pub fn into_json(self) -> Result { + Ok(LikeOrAnnounce { + kind: LikeOrAnnounceType::LikeType(LikeType::Like), id: Url::parse(&self.id)?, actor: Url::parse(&self.actor)?.into(), object: Url::parse(&self.object)?.into(), }) } - pub fn from_json(json: &Like) -> Result { + pub fn from_json(json: &LikeOrAnnounce) -> Result { Ok(DbReceivedLike { id: json.id.to_string(), actor: json.actor.to_string(), diff --git a/crates/apub/src/activities/like_or_announce/like_or_announce.rs b/crates/apub/src/activities/like_or_announce/like_or_announce.rs new file mode 100644 index 00000000..4fcc3daa --- /dev/null +++ b/crates/apub/src/activities/like_or_announce/like_or_announce.rs @@ -0,0 +1,103 @@ +use std::ops::Deref; + +use activitypub_federation::{config::Data, fetch::object_id::ObjectId, traits::ActivityHandler}; +use hatsu_db_schema::{ + prelude::{ReceivedAnnounce, ReceivedLike}, + received_announce, + received_like, +}; +use hatsu_utils::{AppData, AppError}; +use sea_orm::{ + ActiveModelTrait, + ColumnTrait, + Condition, + EntityTrait, + IntoActiveModel, + QueryFilter, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{ + activities::{ApubReceivedAnnounce, ApubReceivedLike, LikeOrAnnounceType}, + actors::ApubUser, + objects::ApubPost, +}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LikeOrAnnounce { + #[serde(rename = "type")] + pub(crate) kind: LikeOrAnnounceType, + pub(crate) id: Url, + pub(crate) actor: ObjectId, + pub(crate) object: ObjectId, +} + +/// receive only +#[async_trait::async_trait] +impl ActivityHandler for LikeOrAnnounce { + type DataType = AppData; + type Error = AppError; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { + // TODO + Ok(()) + } + + async fn receive(self, data: &Data) -> Result<(), Self::Error> { + let actor = self.actor.dereference(data).await?; + let object = self.object.dereference_local(data).await?; + + match self.kind { + LikeOrAnnounceType::LikeType(_) => { + if ReceivedLike::find() + .filter( + Condition::all() + .add(received_like::Column::Actor.eq(&actor.id)) + .add(received_like::Column::Object.eq(&object.id)), + ) + .one(&data.conn) + .await? + .is_none() + { + ApubReceivedLike::from_json(&self)? + .deref() + .clone() + .into_active_model() + .insert(&data.conn) + .await?; + } + }, + LikeOrAnnounceType::AnnounceType(_) => { + if ReceivedAnnounce::find() + .filter( + Condition::all() + .add(received_announce::Column::Actor.eq(&actor.id)) + .add(received_announce::Column::Object.eq(&object.id)), + ) + .one(&data.conn) + .await? + .is_none() + { + ApubReceivedAnnounce::from_json(&self)? + .deref() + .clone() + .into_active_model() + .insert(&data.conn) + .await?; + } + }, + } + + Ok(()) + } +} diff --git a/crates/apub/src/activities/like_or_announce/mod.rs b/crates/apub/src/activities/like_or_announce/mod.rs new file mode 100644 index 00000000..d54433bf --- /dev/null +++ b/crates/apub/src/activities/like_or_announce/mod.rs @@ -0,0 +1,33 @@ +use std::fmt::{Display, Formatter, Result}; + +use activitypub_federation::kinds::activity::{AnnounceType, LikeType}; +use serde::{Deserialize, Serialize}; + +mod db_received_announce; +mod db_received_announce_impl; +mod db_received_like; +mod db_received_like_impl; + +mod like_or_announce; +mod undo_like_or_announce; + +pub use db_received_announce::ApubReceivedAnnounce; +pub use db_received_like::ApubReceivedLike; +pub use like_or_announce::LikeOrAnnounce; +pub use undo_like_or_announce::UndoLikeOrAnnounce; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum LikeOrAnnounceType { + LikeType(LikeType), + AnnounceType(AnnounceType), +} + +impl Display for LikeOrAnnounceType { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::LikeType(_) => f.write_str(&LikeType::Like.to_string()), + Self::AnnounceType(_) => f.write_str(&AnnounceType::Announce.to_string()), + } + } +} diff --git a/crates/apub/src/activities/like/undo_like.rs b/crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs similarity index 57% rename from crates/apub/src/activities/like/undo_like.rs rename to crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs index 40521243..7a4371a3 100644 --- a/crates/apub/src/activities/like/undo_like.rs +++ b/crates/apub/src/activities/like_or_announce/undo_like_or_announce.rs @@ -4,27 +4,28 @@ use activitypub_federation::{ kinds::activity::UndoType, traits::ActivityHandler, }; -use hatsu_db_schema::prelude::ReceivedLike; +use hatsu_db_schema::prelude::{ReceivedAnnounce, ReceivedLike}; use hatsu_utils::{AppData, AppError}; use sea_orm::EntityTrait; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{activities::Like, actors::ApubUser}; +use super::LikeOrAnnounceType; +use crate::{activities::LikeOrAnnounce, actors::ApubUser}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct UndoLike { +pub struct UndoLikeOrAnnounce { #[serde(rename = "type")] pub(crate) kind: UndoType, pub(crate) id: Url, pub(crate) actor: ObjectId, - pub(crate) object: Like, + pub(crate) object: LikeOrAnnounce, } /// receive only #[async_trait::async_trait] -impl ActivityHandler for UndoLike { +impl ActivityHandler for UndoLikeOrAnnounce { type DataType = AppData; type Error = AppError; @@ -42,9 +43,16 @@ impl ActivityHandler for UndoLike { } async fn receive(self, data: &Data) -> Result<(), Self::Error> { - ReceivedLike::delete_by_id(self.object.id) - .exec(&data.conn) - .await?; + match self.object.kind { + LikeOrAnnounceType::LikeType(_) => + ReceivedLike::delete_by_id(self.object.id) + .exec(&data.conn) + .await?, + LikeOrAnnounceType::AnnounceType(_) => + ReceivedAnnounce::delete_by_id(self.object.id) + .exec(&data.conn) + .await?, + }; Ok(()) } diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index e1a88c09..6c87a83a 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -3,10 +3,16 @@ mod create_or_update; mod db_activity; mod db_activity_impl; mod following; -mod like; +mod like_or_announce; -pub use activity_lists::{ServiceInboxActivities, SharedInboxActivities}; +pub use activity_lists::UserInboxActivities; pub use create_or_update::{CreateOrUpdateNote, CreateOrUpdateType}; pub use db_activity::ApubActivity; pub use following::{AcceptFollow, ApubReceivedFollow, Follow, UndoFollow}; -pub use like::{ApubReceivedLike, Like, UndoLike}; +pub use like_or_announce::{ + ApubReceivedAnnounce, + ApubReceivedLike, + LikeOrAnnounce, + LikeOrAnnounceType, + UndoLikeOrAnnounce, +}; diff --git a/crates/apub/tests/activities.rs b/crates/apub/tests/activities.rs new file mode 100644 index 00000000..95a463f0 --- /dev/null +++ b/crates/apub/tests/activities.rs @@ -0,0 +1,16 @@ +use hatsu_apub::{ + activities::{CreateOrUpdateNote, Follow, LikeOrAnnounce, UndoFollow, UndoLikeOrAnnounce}, + tests::test_asset, +}; +use hatsu_utils::AppError; + +#[test] +fn test_parse_activities() -> Result<(), AppError> { + test_asset::("assets/mastodon/activities/create_note.json")?; + test_asset::("assets/mastodon/activities/follow.json")?; + test_asset::("assets/mastodon/activities/like_page.json")?; + test_asset::("assets/mastodon/activities/undo_follow.json")?; + test_asset::("assets/mastodon/activities/undo_like_page.json")?; + + Ok(()) +} diff --git a/crates/db_migration/src/lib.rs b/crates/db_migration/src/lib.rs index c7ee5875..2f3098a6 100644 --- a/crates/db_migration/src/lib.rs +++ b/crates/db_migration/src/lib.rs @@ -6,6 +6,7 @@ mod m20240131_000003_post; mod m20240131_000004_activity; mod m20240131_000005_received_follow; mod m20240501_000001_received_like; +mod m20240501_000002_received_announce; pub struct Migrator; @@ -19,6 +20,7 @@ impl MigratorTrait for Migrator { Box::new(m20240131_000004_activity::Migration), Box::new(m20240131_000005_received_follow::Migration), Box::new(m20240501_000001_received_like::Migration), + Box::new(m20240501_000002_received_announce::Migration), ] } } diff --git a/crates/db_migration/src/m20240501_000002_received_announce.rs b/crates/db_migration/src/m20240501_000002_received_announce.rs new file mode 100644 index 00000000..d26c2ac8 --- /dev/null +++ b/crates/db_migration/src/m20240501_000002_received_announce.rs @@ -0,0 +1,48 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(ReceivedAnnounce::Table) + .if_not_exists() + .col( + ColumnDef::new(ReceivedAnnounce::Id) + .string() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(ReceivedAnnounce::Actor).string().not_null()) + .col(ColumnDef::new(ReceivedAnnounce::Object).string().not_null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(ReceivedAnnounce::Table).to_owned()) + .await?; + + Ok(()) + } +} + +/// +#[derive(Iden)] +enum ReceivedAnnounce { + Table, + // Announce Activity Url + Id, + // Attributed To + Actor, + // Announced Post Url + Object, +} diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 4626a858..5bfa26eb 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -4,6 +4,7 @@ pub mod prelude; pub mod activity; pub mod post; +pub mod received_announce; pub mod received_follow; pub mod received_like; pub mod user; diff --git a/crates/db_schema/src/post.rs b/crates/db_schema/src/post.rs index 5ad52fcd..56697fbb 100644 --- a/crates/db_schema/src/post.rs +++ b/crates/db_schema/src/post.rs @@ -19,6 +19,8 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { + #[sea_orm(has_many = "super::received_announce::Entity")] + ReceivedAnnounce, #[sea_orm(has_many = "super::received_like::Entity")] ReceivedLike, #[sea_orm( @@ -43,6 +45,12 @@ pub struct SelfReferencingLink; impl ActiveModelBehavior for ActiveModel {} +impl Related for Entity { + fn to() -> RelationDef { + Relation::ReceivedAnnounce.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::ReceivedLike.def() diff --git a/crates/db_schema/src/prelude.rs b/crates/db_schema/src/prelude.rs index b8b356a4..29f3cd06 100644 --- a/crates/db_schema/src/prelude.rs +++ b/crates/db_schema/src/prelude.rs @@ -3,6 +3,7 @@ pub use super::{ activity::Entity as Activity, post::Entity as Post, + received_announce::Entity as ReceivedAnnounce, received_follow::Entity as ReceivedFollow, received_like::Entity as ReceivedLike, user::Entity as User, diff --git a/crates/db_schema/src/received_announce.rs b/crates/db_schema/src/received_announce.rs new file mode 100644 index 00000000..a575bd32 --- /dev/null +++ b/crates/db_schema/src/received_announce.rs @@ -0,0 +1,43 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "received_announce")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + pub actor: String, + pub object: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + +pub enum Relation { + #[sea_orm( + belongs_to = "super::post::Entity", + from = "Column::Object", + to = "super::post::Column::Id" + )] + Post, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Actor", + to = "super::user::Column::Id" + )] + User, +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Post.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} diff --git a/crates/db_schema/src/user.rs b/crates/db_schema/src/user.rs index 859e98f8..2c67531e 100644 --- a/crates/db_schema/src/user.rs +++ b/crates/db_schema/src/user.rs @@ -31,6 +31,8 @@ pub enum Relation { Activity, #[sea_orm(has_many = "super::post::Entity")] Post, + #[sea_orm(has_many = "super::received_announce::Entity")] + ReceivedAnnounce, #[sea_orm(has_many = "super::received_follow::Entity")] ReceivedFollow, #[sea_orm(has_many = "super::received_like::Entity")] @@ -53,6 +55,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::ReceivedAnnounce.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::ReceivedFollow.def() diff --git a/docs/src/others/federation.md b/docs/src/others/federation.md index b34ee929..418f6dfd 100644 --- a/docs/src/others/federation.md +++ b/docs/src/others/federation.md @@ -32,9 +32,9 @@ The following activities and object types are supported: - `Follow(Actor)`, `Undo(Follow)` - `Create(Note)` - `Like(Note)`, `Undo(Like)` +- `Announce(Note)`, `Undo(Announce)` - Activities are implemented in way that is compatible with Mastodon and other popular ActivityPub servers.