diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 2853beb3..0660a52c 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -561,7 +561,7 @@ impl Perform for Oper { comment.send_dislike(&user, &conn)?; } } else { - // TODO tombstone the like + comment.send_undo_like(&user, &conn)?; } // Have to refetch the comment to get the current state diff --git a/server/src/api/post.rs b/server/src/api/post.rs index b9c4c083..42c35074 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -397,7 +397,7 @@ impl Perform for Oper { post.send_dislike(&user, &conn)?; } } else { - // TODO tombstone the post like + post.send_undo_like(&user, &conn)?; } let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) { diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 55dec23b..17da45a6 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -413,4 +413,51 @@ impl ApubLikeableType for Comment { )?; Ok(()) } + + fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let note = self.to_apub(&conn)?; + let post = Post::read(&conn, self.post_id)?; + let community = Community::read(&conn, post.community_id)?; + let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); + + let mut like = Like::new(); + populate_object_props(&mut like.object_props, &community.get_followers_url(), &id)?; + like + .like_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(note)?; + + // TODO + // Undo that fake activity + let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut undo = Undo::default(); + + populate_object_props( + &mut undo.object_props, + &community.get_followers_url(), + &undo_id, + )?; + + undo + .undo_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(like)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: creator.id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &undo, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 0438f92e..c5bd2ea4 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -208,7 +208,7 @@ pub trait ApubObjectType { pub trait ApubLikeableType { fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - // TODO add send_undo_like / undo_dislike + fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; } pub fn get_shared_inbox(actor_id: &str) -> String { diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 40816437..61fbf827 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -416,4 +416,50 @@ impl ApubLikeableType for Post { )?; Ok(()) } + + fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let page = self.to_apub(conn)?; + let community = Community::read(conn, self.community_id)?; + let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); + + let mut like = Like::new(); + populate_object_props(&mut like.object_props, &community.get_followers_url(), &id)?; + like + .like_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(page)?; + + // TODO + // Undo that fake activity + let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut undo = Undo::default(); + + populate_object_props( + &mut undo.object_props, + &community.get_followers_url(), + &undo_id, + )?; + + undo + .undo_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(like)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: creator.id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &undo, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index d77788e5..4b29c397 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -91,6 +91,9 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Undo(u), Some("Remove")) => { receive_undo_remove(&u, &request, &conn, chat_server) } + (SharedAcceptedObjects::Undo(u), Some("Like")) => { + receive_undo_like(&u, &request, &conn, chat_server) + } _ => Err(format_err!("Unknown incoming activity type.")), } } @@ -1424,3 +1427,141 @@ fn receive_undo_remove_community( Ok(HttpResponse::Ok().finish()) } + +fn receive_undo_like( + undo: &Undo, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let like = undo + .undo_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let type_ = like + .like_props + .get_object_base_box() + .to_owned() + .unwrap() + .kind() + .unwrap(); + + match type_ { + "Note" => receive_undo_like_comment(&like, &request, &conn, chat_server), + "Page" => receive_undo_like_post(&like, &request, &conn, chat_server), + d => Err(format_err!("Undo Delete type {} not supported", d)), + } +} + +fn receive_undo_like_comment( + like: &Like, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let note = like + .like_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string(); + + let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; + verify(request, &user.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: user.id, + data: serde_json::to_value(&like)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let comment = CommentForm::from_apub(¬e, &conn)?; + let comment_id = Comment::read_from_apub_id(conn, &comment.ap_id)?.id; + let like_form = CommentLikeForm { + comment_id, + post_id: comment.post_id, + user_id: user.id, + score: 0, + }; + CommentLike::remove(&conn, &like_form)?; + + // Refetch the view + let comment_view = CommentView::read(&conn, comment_id, None)?; + + // TODO get those recipient actor ids from somewhere + let recipient_ids = vec![]; + let res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + chat_server.do_send(SendComment { + op: UserOperation::CreateCommentLike, + comment: res, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} + +fn receive_undo_like_post( + like: &Like, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let page = like + .like_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string(); + + let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; + verify(request, &user.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: user.id, + data: serde_json::to_value(&like)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let post = PostForm::from_apub(&page, conn)?; + let post_id = Post::read_from_apub_id(conn, &post.ap_id)?.id; + + let like_form = PostLikeForm { + post_id, + user_id: user.id, + score: 1, + }; + PostLike::remove(&conn, &like_form)?; + + // Refetch the view + let post_view = PostView::read(&conn, post_id, None)?; + + let res = PostResponse { post: post_view }; + + chat_server.do_send(SendPost { + op: UserOperation::CreatePostLike, + post: res, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index ed97174e..b25c8df5 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -16,6 +16,8 @@ import { CommunityForm, GetCommunityForm, GetCommunityResponse, + CommentLikeForm, + CreatePostLikeForm, } from '../interfaces'; let lemmyAlphaUrl = 'http://localhost:8540'; @@ -163,11 +165,28 @@ describe('main', () => { } ).then(d => d.json()); + let unlikePostForm: CreatePostLikeForm = { + post_id: createResponse.post.id, + score: 0, + auth: lemmyAlphaAuth, + }; expect(createResponse.post.name).toBe(name); expect(createResponse.post.community_local).toBe(false); expect(createResponse.post.creator_local).toBe(true); expect(createResponse.post.score).toBe(1); + let unlikePostRes: PostResponse = await fetch( + `${lemmyAlphaApiUrl}/post/like`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(unlikePostForm), + } + ).then(d => d.json()); + expect(unlikePostRes.post.score).toBe(0); + let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`; let getPostRes: GetPostResponse = await fetch(getPostUrl, { method: 'GET', @@ -176,7 +195,7 @@ describe('main', () => { expect(getPostRes.post.name).toBe(name); expect(getPostRes.post.community_local).toBe(true); expect(getPostRes.post.creator_local).toBe(false); - expect(getPostRes.post.score).toBe(1); + expect(getPostRes.post.score).toBe(0); }); }); @@ -243,6 +262,27 @@ describe('main', () => { expect(createResponse.comment.creator_local).toBe(true); expect(createResponse.comment.score).toBe(1); + // Do an unlike, to test it + let unlikeCommentForm: CommentLikeForm = { + comment_id: createResponse.comment.id, + score: 0, + post_id: 2, + auth: lemmyAlphaAuth, + }; + + let unlikeCommentRes: CommentResponse = await fetch( + `${lemmyAlphaApiUrl}/comment/like`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(unlikeCommentForm), + } + ).then(d => d.json()); + + expect(unlikeCommentRes.comment.score).toBe(0); + let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`; let getPostRes: GetPostResponse = await fetch(getPostUrl, { method: 'GET', @@ -251,7 +291,7 @@ describe('main', () => { expect(getPostRes.comments[0].content).toBe(content); expect(getPostRes.comments[0].community_local).toBe(true); expect(getPostRes.comments[0].creator_local).toBe(false); - expect(getPostRes.comments[0].score).toBe(1); + expect(getPostRes.comments[0].score).toBe(0); // Now do beta replying to that comment, as a child comment let contentBeta = 'A child federated comment from beta';