From 53215073a8f0843bbf887867336f29023d16cdc9 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 28 Apr 2020 00:16:02 -0400 Subject: [PATCH] Adding federated post and comment likes. --- server/src/api/comment.rs | 13 +- server/src/apub/comment.rs | 76 +++++++++- server/src/apub/community.rs | 4 +- server/src/apub/mod.rs | 7 +- server/src/apub/post.rs | 20 ++- server/src/apub/shared_inbox.rs | 256 +++++++++++++++++++++++++++++++- server/src/apub/user.rs | 1 - ui/src/api_tests/api.spec.ts | 6 + 8 files changed, 359 insertions(+), 24 deletions(-) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index fddb42abe..a6742e4c0 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -235,6 +235,8 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), }; + updated_comment.send_like(&user, &conn)?; + let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?; let mut res = CommentResponse { @@ -500,7 +502,8 @@ impl Perform for Oper { } // Check for a site ban - if UserView::read(&conn, user_id)?.banned { + let user = User_::read(&conn, user_id)?; + if user.banned { return Err(APIError::err("site_ban").into()); } @@ -537,6 +540,14 @@ impl Perform for Oper { Ok(like) => like, Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), }; + + if like_form.score == 1 { + comment.send_like(&user, &conn)?; + } else if like_form.score == -1 { + comment.send_dislike(&user, &conn)?; + } + } else { + // TODO tombstone the like } // Have to refetch the comment to get the current state diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index d108b2ee7..db47106e6 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -94,11 +94,13 @@ impl ApubObjectType for Comment { 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!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut create = Create::new(); populate_object_props( &mut create.object_props, &community.get_followers_url(), - &self.ap_id, + &id, )?; create .create_props @@ -128,11 +130,13 @@ impl ApubObjectType for Comment { 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!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut update = Update::new(); populate_object_props( &mut update.object_props, &community.get_followers_url(), - &self.ap_id, + &id, )?; update .update_props @@ -157,3 +161,71 @@ impl ApubObjectType for Comment { Ok(()) } } + +impl ApubLikeableType for Comment { + fn send_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!("{}/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(note)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: creator.id, + data: serde_json::to_value(&like)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &like, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } + + fn send_dislike(&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 dislike = Dislike::new(); + populate_object_props( + &mut dislike.object_props, + &community.get_followers_url(), + &id, + )?; + dislike + .dislike_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(note)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: creator.id, + data: serde_json::to_value(&dislike)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &dislike, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } +} diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index f5e4f44de..46bd9024c 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -63,11 +63,9 @@ impl ActorType for Community { .get_actor_xsd_any_uri() .unwrap() .to_string(); + let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4()); let mut accept = Accept::new(); - // TODO using a fake accept id - let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4()); - //follow accept .object_props .set_context_xsd_any_uri(context())? diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 4b51ca9f6..14f3e798b 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -42,16 +42,15 @@ use url::Url; use crate::api::comment::CommentResponse; use crate::api::post::PostResponse; use crate::api::site::SearchResponse; -use crate::db::activity; -use crate::db::comment::{Comment, CommentForm}; +use crate::db::comment::{Comment, CommentForm, CommentLike, CommentLikeForm}; use crate::db::comment_view::CommentView; use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm}; use crate::db::community_view::{CommunityFollowerView, CommunityView}; -use crate::db::post::{Post, PostForm}; +use crate::db::post::{Post, PostForm, PostLike, PostLikeForm}; use crate::db::post_view::PostView; use crate::db::user::{UserForm, User_}; use crate::db::user_view::UserView; -use crate::db::{Crud, Followable, SearchType}; +use crate::db::{activity, Crud, Followable, Likeable, SearchType}; use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use crate::routes::{ChatServerParam, DbPoolParam}; use crate::websocket::{ diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 505ab98a0..af8ee5998 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -98,11 +98,13 @@ impl ApubObjectType for Post { fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = self.to_apub(conn)?; let community = Community::read(conn, self.community_id)?; + let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut create = Create::new(); populate_object_props( &mut create.object_props, &community.get_followers_url(), - &self.ap_id, + &id, )?; create .create_props @@ -131,11 +133,13 @@ impl ApubObjectType for Post { fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = self.to_apub(conn)?; let community = Community::read(conn, self.community_id)?; + let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut update = Update::new(); populate_object_props( &mut update.object_props, &community.get_followers_url(), - &self.ap_id, + &id, )?; update .update_props @@ -165,12 +169,10 @@ impl ApubLikeableType for Post { fn send_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(), - &self.ap_id, - )?; + 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())? @@ -197,11 +199,13 @@ impl ApubLikeableType for Post { fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = self.to_apub(conn)?; let community = Community::read(conn, self.community_id)?; + let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut dislike = Dislike::new(); populate_object_props( &mut dislike.object_props, &community.get_followers_url(), - &self.ap_id, + &id, )?; dislike .dislike_props diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 02244726b..705a578a0 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -5,6 +5,8 @@ use super::*; pub enum SharedAcceptedObjects { Create(Create), Update(Update), + Like(Like), + Dislike(Dislike), } impl SharedAcceptedObjects { @@ -12,6 +14,8 @@ impl SharedAcceptedObjects { match self { SharedAcceptedObjects::Create(c) => c.create_props.get_object_base_box(), SharedAcceptedObjects::Update(u) => u.update_props.get_object_base_box(), + SharedAcceptedObjects::Like(l) => l.like_props.get_object_base_box(), + SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_object_base_box(), } } } @@ -33,17 +37,29 @@ pub async fn shared_inbox( let object = activity.object().cloned().unwrap(); match (activity, object.kind()) { - (SharedAcceptedObjects::Create(c), Some("Note")) => { - receive_create_comment(&c, &request, &conn, chat_server) - } (SharedAcceptedObjects::Create(c), Some("Page")) => { receive_create_post(&c, &request, &conn, chat_server) } + (SharedAcceptedObjects::Update(u), Some("Page")) => { + receive_update_post(&u, &request, &conn, chat_server) + } + (SharedAcceptedObjects::Like(l), Some("Page")) => { + receive_like_post(&l, &request, &conn, chat_server) + } + (SharedAcceptedObjects::Dislike(d), Some("Page")) => { + receive_dislike_post(&d, &request, &conn, chat_server) + } + (SharedAcceptedObjects::Create(c), Some("Note")) => { + receive_create_comment(&c, &request, &conn, chat_server) + } (SharedAcceptedObjects::Update(u), Some("Note")) => { receive_update_comment(&u, &request, &conn, chat_server) } - (SharedAcceptedObjects::Update(u), Some("Page")) => { - receive_update_post(&u, &request, &conn, chat_server) + (SharedAcceptedObjects::Like(l), Some("Note")) => { + receive_like_comment(&l, &request, &conn, chat_server) + } + (SharedAcceptedObjects::Dislike(d), Some("Note")) => { + receive_dislike_comment(&d, &request, &conn, chat_server) } _ => Err(format_err!("Unknown incoming activity type.")), } @@ -202,6 +218,116 @@ fn receive_update_post( Ok(HttpResponse::Ok().finish()) } +fn receive_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() + .to_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)?; + PostLike::like(&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()) +} + +fn receive_dislike_post( + dislike: &Dislike, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let page = dislike + .dislike_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; + + let user_uri = dislike + .dislike_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(&dislike)?, + 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)?; + PostLike::like(&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()) +} + fn receive_update_comment( update: &Update, request: &HttpRequest, @@ -256,3 +382,123 @@ fn receive_update_comment( Ok(HttpResponse::Ok().finish()) } + +fn receive_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() + .to_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: 1, + }; + CommentLike::remove(&conn, &like_form)?; + CommentLike::like(&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_dislike_comment( + dislike: &Dislike, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let note = dislike + .dislike_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; + + let user_uri = dislike + .dislike_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(&dislike)?, + 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: -1, + }; + CommentLike::remove(&conn, &like_form)?; + CommentLike::like(&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()) +} diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index bb9f064cb..d7fd22826 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -58,7 +58,6 @@ impl ActorType for User_ { fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { let mut follow = Follow::new(); - // TODO using a fake accept id let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4()); follow diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 49fd08782..8a3e94f29 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -163,6 +163,7 @@ describe('main', () => { 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 getPostUrl = `${lemmyBetaApiUrl}/post?id=2`; let getPostRes: GetPostResponse = await fetch(getPostUrl, { @@ -172,6 +173,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); }); }); @@ -236,6 +238,7 @@ describe('main', () => { expect(createResponse.comment.content).toBe(content); expect(createResponse.comment.community_local).toBe(false); expect(createResponse.comment.creator_local).toBe(true); + expect(createResponse.comment.score).toBe(1); let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`; let getPostRes: GetPostResponse = await fetch(getPostUrl, { @@ -245,6 +248,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); // Now do beta replying to that comment, as a child comment let contentBeta = 'A child federated comment from beta'; @@ -270,6 +274,7 @@ describe('main', () => { expect(createResponseBeta.comment.community_local).toBe(true); expect(createResponseBeta.comment.creator_local).toBe(true); expect(createResponseBeta.comment.parent_id).toBe(1); + expect(createResponseBeta.comment.score).toBe(1); // Make sure lemmy alpha sees that new child comment from beta let getPostUrlAlpha = `${lemmyAlphaApiUrl}/post?id=2`; @@ -281,6 +286,7 @@ describe('main', () => { expect(getPostResAlpha.comments[0].content).toBe(contentBeta); expect(getPostResAlpha.comments[0].community_local).toBe(false); expect(getPostResAlpha.comments[0].creator_local).toBe(false); + expect(getPostResAlpha.comments[0].score).toBe(1); }); });