From cf69ac6eb2f6afef3c39baa8f797b96a5677377f Mon Sep 17 00:00:00 2001 From: silverpill Date: Wed, 15 Dec 2021 01:00:42 +0000 Subject: [PATCH] Send Undo(Like) activity when post is unfavourited --- README.md | 1 + src/activitypub/activity.rs | 32 +++++++++++++++++++----- src/activitypub/receiver.rs | 16 +++++++++++- src/mastodon_api/statuses/views.rs | 40 +++++++++++++++++++++++------- src/models/reactions/queries.rs | 29 +++++++++++++++++----- 5 files changed, 96 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9d7a0c9..e3c2919 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ The following activities are supported: - Create(Note) - Delete(Note) - Like(Note) +- Undo(Like) - Announce(Note) - Follow(Person) - Update(Person) diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs index 49db8f6..8e6cd36 100644 --- a/src/activitypub/activity.rs +++ b/src/activitypub/activity.rs @@ -125,7 +125,7 @@ fn create_activity( instance_url: &str, actor_name: &str, activity_type: &str, - activity_uuid: Option, + internal_activity_id: Option<&Uuid>, object: impl Serialize, recipients: Vec, ) -> Activity { @@ -135,7 +135,7 @@ fn create_activity( ); let activity_id = get_object_url( instance_url, - &activity_uuid.unwrap_or(new_uuid()), + internal_activity_id.unwrap_or(&new_uuid()), ); Activity { context: json!(AP_CONTEXT), @@ -248,19 +248,39 @@ pub fn create_activity_note( pub fn create_activity_like( instance_url: &str, actor_profile: &DbActorProfile, - object_id: &str, + note_id: &str, + reaction_id: &Uuid, ) -> Activity { let activity = create_activity( instance_url, &actor_profile.username, LIKE, - None, - object_id, + Some(reaction_id), + note_id, vec![AP_PUBLIC.to_string()], ); activity } +pub fn create_activity_undo_like( + instance_url: &str, + actor_profile: &DbActorProfile, + reaction_id: &Uuid, +) -> Activity { + let object_id = get_object_url( + instance_url, + reaction_id, + ); + create_activity( + instance_url, + &actor_profile.username, + UNDO, + None, + object_id, + vec![AP_PUBLIC.to_string()], + ) +} + pub fn create_activity_announce( instance_url: &str, actor_profile: &DbActorProfile, @@ -317,7 +337,7 @@ pub fn create_activity_follow( instance_url, &actor_profile.username, FOLLOW, - Some(*follow_request_id), + Some(follow_request_id), object, vec![target_actor_id.to_string()], ); diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index 7767d8d..6d182e2 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -24,7 +24,11 @@ use crate::models::profiles::queries::{ update_profile, }; use crate::models::profiles::types::{DbActorProfile, ProfileUpdateData}; -use crate::models::reactions::queries::create_reaction; +use crate::models::reactions::queries::{ + create_reaction, + get_reaction_by_activity_id, + delete_reaction, +}; use crate::models::relationships::queries::{ follow_request_accepted, follow_request_rejected, @@ -433,6 +437,16 @@ pub async fn receive_activity( let target_profile = get_profile_by_acct(db_client, &target_username).await?; unfollow(db_client, &source_profile.id, &target_profile.id).await?; }, + (UNDO, _) => { + // Undo(Like) + let object_id = get_object_id(activity.object)?; + let reaction = get_reaction_by_activity_id(db_client, &object_id).await?; + delete_reaction( + db_client, + &reaction.author_id, + &reaction.post_id, + ).await?; + }, (UPDATE, PERSON) => { let actor: Actor = serde_json::from_value(activity.object) .map_err(|_| ValidationError("invalid actor data"))?; diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index 513e17b..37c9bd4 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -7,6 +7,7 @@ use uuid::Uuid; use crate::activitypub::activity::{ create_activity_note, create_activity_like, + create_activity_undo_like, create_activity_announce, create_activity_delete_note, }; @@ -232,20 +233,20 @@ async fn favourite( if !can_view_post(Some(¤t_user), &post) { return Err(HttpError::NotFoundError("post")); }; - let reaction_created = match create_reaction( + let maybe_reaction_created = match create_reaction( db_client, ¤t_user.id, &status_id, None, ).await { - Ok(_) => { + Ok(reaction) => { post.reaction_count += 1; - true + Some(reaction) }, - Err(DatabaseError::AlreadyExists(_)) => false, // post already favourited + Err(DatabaseError::AlreadyExists(_)) => None, // post already favourited Err(other_error) => return Err(other_error.into()), }; get_reposted_posts(db_client, vec![&mut post]).await?; get_actions_for_posts(db_client, ¤t_user.id, vec![&mut post]).await?; - if reaction_created { + if let Some(reaction) = maybe_reaction_created { let maybe_remote_actor = post.author.remote_actor() .map_err(|_| HttpError::InternalError)?; if let Some(remote_actor) = maybe_remote_actor { @@ -255,6 +256,7 @@ async fn favourite( &config.instance_url(), ¤t_user.profile, object_id, + &reaction.id, ); deliver_activity(&config, ¤t_user, activity, vec![remote_actor]); } @@ -277,13 +279,33 @@ async fn unfavourite( if !can_view_post(Some(¤t_user), &post) { return Err(HttpError::NotFoundError("post")); }; - match delete_reaction(db_client, ¤t_user.id, &status_id).await { - Ok(_) => post.reaction_count -= 1, - Err(DatabaseError::NotFound(_)) => (), // post not favourited + let maybe_reaction_deleted = match delete_reaction( + db_client, ¤t_user.id, &status_id, + ).await { + Ok(reaction_id) => { + post.reaction_count -= 1; + Some(reaction_id) + }, + Err(DatabaseError::NotFound(_)) => None, // post not favourited Err(other_error) => return Err(other_error.into()), - } + }; get_reposted_posts(db_client, vec![&mut post]).await?; get_actions_for_posts(db_client, ¤t_user.id, vec![&mut post]).await?; + + if let Some(reaction_id) = maybe_reaction_deleted { + let maybe_remote_actor = post.author.remote_actor() + .map_err(|_| HttpError::InternalError)?; + if let Some(remote_actor) = maybe_remote_actor { + // Federate + let activity = create_activity_undo_like( + &config.instance_url(), + ¤t_user.profile, + &reaction_id, + ); + deliver_activity(&config, ¤t_user, activity, vec![remote_actor]); + }; + }; + let status = Status::from_post(post, &config.instance_url()); Ok(HttpResponse::Ok().json(status)) } diff --git a/src/models/reactions/queries.rs b/src/models/reactions/queries.rs index 3c9d93f..4cfc913 100644 --- a/src/models/reactions/queries.rs +++ b/src/models/reactions/queries.rs @@ -42,25 +42,42 @@ pub async fn create_reaction( Ok(reaction) } +pub async fn get_reaction_by_activity_id( + db_client: &impl GenericClient, + activity_id: &str, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT post_reaction + FROM post_reaction + WHERE activity_id = $1 + ", + &[&activity_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("reaction"))?; + let reaction = row.try_get("post_reaction")?; + Ok(reaction) +} + pub async fn delete_reaction( db_client: &mut impl GenericClient, author_id: &Uuid, post_id: &Uuid, -) -> Result<(), DatabaseError> { +) -> Result { let transaction = db_client.transaction().await?; - let deleted_count = transaction.execute( + let maybe_row = transaction.query_opt( " DELETE FROM post_reaction WHERE author_id = $1 AND post_id = $2 + RETURNING post_reaction.id ", &[&author_id, &post_id], ).await?; - if deleted_count == 0 { - return Err(DatabaseError::NotFound("reaction")); - } + let row = maybe_row.ok_or(DatabaseError::NotFound("reaction"))?; + let reaction_id = row.try_get("id")?; update_reaction_count(&transaction, post_id, -1).await?; transaction.commit().await?; - Ok(()) + Ok(reaction_id) } /// Finds favourites among given posts and returns their IDs