From 4d82b89947aea6a0bc0b1c53819c80a546e209d9 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 3 May 2020 22:41:45 -0400 Subject: [PATCH] Adding undo follow community. --- server/src/api/community.rs | 24 +++++++++---- server/src/apub/community.rs | 8 +++++ server/src/apub/community_inbox.rs | 57 ++++++++++++++++++++++++++++++ server/src/apub/mod.rs | 15 +++----- server/src/apub/user.rs | 56 +++++++++++++++++++++++++++++ ui/src/api_tests/api.spec.ts | 44 +++++++++++++++++++++++ 6 files changed, 186 insertions(+), 18 deletions(-) diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 9659469b9..9855b7887 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -483,12 +483,12 @@ impl Perform for Oper { let conn = pool.get()?; let community = Community::read(&conn, data.community_id)?; - if community.local { - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - user_id, - }; + let community_follower_form = CommunityFollowerForm { + community_id: data.community_id, + user_id, + }; + if community.local { if data.follow { match CommunityFollower::follow(&conn, &community_follower_form) { Ok(user) => user, @@ -501,9 +501,19 @@ impl Perform for Oper { }; } } else { - // TODO: still have to implement unfollow let user = User_::read(&conn, user_id)?; - user.send_follow(&community.actor_id, &conn)?; + + if data.follow { + // Dont actually add to the community followers here, because you need + // to wait for the accept + user.send_follow(&community.actor_id, &conn)?; + } else { + user.send_unfollow(&community.actor_id, &conn)?; + match CommunityFollower::ignore(&conn, &community_follower_form) { + Ok(user) => user, + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), + }; + } // TODO: this needs to return a "pending" state, until Accept is received from the remote server } diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index a05d1ce0c..feffa70ef 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -289,6 +289,14 @@ impl ActorType for Community { .collect(), ) } + + fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } + + fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } } impl FromApub for CommunityForm { diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index 92422cbbd..0aec5ff6e 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -4,6 +4,7 @@ use super::*; #[derive(Deserialize, Debug)] pub enum CommunityAcceptedObjects { Follow(Follow), + Undo(Undo), } // TODO Consolidate community and user inboxes into a single shared one @@ -25,6 +26,9 @@ pub async fn community_inbox( CommunityAcceptedObjects::Follow(f) => { handle_follow(&f, &request, &community_name, db, chat_server) } + CommunityAcceptedObjects::Undo(u) => { + handle_undo_follow(&u, &request, &community_name, db, chat_server) + } } } @@ -76,3 +80,56 @@ fn handle_follow( Ok(HttpResponse::Ok().finish()) } + +fn handle_undo_follow( + undo: &Undo, + request: &HttpRequest, + community_name: &str, + db: DbPoolParam, + _chat_server: ChatServerParam, +) -> Result { + let follow = undo + .undo_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let user_uri = follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let _community_uri = follow + .follow_props + .get_object_xsd_any_uri() + .unwrap() + .to_string(); + + let conn = db.get()?; + + let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; + let community = Community::read_from_name(&conn, &community_name)?; + + 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(&follow)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community_follower_form = CommunityFollowerForm { + community_id: community.id, + user_id: user.id, + }; + + CommunityFollower::ignore(&conn, &community_follower_form).ok(); + + Ok(HttpResponse::Ok().finish()) +} diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index c5bd2ea43..40f4322ea 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -233,14 +233,11 @@ pub trait ActorType { // These two have default impls, since currently a community can't follow anything, // and a user can't be followed (yet) #[allow(unused_variables)] - fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { - Err(format_err!("Follow not implemented.")) - } + fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; + fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; #[allow(unused_variables)] - fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> { - Err(format_err!("Accept not implemented.")) - } + fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>; fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; @@ -248,12 +245,8 @@ pub trait ActorType { fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; - // TODO default because there is no user following yet. - #[allow(unused_variables)] /// For a given community, returns the inboxes of all followers. - fn get_follower_inboxes(&self, conn: &PgConnection) -> Result, Error> { - Ok(vec![]) - } + fn get_follower_inboxes(&self, conn: &PgConnection) -> Result, Error>; // TODO move these to the db rows fn get_inbox_url(&self) -> String { diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index d9c7e86a0..71f6f5c93 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -91,6 +91,54 @@ impl ActorType for User_ { Ok(()) } + fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { + let mut follow = Follow::new(); + + let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4()); + + follow + .object_props + .set_context_xsd_any_uri(context())? + .set_id(id)?; + follow + .follow_props + .set_actor_xsd_any_uri(self.actor_id.to_owned())? + .set_object_xsd_any_uri(follow_actor_id)?; + let to = format!("{}/inbox", follow_actor_id); + + // TODO + // Undo that fake activity + let undo_id = format!("{}/undo/follow/{}", self.actor_id, uuid::Uuid::new_v4()); + let mut undo = Undo::default(); + + undo + .object_props + .set_context_xsd_any_uri(context())? + .set_id(undo_id)?; + + undo + .undo_props + .set_actor_xsd_any_uri(self.actor_id.to_owned())? + .set_object_base_box(follow)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &undo, + &self.private_key.as_ref().unwrap(), + &follow_actor_id, + vec![to], + )?; + Ok(()) + } + fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { unimplemented!() } @@ -106,6 +154,14 @@ impl ActorType for User_ { fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { unimplemented!() } + + fn send_accept_follow(&self, _follow: &Follow, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } + + fn get_follower_inboxes(&self, _conn: &PgConnection) -> Result, Error> { + unimplemented!() + } } impl FromApub for UserForm { diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index b25c8df5f..a3826504a 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -140,6 +140,50 @@ describe('main', () => { ).then(d => d.json()); expect(followedCommunitiesRes.communities[1].community_local).toBe(false); + + // Test out unfollowing + let unfollowForm: FollowCommunityForm = { + community_id: searchResponse.communities[0].id, + follow: false, + auth: lemmyAlphaAuth, + }; + + let unfollowRes: CommunityResponse = await fetch( + `${lemmyAlphaApiUrl}/community/follow`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(unfollowForm), + } + ).then(d => d.json()); + + // Check that you are unsubscribed to it locally + let followedCommunitiesResAgain: GetFollowedCommunitiesResponse = await fetch( + followedCommunitiesUrl, + { + method: 'GET', + } + ).then(d => d.json()); + + expect(followedCommunitiesResAgain.communities.length).toBe(1); + + // Follow again, for other tests + let followResAgain: CommunityResponse = await fetch( + `${lemmyAlphaApiUrl}/community/follow`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(followForm), + } + ).then(d => d.json()); + + // Make sure the follow response went through + expect(followResAgain.community.local).toBe(false); + expect(followResAgain.community.name).toBe('main'); }); });