]> Untitled Git - lemmy.git/commitdiff
Adding federated mod remove actions.
authorDessalines <tyhou13@gmx.com>
Sun, 3 May 2020 14:00:59 +0000 (10:00 -0400)
committerDessalines <tyhou13@gmx.com>
Sun, 3 May 2020 14:00:59 +0000 (10:00 -0400)
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/post.rs
server/src/apub/comment.rs
server/src/apub/community.rs
server/src/apub/fetcher.rs
server/src/apub/mod.rs
server/src/apub/post.rs
server/src/apub/shared_inbox.rs
server/src/apub/user.rs
ui/src/api_tests/api.spec.ts

index 1ecedb2c888d61153b67385e11715cf5541af958..2853beb3411c9ad3ee4348b268d59529d7cc606c 100644 (file)
@@ -343,6 +343,12 @@ impl Perform for Oper<EditComment> {
       } else {
         updated_comment.send_undo_delete(&user, &conn)?;
       }
+    } else if let Some(removed) = data.removed.to_owned() {
+      if removed {
+        updated_comment.send_remove(&user, &conn)?;
+      } else {
+        updated_comment.send_undo_remove(&user, &conn)?;
+      }
     } else {
       updated_comment.send_update(&user, &conn)?;
     }
index 71da6712a26f39c9a81ab5d88f0b7a1d146936a8..9659469b9e33ed80cf70ccdfb28a5145bd896e92 100644 (file)
@@ -386,6 +386,12 @@ impl Perform for Oper<EditCommunity> {
       } else {
         updated_community.send_undo_delete(&user, &conn)?;
       }
+    } else if let Some(removed) = data.removed.to_owned() {
+      if removed {
+        updated_community.send_remove(&user, &conn)?;
+      } else {
+        updated_community.send_undo_remove(&user, &conn)?;
+      }
     }
 
     let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
index 55e0612fe9b912b45963dbae91aef03e734cc9a0..b9c4c083614cba91f6dcd13a01d8542000fd4ad8 100644 (file)
@@ -547,6 +547,12 @@ impl Perform for Oper<EditPost> {
       } else {
         updated_post.send_undo_delete(&user, &conn)?;
       }
+    } else if let Some(removed) = data.removed.to_owned() {
+      if removed {
+        updated_post.send_remove(&user, &conn)?;
+      } else {
+        updated_post.send_undo_remove(&user, &conn)?;
+      }
     } else {
       updated_post.send_update(&user, &conn)?;
     }
index 65dd3c19a955aec4eba7d87ea61803ddddd10499..872582751445d75f155330fec657f2f7529a8958 100644 (file)
@@ -187,7 +187,7 @@ impl ApubObjectType for Comment {
 
     // Insert the sent activity into the activity table
     let activity_form = activity::ActivityForm {
-      user_id: self.creator_id,
+      user_id: creator.id,
       data: serde_json::to_value(&delete)?,
       local: true,
       updated: None,
@@ -240,7 +240,7 @@ impl ApubObjectType for Comment {
 
     // Insert the sent activity into the activity table
     let activity_form = activity::ActivityForm {
-      user_id: self.creator_id,
+      user_id: creator.id,
       data: serde_json::to_value(&undo)?,
       local: true,
       updated: None,
@@ -255,6 +255,95 @@ impl ApubObjectType for Comment {
     )?;
     Ok(())
   }
+
+  fn send_remove(&self, mod_: &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!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
+    let mut remove = Remove::default();
+
+    populate_object_props(
+      &mut remove.object_props,
+      &community.get_followers_url(),
+      &id,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    // Insert the sent activity into the activity table
+    let activity_form = activity::ActivityForm {
+      user_id: mod_.id,
+      data: serde_json::to_value(&remove)?,
+      local: true,
+      updated: None,
+    };
+    activity::Activity::create(&conn, &activity_form)?;
+
+    send_activity(
+      &remove,
+      &mod_.private_key.as_ref().unwrap(),
+      &mod_.actor_id,
+      community.get_follower_inboxes(&conn)?,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_remove(&self, mod_: &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)?;
+
+    // Generate a fake delete activity, with the correct object
+    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
+    let mut remove = Remove::default();
+
+    populate_object_props(
+      &mut remove.object_props,
+      &community.get_followers_url(),
+      &id,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    // Undo that fake activity
+    let undo_id = format!("{}/undo/remove/{}", 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(mod_.actor_id.to_owned())?
+      .set_object_base_box(remove)?;
+
+    // Insert the sent activity into the activity table
+    let activity_form = activity::ActivityForm {
+      user_id: mod_.id,
+      data: serde_json::to_value(&undo)?,
+      local: true,
+      updated: None,
+    };
+    activity::Activity::create(&conn, &activity_form)?;
+
+    send_activity(
+      &undo,
+      &mod_.private_key.as_ref().unwrap(),
+      &mod_.actor_id,
+      community.get_follower_inboxes(&conn)?,
+    )?;
+    Ok(())
+  }
 }
 
 impl ApubLikeableType for Comment {
index c4d9bf839c4c37e4294b43fdd843d6da47f934fa..3510fbffc093298fa8f756bbcd9fa54842774b46 100644 (file)
@@ -14,13 +14,21 @@ impl ToApub for Community {
     let mut group = Group::default();
     let oprops: &mut ObjectProperties = group.as_mut();
 
-    let creator = User_::read(conn, self.creator_id)?;
+    // The attributed to, is an ordered vector with the creator actor_ids first,
+    // then the rest of the moderators
+    // TODO Technically the instance admins can mod the community, but lets
+    // ignore that for now
+    let moderators = CommunityModeratorView::for_community(&conn, self.id)?
+      .into_iter()
+      .map(|m| m.user_actor_id)
+      .collect();
+
     oprops
       .set_context_xsd_any_uri(context())?
       .set_id(self.actor_id.to_owned())?
       .set_name_xsd_string(self.name.to_owned())?
       .set_published(convert_datetime(self.published))?
-      .set_attributed_to_xsd_any_uri(creator.actor_id)?;
+      .set_many_attributed_to_xsd_any_uris(moderators)?;
 
     if let Some(u) = self.updated.to_owned() {
       oprops.set_updated(convert_datetime(u))?;
@@ -181,6 +189,83 @@ impl ActorType for Community {
     Ok(())
   }
 
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let group = self.to_apub(conn)?;
+    let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
+
+    let mut remove = Remove::default();
+    populate_object_props(&mut remove.object_props, &self.get_followers_url(), &id)?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(group)?;
+
+    // Insert the sent activity into the activity table
+    let activity_form = activity::ActivityForm {
+      user_id: mod_.id,
+      data: serde_json::to_value(&remove)?,
+      local: true,
+      updated: None,
+    };
+    activity::Activity::create(&conn, &activity_form)?;
+
+    // Note: For an accept, since it was automatic, no one pushed a button,
+    // the community was the actor.
+    // But for delete, the creator is the actor, and does the signing
+    send_activity(
+      &remove,
+      &mod_.private_key.as_ref().unwrap(),
+      &mod_.actor_id,
+      self.get_follower_inboxes(&conn)?,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let group = self.to_apub(conn)?;
+    let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
+
+    let mut remove = Remove::default();
+    populate_object_props(&mut remove.object_props, &self.get_followers_url(), &id)?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(group)?;
+
+    // Undo that fake activity
+    let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4());
+    let mut undo = Undo::default();
+
+    populate_object_props(&mut undo.object_props, &self.get_followers_url(), &undo_id)?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(remove)?;
+
+    // Insert the sent activity into the activity table
+    let activity_form = activity::ActivityForm {
+      user_id: mod_.id,
+      data: serde_json::to_value(&undo)?,
+      local: true,
+      updated: None,
+    };
+    activity::Activity::create(&conn, &activity_form)?;
+
+    // Note: For an accept, since it was automatic, no one pushed a button,
+    // the community was the actor.
+    // But for remove , the creator is the actor, and does the signing
+    send_activity(
+      &undo,
+      &mod_.private_key.as_ref().unwrap(),
+      &mod_.actor_id,
+      self.get_follower_inboxes(&conn)?,
+    )?;
+    Ok(())
+  }
+
   /// For a given community, returns the inboxes of all followers.
   fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
     Ok(
@@ -220,8 +305,11 @@ impl FromApub for CommunityForm {
     // TODO don't do extra fetching here
     // let _outbox = fetch_remote_object::<OrderedCollection>(&outbox_uri)?;
     // let _followers = fetch_remote_object::<UnorderedCollection>(&followers_uri)?;
-    let apub_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
-    let creator = get_or_fetch_and_upsert_remote_user(&apub_id, conn)?;
+    let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap();
+    let creator = creator_and_moderator_uris
+      .next()
+      .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
+      .unwrap();
 
     Ok(CommunityForm {
       name: oprops.get_name_xsd_string().unwrap().to_string(),
index e07e410bac3ebcd4b16536e8769fbf52c366665b..e581e14d077ea1e619db1af97c5d8c999376cc0e 100644 (file)
@@ -86,10 +86,10 @@ pub fn get_or_fetch_and_upsert_remote_user(
   match User_::read_from_actor_id(&conn, &apub_id) {
     Ok(u) => {
       // If its older than a day, re-fetch it
-      // TODO the less than needs to be tested
-      if u
-        .last_refreshed_at
-        .lt(&(naive_now() - chrono::Duration::days(1)))
+      if !u.local
+        && u
+          .last_refreshed_at
+          .lt(&(naive_now() - chrono::Duration::days(1)))
       {
         debug!("Fetching and updating from remote user: {}", apub_id);
         let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
@@ -118,10 +118,10 @@ pub fn get_or_fetch_and_upsert_remote_community(
   match Community::read_from_actor_id(&conn, &apub_id) {
     Ok(c) => {
       // If its older than a day, re-fetch it
-      // TODO the less than needs to be tested
-      if c
-        .last_refreshed_at
-        .lt(&(naive_now() - chrono::Duration::days(1)))
+      if !c.local
+        && c
+          .last_refreshed_at
+          .lt(&(naive_now() - chrono::Duration::days(1)))
       {
         debug!("Fetching and updating from remote community: {}", apub_id);
         let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
@@ -136,7 +136,28 @@ pub fn get_or_fetch_and_upsert_remote_community(
       debug!("Fetching and creating remote community: {}", apub_id);
       let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
       let cf = CommunityForm::from_apub(&group, conn)?;
-      Ok(Community::create(conn, &cf)?)
+      let community = Community::create(conn, &cf)?;
+
+      // Also add the community moderators too
+      let creator_and_moderator_uris = group
+        .base
+        .base
+        .object_props
+        .get_many_attributed_to_xsd_any_uris()
+        .unwrap();
+      let creator_and_moderators = creator_and_moderator_uris
+        .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
+        .collect::<Vec<User_>>();
+
+      for mod_ in creator_and_moderators {
+        let community_moderator_form = CommunityModeratorForm {
+          community_id: community.id,
+          user_id: mod_.id,
+        };
+        CommunityModerator::join(&conn, &community_moderator_form)?;
+      }
+
+      Ok(community)
     }
     Err(e) => Err(Error::from(e)),
   }
index 1d86050251cb6ed934d11859253b67aad5f5ba03..3c18a013339a86269d8c75db38c129614c2408ac 100644 (file)
@@ -13,7 +13,7 @@ use crate::api::community::CommunityResponse;
 use crate::websocket::server::SendCommunityRoomMessage;
 use activitystreams::object::kind::{NoteType, PageType};
 use activitystreams::{
-  activity::{Accept, Create, Delete, Dislike, Follow, Like, Undo, Update},
+  activity::{Accept, Create, Delete, Dislike, Follow, Like, Remove, Undo, Update},
   actor::{properties::ApActorProperties, Actor, Group, Person},
   collection::UnorderedCollection,
   context,
@@ -47,13 +47,16 @@ use crate::api::post::PostResponse;
 use crate::api::site::SearchResponse;
 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::community::{
+  Community, CommunityFollower, CommunityFollowerForm, CommunityForm, CommunityModerator,
+  CommunityModeratorForm,
+};
+use crate::db::community_view::{CommunityFollowerView, CommunityModeratorView, CommunityView};
 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::{activity, Crud, Followable, Likeable, SearchType};
+use crate::db::{activity, Crud, Followable, Joinable, Likeable, SearchType};
 use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
 use crate::routes::{ChatServerParam, DbPoolParam};
 use crate::websocket::{
@@ -197,6 +200,8 @@ pub trait ApubObjectType {
   fn send_update(&self, creator: &User_, 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>;
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
 }
 
 pub trait ApubLikeableType {
@@ -239,6 +244,9 @@ pub trait ActorType {
   fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
   fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
 
+  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.
index 5a7383c0e87584367d0467df6d0bcfcd4cba3f6d..2d1f1c710ca739ac572d31ba02f122fd57ee5bb3 100644 (file)
@@ -263,6 +263,92 @@ impl ApubObjectType for Post {
     )?;
     Ok(())
   }
+
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
+    let mut remove = Remove::default();
+
+    populate_object_props(
+      &mut remove.object_props,
+      &community.get_followers_url(),
+      &id,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(page)?;
+
+    // Insert the sent activity into the activity table
+    let activity_form = activity::ActivityForm {
+      user_id: mod_.id,
+      data: serde_json::to_value(&remove)?,
+      local: true,
+      updated: None,
+    };
+    activity::Activity::create(&conn, &activity_form)?;
+
+    let community = Community::read(conn, self.community_id)?;
+    send_activity(
+      &remove,
+      &mod_.private_key.as_ref().unwrap(),
+      &mod_.actor_id,
+      community.get_follower_inboxes(&conn)?,
+    )?;
+    Ok(())
+  }
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
+    let mut remove = Remove::default();
+
+    populate_object_props(
+      &mut remove.object_props,
+      &community.get_followers_url(),
+      &id,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(page)?;
+
+    // Undo that fake activity
+    let undo_id = format!("{}/undo/remove/{}", 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(mod_.actor_id.to_owned())?
+      .set_object_base_box(remove)?;
+
+    // Insert the sent activity into the activity table
+    let activity_form = activity::ActivityForm {
+      user_id: mod_.id,
+      data: serde_json::to_value(&undo)?,
+      local: true,
+      updated: None,
+    };
+    activity::Activity::create(&conn, &activity_form)?;
+
+    let community = Community::read(conn, self.community_id)?;
+    send_activity(
+      &undo,
+      &mod_.private_key.as_ref().unwrap(),
+      &mod_.actor_id,
+      community.get_follower_inboxes(&conn)?,
+    )?;
+    Ok(())
+  }
 }
 
 impl ApubLikeableType for Post {
index a9a61020042c4b74bdad34e61d75df9dc290a639..d77788e552f79b7d875fb4f457800fa0c6a816e6 100644 (file)
@@ -3,12 +3,13 @@ use super::*;
 #[serde(untagged)]
 #[derive(Serialize, Deserialize, Debug)]
 pub enum SharedAcceptedObjects {
-  Create(Create),
-  Update(Update),
-  Like(Like),
-  Dislike(Dislike),
-  Delete(Delete),
-  Undo(Undo),
+  Create(Box<Create>),
+  Update(Box<Update>),
+  Like(Box<Like>),
+  Dislike(Box<Dislike>),
+  Delete(Box<Delete>),
+  Undo(Box<Undo>),
+  Remove(Box<Remove>),
 }
 
 impl SharedAcceptedObjects {
@@ -20,6 +21,7 @@ impl SharedAcceptedObjects {
       SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_object_base_box(),
       SharedAcceptedObjects::Delete(d) => d.delete_props.get_object_base_box(),
       SharedAcceptedObjects::Undo(d) => d.undo_props.get_object_base_box(),
+      SharedAcceptedObjects::Remove(r) => r.remove_props.get_object_base_box(),
     }
   }
 }
@@ -56,6 +58,9 @@ pub async fn shared_inbox(
     (SharedAcceptedObjects::Delete(d), Some("Page")) => {
       receive_delete_post(&d, &request, &conn, chat_server)
     }
+    (SharedAcceptedObjects::Remove(r), Some("Page")) => {
+      receive_remove_post(&r, &request, &conn, chat_server)
+    }
     (SharedAcceptedObjects::Create(c), Some("Note")) => {
       receive_create_comment(&c, &request, &conn, chat_server)
     }
@@ -71,12 +76,21 @@ pub async fn shared_inbox(
     (SharedAcceptedObjects::Delete(d), Some("Note")) => {
       receive_delete_comment(&d, &request, &conn, chat_server)
     }
+    (SharedAcceptedObjects::Remove(r), Some("Note")) => {
+      receive_remove_comment(&r, &request, &conn, chat_server)
+    }
     (SharedAcceptedObjects::Delete(d), Some("Group")) => {
       receive_delete_community(&d, &request, &conn, chat_server)
     }
+    (SharedAcceptedObjects::Remove(r), Some("Group")) => {
+      receive_remove_community(&r, &request, &conn, chat_server)
+    }
     (SharedAcceptedObjects::Undo(u), Some("Delete")) => {
       receive_undo_delete(&u, &request, &conn, chat_server)
     }
+    (SharedAcceptedObjects::Undo(u), Some("Remove")) => {
+      receive_undo_remove(&u, &request, &conn, chat_server)
+    }
     _ => Err(format_err!("Unknown incoming activity type.")),
   }
 }
@@ -588,6 +602,75 @@ fn receive_delete_community(
   Ok(HttpResponse::Ok().finish())
 }
 
+fn receive_remove_community(
+  remove: &Remove,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let mod_uri = remove
+    .remove_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let group = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<GroupExt>()?;
+
+  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?;
+  verify(request, &mod_.public_key.unwrap())?;
+
+  // Insert the received activity into the activity table
+  let activity_form = activity::ActivityForm {
+    user_id: mod_.id,
+    data: serde_json::to_value(&remove)?,
+    local: false,
+    updated: None,
+  };
+  activity::Activity::create(&conn, &activity_form)?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
+  let community = Community::read_from_actor_id(conn, &community_actor_id)?;
+
+  let community_form = CommunityForm {
+    name: community.name.to_owned(),
+    title: community.title.to_owned(),
+    description: community.description.to_owned(),
+    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
+    removed: Some(true),
+    published: None,
+    updated: Some(naive_now()),
+    deleted: None,
+    nsfw: community.nsfw,
+    actor_id: community.actor_id,
+    local: community.local,
+    private_key: community.private_key,
+    public_key: community.public_key,
+    last_refreshed_at: None,
+  };
+
+  Community::update(&conn, community.id, &community_form)?;
+
+  let res = CommunityResponse {
+    community: CommunityView::read(&conn, community.id, None)?,
+  };
+
+  chat_server.do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id: community.id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
 fn receive_delete_post(
   delete: &Delete,
   request: &HttpRequest,
@@ -659,6 +742,77 @@ fn receive_delete_post(
   Ok(HttpResponse::Ok().finish())
 }
 
+fn receive_remove_post(
+  remove: &Remove,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let mod_uri = remove
+    .remove_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let page = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<Page>()?;
+
+  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?;
+  verify(request, &mod_.public_key.unwrap())?;
+
+  // Insert the received activity into the activity table
+  let activity_form = activity::ActivityForm {
+    user_id: mod_.id,
+    data: serde_json::to_value(&remove)?,
+    local: false,
+    updated: None,
+  };
+  activity::Activity::create(&conn, &activity_form)?;
+
+  let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
+  let post = Post::read_from_apub_id(conn, &post_ap_id)?;
+
+  let post_form = PostForm {
+    name: post.name.to_owned(),
+    url: post.url.to_owned(),
+    body: post.body.to_owned(),
+    creator_id: post.creator_id.to_owned(),
+    community_id: post.community_id,
+    removed: Some(true),
+    deleted: None,
+    nsfw: post.nsfw,
+    locked: None,
+    stickied: None,
+    updated: Some(naive_now()),
+    embed_title: post.embed_title,
+    embed_description: post.embed_description,
+    embed_html: post.embed_html,
+    thumbnail_url: post.thumbnail_url,
+    ap_id: post.ap_id,
+    local: post.local,
+    published: None,
+  };
+  Post::update(&conn, post.id, &post_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::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
 fn receive_delete_comment(
   delete: &Delete,
   request: &HttpRequest,
@@ -727,6 +881,74 @@ fn receive_delete_comment(
   Ok(HttpResponse::Ok().finish())
 }
 
+fn receive_remove_comment(
+  remove: &Remove,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let mod_uri = remove
+    .remove_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let note = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<Note>()?;
+
+  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?;
+  verify(request, &mod_.public_key.unwrap())?;
+
+  // Insert the received activity into the activity table
+  let activity_form = activity::ActivityForm {
+    user_id: mod_.id,
+    data: serde_json::to_value(&remove)?,
+    local: false,
+    updated: None,
+  };
+  activity::Activity::create(&conn, &activity_form)?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
+  let comment = Comment::read_from_apub_id(conn, &comment_ap_id)?;
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    post_id: comment.post_id,
+    creator_id: comment.creator_id,
+    removed: Some(true),
+    deleted: None,
+    read: None,
+    published: None,
+    updated: Some(naive_now()),
+    ap_id: comment.ap_id,
+    local: comment.local,
+  };
+  Comment::update(&conn, comment.id, &comment_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::EditComment,
+    comment: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
 fn receive_undo_delete(
   undo: &Undo,
   request: &HttpRequest,
@@ -757,6 +979,36 @@ fn receive_undo_delete(
   }
 }
 
+fn receive_undo_remove(
+  undo: &Undo,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let remove = undo
+    .undo_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<Remove>()?;
+
+  let type_ = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .kind()
+    .unwrap();
+
+  match type_ {
+    "Note" => receive_undo_remove_comment(&remove, &request, &conn, chat_server),
+    "Page" => receive_undo_remove_post(&remove, &request, &conn, chat_server),
+    "Group" => receive_undo_remove_community(&remove, &request, &conn, chat_server),
+    d => Err(format_err!("Undo Delete type {} not supported", d)),
+  }
+}
+
 fn receive_undo_delete_comment(
   delete: &Delete,
   request: &HttpRequest,
@@ -825,6 +1077,74 @@ fn receive_undo_delete_comment(
   Ok(HttpResponse::Ok().finish())
 }
 
+fn receive_undo_remove_comment(
+  remove: &Remove,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let mod_uri = remove
+    .remove_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let note = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<Note>()?;
+
+  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?;
+  verify(request, &mod_.public_key.unwrap())?;
+
+  // Insert the received activity into the activity table
+  let activity_form = activity::ActivityForm {
+    user_id: mod_.id,
+    data: serde_json::to_value(&remove)?,
+    local: false,
+    updated: None,
+  };
+  activity::Activity::create(&conn, &activity_form)?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
+  let comment = Comment::read_from_apub_id(conn, &comment_ap_id)?;
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    post_id: comment.post_id,
+    creator_id: comment.creator_id,
+    removed: Some(false),
+    deleted: None,
+    read: None,
+    published: None,
+    updated: Some(naive_now()),
+    ap_id: comment.ap_id,
+    local: comment.local,
+  };
+  Comment::update(&conn, comment.id, &comment_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::EditComment,
+    comment: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
 fn receive_undo_delete_post(
   delete: &Delete,
   request: &HttpRequest,
@@ -896,6 +1216,77 @@ fn receive_undo_delete_post(
   Ok(HttpResponse::Ok().finish())
 }
 
+fn receive_undo_remove_post(
+  remove: &Remove,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let mod_uri = remove
+    .remove_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let page = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<Page>()?;
+
+  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?;
+  verify(request, &mod_.public_key.unwrap())?;
+
+  // Insert the received activity into the activity table
+  let activity_form = activity::ActivityForm {
+    user_id: mod_.id,
+    data: serde_json::to_value(&remove)?,
+    local: false,
+    updated: None,
+  };
+  activity::Activity::create(&conn, &activity_form)?;
+
+  let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
+  let post = Post::read_from_apub_id(conn, &post_ap_id)?;
+
+  let post_form = PostForm {
+    name: post.name.to_owned(),
+    url: post.url.to_owned(),
+    body: post.body.to_owned(),
+    creator_id: post.creator_id.to_owned(),
+    community_id: post.community_id,
+    removed: Some(false),
+    deleted: None,
+    nsfw: post.nsfw,
+    locked: None,
+    stickied: None,
+    updated: Some(naive_now()),
+    embed_title: post.embed_title,
+    embed_description: post.embed_description,
+    embed_html: post.embed_html,
+    thumbnail_url: post.thumbnail_url,
+    ap_id: post.ap_id,
+    local: post.local,
+    published: None,
+  };
+  Post::update(&conn, post.id, &post_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::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
 fn receive_undo_delete_community(
   delete: &Delete,
   request: &HttpRequest,
@@ -964,3 +1355,72 @@ fn receive_undo_delete_community(
 
   Ok(HttpResponse::Ok().finish())
 }
+
+fn receive_undo_remove_community(
+  remove: &Remove,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let mod_uri = remove
+    .remove_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let group = remove
+    .remove_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<GroupExt>()?;
+
+  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?;
+  verify(request, &mod_.public_key.unwrap())?;
+
+  // Insert the received activity into the activity table
+  let activity_form = activity::ActivityForm {
+    user_id: mod_.id,
+    data: serde_json::to_value(&remove)?,
+    local: false,
+    updated: None,
+  };
+  activity::Activity::create(&conn, &activity_form)?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
+  let community = Community::read_from_actor_id(conn, &community_actor_id)?;
+
+  let community_form = CommunityForm {
+    name: community.name.to_owned(),
+    title: community.title.to_owned(),
+    description: community.description.to_owned(),
+    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
+    removed: Some(false),
+    published: None,
+    updated: Some(naive_now()),
+    deleted: None,
+    nsfw: community.nsfw,
+    actor_id: community.actor_id,
+    local: community.local,
+    private_key: community.private_key,
+    public_key: community.public_key,
+    last_refreshed_at: None,
+  };
+
+  Community::update(&conn, community.id, &community_form)?;
+
+  let res = CommunityResponse {
+    community: CommunityView::read(&conn, community.id, None)?,
+  };
+
+  chat_server.do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id: community.id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
index b5f47e251dcb1197175320d916e68d3383ac6e90..d9c7e86a00ceb568754b6c5b633d703e0f77c78b 100644 (file)
@@ -98,6 +98,14 @@ impl ActorType for User_ {
   fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
     unimplemented!()
   }
+
+  fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
 }
 
 impl FromApub for UserForm {
index e6f7bd86497ac27e1ac3c22832eb85bc9a587bc7..cc240476c1af0e2a05bf3b3d078113ae9e5fd05b 100644 (file)
@@ -603,6 +603,282 @@ describe('main', () => {
       expect(getCommunityResAgain.community.deleted).toBe(false);
     });
   });
+
+  describe('remove things', () => {
+    test('/u/lemmy_beta removes and unremoves a federated comment, post, and community, lemmy_alpha sees its removed.', async () => {
+      // Create a test community
+      let communityName = 'test_community_rem';
+      let communityForm: CommunityForm = {
+        name: communityName,
+        title: communityName,
+        category_id: 1,
+        nsfw: false,
+        auth: lemmyBetaAuth,
+      };
+
+      let createCommunityRes: CommunityResponse = await fetch(
+        `${lemmyBetaApiUrl}/community`,
+        {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(communityForm),
+        }
+      ).then(d => d.json());
+
+      expect(createCommunityRes.community.name).toBe(communityName);
+
+      // Cache it on lemmy_alpha
+      let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/${communityName}&type_=All&sort=TopAll`;
+      let searchResponse: SearchResponse = await fetch(searchUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+
+      let communityOnAlphaId = searchResponse.communities[0].id;
+
+      // Follow it
+      let followForm: FollowCommunityForm = {
+        community_id: communityOnAlphaId,
+        follow: true,
+        auth: lemmyAlphaAuth,
+      };
+
+      let followRes: 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(followRes.community.local).toBe(false);
+      expect(followRes.community.name).toBe(communityName);
+
+      // Lemmy beta creates a test post
+      let postName = 'A jest test post with remove';
+      let createPostForm: PostForm = {
+        name: postName,
+        auth: lemmyBetaAuth,
+        community_id: createCommunityRes.community.id,
+        creator_id: 2,
+        nsfw: false,
+      };
+
+      let createPostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: wrapper(createPostForm),
+      }).then(d => d.json());
+      expect(createPostRes.post.name).toBe(postName);
+
+      // Lemmy beta creates a test comment
+      let commentContent = 'A jest test federated comment with remove';
+      let createCommentForm: CommentForm = {
+        content: commentContent,
+        post_id: createPostRes.post.id,
+        auth: lemmyBetaAuth,
+      };
+
+      let createCommentRes: CommentResponse = await fetch(
+        `${lemmyBetaApiUrl}/comment`,
+        {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(createCommentForm),
+        }
+      ).then(d => d.json());
+
+      expect(createCommentRes.comment.content).toBe(commentContent);
+
+      // lemmy_beta removes the comment
+      let removeCommentForm: CommentForm = {
+        content: commentContent,
+        edit_id: createCommentRes.comment.id,
+        post_id: createPostRes.post.id,
+        removed: true,
+        auth: lemmyBetaAuth,
+        creator_id: createCommentRes.comment.creator_id,
+      };
+
+      let removeCommentRes: CommentResponse = await fetch(
+        `${lemmyBetaApiUrl}/comment`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(removeCommentForm),
+        }
+      ).then(d => d.json());
+      expect(removeCommentRes.comment.removed).toBe(true);
+
+      // lemmy_alpha sees that the comment is removed
+      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=4`;
+      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+      expect(getPostRes.comments[0].removed).toBe(true);
+
+      // lemmy_beta undeletes the comment
+      let unremoveCommentForm: CommentForm = {
+        content: commentContent,
+        edit_id: createCommentRes.comment.id,
+        post_id: createPostRes.post.id,
+        removed: false,
+        auth: lemmyBetaAuth,
+        creator_id: createCommentRes.comment.creator_id,
+      };
+
+      let unremoveCommentRes: CommentResponse = await fetch(
+        `${lemmyBetaApiUrl}/comment`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(unremoveCommentForm),
+        }
+      ).then(d => d.json());
+      expect(unremoveCommentRes.comment.removed).toBe(false);
+
+      // lemmy_alpha sees that the comment is undeleted
+      let getPostUnremoveRes: GetPostResponse = await fetch(getPostUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+      expect(getPostUnremoveRes.comments[0].removed).toBe(false);
+
+      // lemmy_beta deletes the post
+      let removePostForm: PostForm = {
+        name: postName,
+        edit_id: createPostRes.post.id,
+        auth: lemmyBetaAuth,
+        community_id: createPostRes.post.community_id,
+        creator_id: createPostRes.post.creator_id,
+        nsfw: false,
+        removed: true,
+      };
+
+      let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: wrapper(removePostForm),
+      }).then(d => d.json());
+      expect(removePostRes.post.removed).toBe(true);
+
+      // Make sure lemmy_alpha sees the post is deleted
+      let getPostResAgain: GetPostResponse = await fetch(getPostUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+      expect(getPostResAgain.post.removed).toBe(true);
+
+      // lemmy_beta unremoves the post
+      let unremovePostForm: PostForm = {
+        name: postName,
+        edit_id: createPostRes.post.id,
+        auth: lemmyBetaAuth,
+        community_id: createPostRes.post.community_id,
+        creator_id: createPostRes.post.creator_id,
+        nsfw: false,
+        removed: false,
+      };
+
+      let unremovePostRes: PostResponse = await fetch(
+        `${lemmyBetaApiUrl}/post`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(unremovePostForm),
+        }
+      ).then(d => d.json());
+      expect(unremovePostRes.post.removed).toBe(false);
+
+      // Make sure lemmy_alpha sees the post is unremoved
+      let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+      expect(getPostResAgainTwo.post.removed).toBe(false);
+
+      // lemmy_beta deletes the community
+      let removeCommunityForm: CommunityForm = {
+        name: communityName,
+        title: communityName,
+        category_id: 1,
+        edit_id: createCommunityRes.community.id,
+        nsfw: false,
+        removed: true,
+        auth: lemmyBetaAuth,
+      };
+
+      let removeCommunityRes: CommunityResponse = await fetch(
+        `${lemmyBetaApiUrl}/community`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(removeCommunityForm),
+        }
+      ).then(d => d.json());
+
+      // Make sure the delete went through
+      expect(removeCommunityRes.community.removed).toBe(true);
+
+      // Re-get it from alpha, make sure its removed there too
+      let getCommunityUrl = `${lemmyAlphaApiUrl}/community?id=${communityOnAlphaId}&auth=${lemmyAlphaAuth}`;
+      let getCommunityRes: GetCommunityResponse = await fetch(getCommunityUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+
+      expect(getCommunityRes.community.removed).toBe(true);
+
+      // lemmy_beta unremoves the community
+      let unremoveCommunityForm: CommunityForm = {
+        name: communityName,
+        title: communityName,
+        category_id: 1,
+        edit_id: createCommunityRes.community.id,
+        nsfw: false,
+        removed: false,
+        auth: lemmyBetaAuth,
+      };
+
+      let unremoveCommunityRes: CommunityResponse = await fetch(
+        `${lemmyBetaApiUrl}/community`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(unremoveCommunityForm),
+        }
+      ).then(d => d.json());
+
+      // Make sure the delete went through
+      expect(unremoveCommunityRes.community.removed).toBe(false);
+
+      // Re-get it from alpha, make sure its deleted there too
+      let getCommunityResAgain: GetCommunityResponse = await fetch(
+        getCommunityUrl,
+        {
+          method: 'GET',
+        }
+      ).then(d => d.json());
+      expect(getCommunityResAgain.community.removed).toBe(false);
+    });
+  });
 });
 
 function wrapper(form: any): string {