From 0c0c68398609d549a757a9c3a26ce2311075fa38 Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 28 Apr 2020 19:46:25 +0200 Subject: [PATCH 1/7] Implement deleting communities --- server/src/api/community.rs | 7 +++- server/src/apub/comment.rs | 12 +++--- server/src/apub/community.rs | 46 ++++++++++++++++++++-- server/src/apub/mod.rs | 30 +++++++++++++-- server/src/apub/post.rs | 12 +++--- server/src/apub/shared_inbox.rs | 68 +++++++++++++++++++++++++++++++++ server/src/apub/user.rs | 10 ++++- 7 files changed, 163 insertions(+), 22 deletions(-) diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 296a77eaf..7610d1b78 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -358,7 +358,7 @@ impl Perform for Oper { published: None, }; - let _updated_community = match Community::update(&conn, data.edit_id, &community_form) { + let updated_community = match Community::update(&conn, data.edit_id, &community_form) { Ok(community) => community, Err(_e) => return Err(APIError::err("couldnt_update_community").into()), }; @@ -377,6 +377,11 @@ impl Perform for Oper { expires, }; ModRemoveCommunity::create(&conn, &form)?; + updated_community.send_delete(&conn)?; + } + + if let Some(_deleted) = data.deleted.to_owned() { + updated_community.send_delete(&conn)?; } let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?; diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 6cede17b2..9fa5731ba 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -3,7 +3,7 @@ use super::*; impl ToApub for Comment { type Response = Note; - fn to_apub(&self, conn: &PgConnection) -> Result { + fn to_apub(&self, conn: &PgConnection) -> Result, Error> { let mut comment = Note::default(); let oprops: &mut ObjectProperties = comment.as_mut(); let creator = User_::read(&conn, self.creator_id)?; @@ -33,7 +33,7 @@ impl ToApub for Comment { oprops.set_updated(convert_datetime(u))?; } - Ok(comment) + Ok(ResponseOrTombstone::Response(comment)) } } @@ -102,7 +102,7 @@ impl ApubObjectType for Comment { create .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_object_base_box(note.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -138,7 +138,7 @@ impl ApubObjectType for Comment { update .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_object_base_box(note.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -171,7 +171,7 @@ impl ApubLikeableType for Comment { like .like_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_object_base_box(note.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -206,7 +206,7 @@ impl ApubLikeableType for Comment { dislike .dislike_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_object_base_box(note.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 46bd9024c..ee3199954 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -9,7 +9,17 @@ impl ToApub for Community { type Response = GroupExt; // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. - fn to_apub(&self, conn: &PgConnection) -> Result { + fn to_apub(&self, conn: &PgConnection) -> Result, Error> { + if self.deleted || self.removed { + let mut tombstone = Tombstone::default(); + // TODO: might want to include updated/deleted times as well + tombstone + .object_props + .set_id(self.actor_id.to_owned())? + .set_published(convert_datetime(self.published))?; + return Ok(ResponseOrTombstone::Tombstone(Box::new(tombstone))); + } + let mut group = Group::default(); let oprops: &mut ObjectProperties = group.as_mut(); @@ -43,7 +53,9 @@ impl ToApub for Community { .set_endpoints(endpoint_props)? .set_followers(self.get_followers_url())?; - Ok(group.extend(actor_props).extend(self.get_public_key_ext())) + Ok(ResponseOrTombstone::Response( + group.extend(actor_props).extend(self.get_public_key_ext()), + )) } } @@ -94,10 +106,36 @@ impl ActorType for Community { Ok(()) } + fn send_delete(&self, conn: &PgConnection) -> Result<(), Error> { + let community = self.to_apub(conn)?; + let mut delete = Delete::default(); + delete + .delete_props + .set_actor_xsd_any_uri(self.actor_id.to_owned())? + .set_object_base_box(BaseBox::from_concrete( + community.as_tombstone()?.to_owned(), + )?)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_id, + data: serde_json::to_value(&delete)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &delete, + &self.private_key.to_owned().unwrap(), + &self.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, Error> { - debug!("got here."); - Ok( CommunityFollowerView::for_community(conn, self.id)? .into_iter() diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 4b08c53aa..9232c2d7e 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -10,13 +10,13 @@ pub mod user; pub mod user_inbox; use activitystreams::{ - activity::{Accept, Create, Dislike, Follow, Like, Update}, + activity::{Accept, Create, Delete, Dislike, Follow, Like, Update}, actor::{properties::ApActorProperties, Actor, Group, Person}, collection::UnorderedCollection, context, endpoint::EndpointProperties, ext::{Ext, Extensible, Extension}, - object::{properties::ObjectProperties, Note, Page}, + object::{properties::ObjectProperties, Note, Page, Tombstone}, public, BaseBox, }; use actix_web::body::Body; @@ -138,10 +138,31 @@ fn is_apub_id_valid(apub_id: &Url) -> bool { } } +#[derive(Serialize)] +pub enum ResponseOrTombstone { + Response(Response), + Tombstone(Box), +} + +impl ResponseOrTombstone { + fn as_response(&self) -> Result<&Response, Error> { + match self { + ResponseOrTombstone::Response(r) => Ok(r), + ResponseOrTombstone::Tombstone(_t) => Err(format_err!("Value is a tombstone")), + } + } + fn as_tombstone(&self) -> Result<&Tombstone, Error> { + match self { + ResponseOrTombstone::Tombstone(t) => Ok(t), + ResponseOrTombstone::Response(_r) => Err(format_err!("Value is a response")), + } + } +} + // TODO Not sure good names for these pub trait ToApub { type Response; - fn to_apub(&self, conn: &PgConnection) -> Result; + fn to_apub(&self, conn: &PgConnection) -> Result, Error>; } pub trait FromApub { @@ -154,6 +175,7 @@ pub trait FromApub { pub trait ApubObjectType { fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + //fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; } pub trait ApubLikeableType { @@ -192,6 +214,8 @@ pub trait ActorType { Err(format_err!("Accept not implemented.")) } + fn send_delete(&self, 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. diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index af8ee5998..381ba3c6c 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -19,7 +19,7 @@ impl ToApub for Post { type Response = Page; // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. - fn to_apub(&self, conn: &PgConnection) -> Result { + fn to_apub(&self, conn: &PgConnection) -> Result, Error> { let mut page = Page::default(); let oprops: &mut ObjectProperties = page.as_mut(); let creator = User_::read(conn, self.creator_id)?; @@ -51,7 +51,7 @@ impl ToApub for Post { oprops.set_updated(convert_datetime(u))?; } - Ok(page) + Ok(ResponseOrTombstone::Response(page)) } } @@ -109,7 +109,7 @@ impl ApubObjectType for Post { create .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page)?; + .set_object_base_box(page.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -144,7 +144,7 @@ impl ApubObjectType for Post { update .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page)?; + .set_object_base_box(page.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -176,7 +176,7 @@ impl ApubLikeableType for Post { like .like_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page)?; + .set_object_base_box(page.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -210,7 +210,7 @@ impl ApubLikeableType for Post { dislike .dislike_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page)?; + .set_object_base_box(page.as_response()?.to_owned())?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 705a578a0..10ed1122d 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -1,4 +1,6 @@ use super::*; +use crate::api::community::CommunityResponse; +use crate::websocket::server::SendCommunityRoomMessage; #[serde(untagged)] #[derive(Serialize, Deserialize, Debug)] @@ -7,6 +9,7 @@ pub enum SharedAcceptedObjects { Update(Update), Like(Like), Dislike(Dislike), + Delete(Delete), } impl SharedAcceptedObjects { @@ -16,6 +19,7 @@ impl SharedAcceptedObjects { 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(), + SharedAcceptedObjects::Delete(d) => d.delete_props.get_object_base_box(), } } } @@ -61,6 +65,9 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Dislike(d), Some("Note")) => { receive_dislike_comment(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Delete(d), Some("Tombstone")) => { + receive_delete_community(&d, &request, &conn, chat_server) + } _ => Err(format_err!("Unknown incoming activity type.")), } } @@ -502,3 +509,64 @@ fn receive_dislike_comment( Ok(HttpResponse::Ok().finish()) } + +fn receive_delete_community( + delete: &Delete, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let tombstone = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; + let community_apub_id = tombstone.object_props.get_id().unwrap().to_string(); + + let community = Community::read_from_actor_id(conn, &community_apub_id)?; + verify(request, &community.public_key.clone().unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: community.creator_id, + data: serde_json::to_value(&delete)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community_form = CommunityForm { + name: "".to_string(), + title: "".to_string(), + description: None, + 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: None, + published: None, + updated: None, + deleted: Some(true), + nsfw: false, + actor_id: community.actor_id, + local: false, + private_key: None, + public_key: community.public_key, + last_refreshed_at: Some(community.last_refreshed_at), + }; + + 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()) +} diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index d7fd22826..36147f7a0 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -9,7 +9,7 @@ impl ToApub for User_ { type Response = PersonExt; // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. - fn to_apub(&self, _conn: &PgConnection) -> Result { + fn to_apub(&self, _conn: &PgConnection) -> Result, Error> { // TODO go through all these to_string and to_owned() let mut person = Person::default(); let oprops: &mut ObjectProperties = person.as_mut(); @@ -41,7 +41,9 @@ impl ToApub for User_ { .set_following(self.get_following_url())? .set_liked(self.get_liked_url())?; - Ok(person.extend(actor_props).extend(self.get_public_key_ext())) + Ok(ResponseOrTombstone::Response( + person.extend(actor_props).extend(self.get_public_key_ext()), + )) } } @@ -87,6 +89,10 @@ impl ActorType for User_ { )?; Ok(()) } + + fn send_delete(&self, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } } impl FromApub for UserForm { From c43f06124a00f3af8559d849541c2b84d3383f13 Mon Sep 17 00:00:00 2001 From: Felix Date: Wed, 29 Apr 2020 16:51:25 +0200 Subject: [PATCH 2/7] Address comments, implement delete for posts and comments --- server/src/api/comment.rs | 8 +++++ server/src/api/community.rs | 9 +++-- server/src/api/post.rs | 8 +++++ server/src/apub/comment.rs | 46 ++++++++++++++++++++---- server/src/apub/community.rs | 36 +++++++++---------- server/src/apub/mod.rs | 64 +++++++++++++++++++++------------ server/src/apub/post.rs | 50 ++++++++++++++++++++++---- server/src/apub/shared_inbox.rs | 1 + server/src/apub/user.rs | 6 ++-- 9 files changed, 165 insertions(+), 63 deletions(-) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index a6742e4c0..17b52d2f5 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -339,6 +339,14 @@ impl Perform for Oper { updated_comment.send_update(&user, &conn)?; + if let Some(deleted) = data.deleted.to_owned() { + if deleted { + updated_comment.send_delete(&user, &conn)?; + } else { + // TODO: undo delete + } + } + let mut recipient_ids = Vec::new(); // Scan the comment for user mentions, add those rows diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 7610d1b78..d7f16c50c 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -377,11 +377,14 @@ impl Perform for Oper { expires, }; ModRemoveCommunity::create(&conn, &form)?; - updated_community.send_delete(&conn)?; } - if let Some(_deleted) = data.deleted.to_owned() { - updated_community.send_delete(&conn)?; + if let Some(deleted) = data.deleted.to_owned() { + if deleted { + updated_community.send_delete(&conn)?; + } else { + // TODO: undo delete + } } let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?; diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 306365fac..56c133737 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -543,6 +543,14 @@ impl Perform for Oper { updated_post.send_update(&user, &conn)?; + if let Some(deleted) = data.deleted.to_owned() { + if deleted { + updated_post.send_delete(&user, &conn)?; + } else { + // TODO: undo delete + } + } + let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; let res = PostResponse { post: post_view }; diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 9fa5731ba..4a5f18b7c 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -3,7 +3,7 @@ use super::*; impl ToApub for Comment { type Response = Note; - fn to_apub(&self, conn: &PgConnection) -> Result, Error> { + fn to_apub(&self, conn: &PgConnection) -> Result { let mut comment = Note::default(); let oprops: &mut ObjectProperties = comment.as_mut(); let creator = User_::read(&conn, self.creator_id)?; @@ -33,7 +33,13 @@ impl ToApub for Comment { oprops.set_updated(convert_datetime(u))?; } - Ok(ResponseOrTombstone::Response(comment)) + Ok(comment) + } +} + +impl ToTombstone for Comment { + fn to_tombstone(&self) -> Result { + create_tombstone(self.deleted, &self.ap_id, self.published, self.updated) } } @@ -102,7 +108,7 @@ impl ApubObjectType for Comment { create .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note.as_response()?.to_owned())?; + .set_object_base_box(note)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -138,7 +144,7 @@ impl ApubObjectType for Comment { update .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note.as_response()?.to_owned())?; + .set_object_base_box(note)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -157,6 +163,34 @@ impl ApubObjectType for Comment { )?; Ok(()) } + + // TODO: this code is literally copied from post.rs + fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error> { + let mut delete = Delete::default(); + delete + .delete_props + .set_actor_xsd_any_uri(actor.actor_id.to_owned())? + .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_id, + data: serde_json::to_value(&delete)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let post = Post::read(conn, self.post_id)?; + let community = Community::read(conn, post.community_id)?; + send_activity( + &delete, + &actor.private_key.to_owned().unwrap(), + &actor.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } impl ApubLikeableType for Comment { @@ -171,7 +205,7 @@ impl ApubLikeableType for Comment { like .like_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note.as_response()?.to_owned())?; + .set_object_base_box(note)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -206,7 +240,7 @@ impl ApubLikeableType for Comment { dislike .dislike_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note.as_response()?.to_owned())?; + .set_object_base_box(note)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index ee3199954..36e33c895 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -9,17 +9,7 @@ impl ToApub for Community { type Response = GroupExt; // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. - fn to_apub(&self, conn: &PgConnection) -> Result, Error> { - if self.deleted || self.removed { - let mut tombstone = Tombstone::default(); - // TODO: might want to include updated/deleted times as well - tombstone - .object_props - .set_id(self.actor_id.to_owned())? - .set_published(convert_datetime(self.published))?; - return Ok(ResponseOrTombstone::Tombstone(Box::new(tombstone))); - } - + fn to_apub(&self, conn: &PgConnection) -> Result { let mut group = Group::default(); let oprops: &mut ObjectProperties = group.as_mut(); @@ -53,9 +43,13 @@ impl ToApub for Community { .set_endpoints(endpoint_props)? .set_followers(self.get_followers_url())?; - Ok(ResponseOrTombstone::Response( - group.extend(actor_props).extend(self.get_public_key_ext()), - )) + Ok(group.extend(actor_props).extend(self.get_public_key_ext())) + } +} + +impl ToTombstone for Community { + fn to_tombstone(&self) -> Result { + create_tombstone(self.deleted, &self.actor_id, self.published, self.updated) } } @@ -107,14 +101,11 @@ impl ActorType for Community { } fn send_delete(&self, conn: &PgConnection) -> Result<(), Error> { - let community = self.to_apub(conn)?; let mut delete = Delete::default(); delete .delete_props .set_actor_xsd_any_uri(self.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete( - community.as_tombstone()?.to_owned(), - )?)?; + .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -208,8 +199,13 @@ pub async fn get_apub_community_http( db: DbPoolParam, ) -> Result, Error> { let community = Community::read_from_name(&&db.get()?, &info.community_name)?; - let c = community.to_apub(&db.get().unwrap())?; - Ok(create_apub_response(&c)) + if !community.deleted { + Ok(create_apub_response( + &community.to_apub(&db.get().unwrap())?, + )) + } else { + Ok(create_apub_tombstone_response(&community.to_tombstone()?)) + } } /// Returns an empty followers collection, only populating the siz (for privacy). diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 9232c2d7e..9d312cc3d 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -60,6 +60,7 @@ use crate::websocket::{ use crate::{convert_datetime, naive_now, Settings}; use activities::{populate_object_props, send_activity}; +use chrono::NaiveDateTime; use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}; use signatures::verify; use signatures::{sign, PublicKey, PublicKeyExtension}; @@ -86,6 +87,14 @@ where .content_type(APUB_JSON_CONTENT_TYPE) .json(data) } +fn create_apub_tombstone_response(data: &T) -> HttpResponse +where + T: Serialize, +{ + HttpResponse::Gone() + .content_type(APUB_JSON_CONTENT_TYPE) + .json(data) +} /// Generates the ActivityPub ID for a given object type and name. /// @@ -138,31 +147,40 @@ fn is_apub_id_valid(apub_id: &Url) -> bool { } } -#[derive(Serialize)] -pub enum ResponseOrTombstone { - Response(Response), - Tombstone(Box), -} - -impl ResponseOrTombstone { - fn as_response(&self) -> Result<&Response, Error> { - match self { - ResponseOrTombstone::Response(r) => Ok(r), - ResponseOrTombstone::Tombstone(_t) => Err(format_err!("Value is a tombstone")), - } - } - fn as_tombstone(&self) -> Result<&Tombstone, Error> { - match self { - ResponseOrTombstone::Tombstone(t) => Ok(t), - ResponseOrTombstone::Response(_r) => Err(format_err!("Value is a response")), - } - } -} - // TODO Not sure good names for these pub trait ToApub { type Response; - fn to_apub(&self, conn: &PgConnection) -> Result, Error>; + fn to_apub(&self, conn: &PgConnection) -> Result; +} + +fn create_tombstone( + deleted: bool, + object_id: &str, + published: NaiveDateTime, + updated: Option, +) -> Result { + if deleted { + let mut tombstone = Tombstone::default(); + // TODO: might want to include deleted time as well + tombstone + .object_props + .set_id(object_id)? + .set_published(convert_datetime(published))?; + if let Some(updated) = updated { + tombstone + .object_props + .set_updated(convert_datetime(updated))?; + } + Ok(tombstone) + } else { + Err(format_err!( + "Cant convert object to tombstone if it wasnt deleted" + )) + } +} + +pub trait ToTombstone { + fn to_tombstone(&self) -> Result; } pub trait FromApub { @@ -175,7 +193,7 @@ pub trait FromApub { pub trait ApubObjectType { fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - //fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error>; } pub trait ApubLikeableType { diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 381ba3c6c..77f8d9842 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -12,14 +12,18 @@ pub async fn get_apub_post( ) -> Result, Error> { let id = info.post_id.parse::()?; let post = Post::read(&&db.get()?, id)?; - Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?)) + if !post.deleted { + Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?)) + } else { + Ok(create_apub_tombstone_response(&post.to_tombstone()?)) + } } impl ToApub for Post { type Response = Page; // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. - fn to_apub(&self, conn: &PgConnection) -> Result, Error> { + fn to_apub(&self, conn: &PgConnection) -> Result { let mut page = Page::default(); let oprops: &mut ObjectProperties = page.as_mut(); let creator = User_::read(conn, self.creator_id)?; @@ -51,7 +55,13 @@ impl ToApub for Post { oprops.set_updated(convert_datetime(u))?; } - Ok(ResponseOrTombstone::Response(page)) + Ok(page) + } +} + +impl ToTombstone for Post { + fn to_tombstone(&self) -> Result { + create_tombstone(self.deleted, &self.ap_id, self.published, self.updated) } } @@ -109,7 +119,7 @@ impl ApubObjectType for Post { create .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page.as_response()?.to_owned())?; + .set_object_base_box(page)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -144,7 +154,7 @@ impl ApubObjectType for Post { update .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page.as_response()?.to_owned())?; + .set_object_base_box(page)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -163,6 +173,32 @@ impl ApubObjectType for Post { )?; Ok(()) } + + fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error> { + let mut delete = Delete::default(); + delete + .delete_props + .set_actor_xsd_any_uri(actor.actor_id.to_owned())? + .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_id, + data: serde_json::to_value(&delete)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community = Community::read(conn, self.community_id)?; + send_activity( + &delete, + &actor.private_key.to_owned().unwrap(), + &actor.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } impl ApubLikeableType for Post { @@ -176,7 +212,7 @@ impl ApubLikeableType for Post { like .like_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page.as_response()?.to_owned())?; + .set_object_base_box(page)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -210,7 +246,7 @@ impl ApubLikeableType for Post { dislike .dislike_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(page.as_response()?.to_owned())?; + .set_object_base_box(page)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 10ed1122d..28cb71e2a 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -66,6 +66,7 @@ pub async fn shared_inbox( receive_dislike_comment(&d, &request, &conn, chat_server) } (SharedAcceptedObjects::Delete(d), Some("Tombstone")) => { + // TODO: is this deleting a community, post, comment or what? receive_delete_community(&d, &request, &conn, chat_server) } _ => Err(format_err!("Unknown incoming activity type.")), diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index 36147f7a0..7426efd5e 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -9,7 +9,7 @@ impl ToApub for User_ { type Response = PersonExt; // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. - fn to_apub(&self, _conn: &PgConnection) -> Result, Error> { + fn to_apub(&self, _conn: &PgConnection) -> Result { // TODO go through all these to_string and to_owned() let mut person = Person::default(); let oprops: &mut ObjectProperties = person.as_mut(); @@ -41,9 +41,7 @@ impl ToApub for User_ { .set_following(self.get_following_url())? .set_liked(self.get_liked_url())?; - Ok(ResponseOrTombstone::Response( - person.extend(actor_props).extend(self.get_public_key_ext()), - )) + Ok(person.extend(actor_props).extend(self.get_public_key_ext())) } } From 770dcbdc49c9bf01f781e5259dc8e6d816143344 Mon Sep 17 00:00:00 2001 From: Felix Date: Wed, 29 Apr 2020 21:10:50 +0200 Subject: [PATCH 3/7] wip: add former_type to tombstone --- server/src/apub/comment.rs | 2 +- server/src/apub/community.rs | 3 ++- server/src/apub/mod.rs | 9 ++++++++- server/src/apub/post.rs | 2 +- server/src/apub/shared_inbox.rs | 14 +++++++++----- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 4a5f18b7c..14a099565 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -39,7 +39,7 @@ impl ToApub for Comment { impl ToTombstone for Comment { fn to_tombstone(&self) -> Result { - create_tombstone(self.deleted, &self.ap_id, self.published, self.updated) + create_tombstone(self.deleted, &self.ap_id, self.published, self.updated, NoteType.to_string()) } } diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 36e33c895..0bfb95d2b 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -1,4 +1,5 @@ use super::*; +use activitystreams::actor::kind::GroupType; #[derive(Deserialize)] pub struct CommunityQuery { @@ -49,7 +50,7 @@ impl ToApub for Community { impl ToTombstone for Community { fn to_tombstone(&self) -> Result { - create_tombstone(self.deleted, &self.actor_id, self.published, self.updated) + create_tombstone(self.deleted, &self.actor_id, self.published, self.updated, GroupType.to_string()) } } diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 9d312cc3d..f72eb17e3 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -19,6 +19,9 @@ use activitystreams::{ object::{properties::ObjectProperties, Note, Page, Tombstone}, public, BaseBox, }; +use activitystreams::object::kind::{NoteType, PageType}; +use crate::api::community::CommunityResponse; +use crate::websocket::server::SendCommunityRoomMessage; use actix_web::body::Body; use actix_web::web::Path; use actix_web::{web, HttpRequest, HttpResponse, Result}; @@ -64,6 +67,7 @@ use chrono::NaiveDateTime; use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}; use signatures::verify; use signatures::{sign, PublicKey, PublicKeyExtension}; +use activitystreams::primitives::XsdString; type GroupExt = Ext, PublicKeyExtension>; type PersonExt = Ext, PublicKeyExtension>; @@ -87,6 +91,7 @@ where .content_type(APUB_JSON_CONTENT_TYPE) .json(data) } + fn create_apub_tombstone_response(data: &T) -> HttpResponse where T: Serialize, @@ -158,6 +163,7 @@ fn create_tombstone( object_id: &str, published: NaiveDateTime, updated: Option, + former_type: String, ) -> Result { if deleted { let mut tombstone = Tombstone::default(); @@ -165,12 +171,13 @@ fn create_tombstone( tombstone .object_props .set_id(object_id)? - .set_published(convert_datetime(published))?; + .set_published(convert_datetime(published)); if let Some(updated) = updated { tombstone .object_props .set_updated(convert_datetime(updated))?; } + tombstone.tombstone_props.set_former_type_object_box(XsdString::from_string(former_type))?; Ok(tombstone) } else { Err(format_err!( diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 77f8d9842..356141819 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -61,7 +61,7 @@ impl ToApub for Post { impl ToTombstone for Post { fn to_tombstone(&self) -> Result { - create_tombstone(self.deleted, &self.ap_id, self.published, self.updated) + create_tombstone(self.deleted, &self.ap_id, self.published, self.updated, PageType.to_string()) } } diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 28cb71e2a..692e6f2b1 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -1,6 +1,4 @@ use super::*; -use crate::api::community::CommunityResponse; -use crate::websocket::server::SendCommunityRoomMessage; #[serde(untagged)] #[derive(Serialize, Deserialize, Debug)] @@ -66,8 +64,7 @@ pub async fn shared_inbox( receive_dislike_comment(&d, &request, &conn, chat_server) } (SharedAcceptedObjects::Delete(d), Some("Tombstone")) => { - // TODO: is this deleting a community, post, comment or what? - receive_delete_community(&d, &request, &conn, chat_server) + receive_delete(&d, &request, &conn, chat_server) } _ => Err(format_err!("Unknown incoming activity type.")), } @@ -511,7 +508,7 @@ fn receive_dislike_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_delete_community( +fn receive_delete( delete: &Delete, request: &HttpRequest, conn: &PgConnection, @@ -524,6 +521,13 @@ fn receive_delete_community( .unwrap() .to_owned() .to_concrete::()?; + // TODO: not sure how to handle formerType (should be a string) + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-formertype + let former_type: &str = tombstone.tombstone_props.get_former_type_object_box().unwrap().to_concrete::(); + match former_type { + "Group" => {}, + d => return Err(format_err!("Delete type {} not supported", d)), + } let community_apub_id = tombstone.object_props.get_id().unwrap().to_string(); let community = Community::read_from_actor_id(conn, &community_apub_id)?; From 461114c143aebf02f70de2b21d8b1272f67f12a2 Mon Sep 17 00:00:00 2001 From: Felix Date: Thu, 30 Apr 2020 18:30:01 +0200 Subject: [PATCH 4/7] update activitystreams lib --- server/Cargo.lock | 8 ++++---- server/Cargo.toml | 2 +- server/src/apub/mod.rs | 5 ++--- server/src/apub/shared_inbox.rs | 25 ++++++++++++------------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index 56e81b476..714422b64 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2,9 +2,9 @@ # It is not intended for manual editing. [[package]] name = "activitystreams" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae98a55a86fc3150f278b225644cd46b5359f4d75067eae6dc3a52b409c537fb" +checksum = "dd5b29a0f2c64cc56f2b79ec29cab68a9dab3b714d811a55668d072f18a8638e" dependencies = [ "activitystreams-derive", "chrono", @@ -17,9 +17,9 @@ dependencies = [ [[package]] name = "activitystreams-derive" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d0384ae423a1df266f216e351ce9b40e8d369467d9242c086121154b4327dd" +checksum = "985d3ca1ee226e83f4118e0235bc11d9fce39c4eec8d53739a21b01dd0b3f30f" dependencies = [ "proc-macro2", "quote", diff --git a/server/Cargo.toml b/server/Cargo.toml index 2777dd2c7..ab76d06fa 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] } diesel_migrations = "1.4.0" dotenv = "0.15.0" -activitystreams = "0.5.0-alpha.16" +activitystreams = "0.6.0" bcrypt = "0.6.2" chrono = { version = "0.4.7", features = ["serde"] } failure = "0.1.5" diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index f72eb17e3..03c43bdc2 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -67,7 +67,6 @@ use chrono::NaiveDateTime; use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}; use signatures::verify; use signatures::{sign, PublicKey, PublicKeyExtension}; -use activitystreams::primitives::XsdString; type GroupExt = Ext, PublicKeyExtension>; type PersonExt = Ext, PublicKeyExtension>; @@ -171,13 +170,13 @@ fn create_tombstone( tombstone .object_props .set_id(object_id)? - .set_published(convert_datetime(published)); + .set_published(convert_datetime(published))?; if let Some(updated) = updated { tombstone .object_props .set_updated(convert_datetime(updated))?; } - tombstone.tombstone_props.set_former_type_object_box(XsdString::from_string(former_type))?; + tombstone.tombstone_props.set_former_type_xsd_string(former_type)?; Ok(tombstone) } else { Err(format_err!( diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 692e6f2b1..7d3826f22 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -82,7 +82,7 @@ fn receive_create_post( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = create .create_props @@ -131,7 +131,7 @@ fn receive_create_comment( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = create .create_props @@ -185,7 +185,7 @@ fn receive_update_post( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = update .update_props @@ -235,7 +235,7 @@ fn receive_like_post( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string(); @@ -288,7 +288,7 @@ fn receive_dislike_post( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = dislike .dislike_props @@ -345,7 +345,7 @@ fn receive_update_comment( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = update .update_props @@ -400,7 +400,7 @@ fn receive_like_comment( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string(); @@ -458,7 +458,7 @@ fn receive_dislike_comment( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; + .into_concrete::()?; let user_uri = dislike .dislike_props @@ -520,11 +520,10 @@ fn receive_delete( .to_owned() .unwrap() .to_owned() - .to_concrete::()?; - // TODO: not sure how to handle formerType (should be a string) - // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-formertype - let former_type: &str = tombstone.tombstone_props.get_former_type_object_box().unwrap().to_concrete::(); - match former_type { + .into_concrete::()?; + let former_type = tombstone.tombstone_props.get_former_type_xsd_string().unwrap().to_string(); + // TODO: handle these + match former_type.as_str() { "Group" => {}, d => return Err(format_err!("Delete type {} not supported", d)), } From 2f1cd9976dbdc034836a01748086056999aff52a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 1 May 2020 10:07:38 -0400 Subject: [PATCH 5/7] Adding federated community, comment, and post deletes. - Unit tests added too. - No undeletes working yet. --- server/src/api/comment.rs | 4 +- server/src/api/community.rs | 7 +- server/src/api/post.rs | 4 +- server/src/apub/comment.rs | 33 ++++-- server/src/apub/community.rs | 27 +++-- server/src/apub/mod.rs | 36 +++--- server/src/apub/post.rs | 29 +++-- server/src/apub/shared_inbox.rs | 195 ++++++++++++++++++++++++++++---- server/src/apub/user.rs | 5 +- server/src/db/community.rs | 1 + ui/src/api_tests/api.spec.ts | 188 ++++++++++++++++++++++++++++++ 11 files changed, 450 insertions(+), 79 deletions(-) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 17b52d2f5..961ef0c11 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -337,14 +337,14 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; - updated_comment.send_update(&user, &conn)?; - if let Some(deleted) = data.deleted.to_owned() { if deleted { updated_comment.send_delete(&user, &conn)?; } else { // TODO: undo delete } + } else { + updated_comment.send_update(&user, &conn)?; } let mut recipient_ids = Vec::new(); diff --git a/server/src/api/community.rs b/server/src/api/community.rs index d7f16c50c..a08424317 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -321,7 +321,8 @@ impl Perform for Oper { let conn = pool.get()?; // 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()); } @@ -381,7 +382,7 @@ impl Perform for Oper { if let Some(deleted) = data.deleted.to_owned() { if deleted { - updated_community.send_delete(&conn)?; + updated_community.send_delete(&user, &conn)?; } else { // TODO: undo delete } @@ -709,7 +710,7 @@ impl Perform for Oper { title: read_community.title, description: read_community.description, category_id: read_community.category_id, - creator_id: data.user_id, + creator_id: data.user_id, // This makes the new user the community creator removed: None, deleted: None, nsfw: read_community.nsfw, diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 56c133737..3d2df4631 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -541,14 +541,14 @@ impl Perform for Oper { ModStickyPost::create(&conn, &form)?; } - updated_post.send_update(&user, &conn)?; - if let Some(deleted) = data.deleted.to_owned() { if deleted { updated_post.send_delete(&user, &conn)?; } else { // TODO: undo delete } + } else { + updated_post.send_update(&user, &conn)?; } let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 14a099565..b30334772 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -35,11 +35,14 @@ impl ToApub for Comment { Ok(comment) } -} -impl ToTombstone for Comment { fn to_tombstone(&self) -> Result { - create_tombstone(self.deleted, &self.ap_id, self.published, self.updated, NoteType.to_string()) + create_tombstone( + self.deleted, + &self.ap_id, + self.updated, + NoteType.to_string(), + ) } } @@ -164,13 +167,23 @@ impl ApubObjectType for Comment { Ok(()) } - // TODO: this code is literally copied from post.rs - fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error> { + fn send_delete(&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!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let mut delete = Delete::default(); + + populate_object_props( + &mut delete.object_props, + &community.get_followers_url(), + &id, + )?; + delete .delete_props - .set_actor_xsd_any_uri(actor.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?; + .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 { @@ -181,12 +194,10 @@ impl ApubObjectType for Comment { }; activity::Activity::create(&conn, &activity_form)?; - let post = Post::read(conn, self.post_id)?; - let community = Community::read(conn, post.community_id)?; send_activity( &delete, - &actor.private_key.to_owned().unwrap(), - &actor.actor_id, + &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 0bfb95d2b..336aa24f0 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -46,11 +46,14 @@ impl ToApub for Community { Ok(group.extend(actor_props).extend(self.get_public_key_ext())) } -} -impl ToTombstone for Community { fn to_tombstone(&self) -> Result { - create_tombstone(self.deleted, &self.actor_id, self.published, self.updated, GroupType.to_string()) + create_tombstone( + self.deleted, + &self.actor_id, + self.updated, + GroupType.to_string(), + ) } } @@ -101,12 +104,17 @@ impl ActorType for Community { Ok(()) } - fn send_delete(&self, conn: &PgConnection) -> Result<(), Error> { + fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let group = self.to_apub(conn)?; + let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); + let mut delete = Delete::default(); + populate_object_props(&mut delete.object_props, &self.get_followers_url(), &id)?; + delete .delete_props - .set_actor_xsd_any_uri(self.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?; + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(group)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -117,10 +125,13 @@ impl ActorType for Community { }; 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( &delete, - &self.private_key.to_owned().unwrap(), - &self.actor_id, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, self.get_follower_inboxes(&conn)?, )?; Ok(()) diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 03c43bdc2..b56d6744f 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -9,6 +9,9 @@ pub mod signatures; pub mod user; pub mod user_inbox; +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, Update}, actor::{properties::ApActorProperties, Actor, Group, Person}, @@ -19,9 +22,6 @@ use activitystreams::{ object::{properties::ObjectProperties, Note, Page, Tombstone}, public, BaseBox, }; -use activitystreams::object::kind::{NoteType, PageType}; -use crate::api::community::CommunityResponse; -use crate::websocket::server::SendCommunityRoomMessage; use actix_web::body::Body; use actix_web::web::Path; use actix_web::{web, HttpRequest, HttpResponse, Result}; @@ -155,29 +155,29 @@ fn is_apub_id_valid(apub_id: &Url) -> bool { pub trait ToApub { type Response; fn to_apub(&self, conn: &PgConnection) -> Result; + fn to_tombstone(&self) -> Result; } fn create_tombstone( deleted: bool, object_id: &str, - published: NaiveDateTime, updated: Option, former_type: String, ) -> Result { if deleted { - let mut tombstone = Tombstone::default(); - // TODO: might want to include deleted time as well - tombstone - .object_props - .set_id(object_id)? - .set_published(convert_datetime(published))?; if let Some(updated) = updated { + let mut tombstone = Tombstone::default(); + tombstone.object_props.set_id(object_id)?; tombstone - .object_props - .set_updated(convert_datetime(updated))?; + .tombstone_props + .set_former_type_xsd_string(former_type)? + .set_deleted(convert_datetime(updated))?; + Ok(tombstone) + } else { + Err(format_err!( + "Cant convert to tombstone because updated time was None." + )) } - tombstone.tombstone_props.set_former_type_xsd_string(former_type)?; - Ok(tombstone) } else { Err(format_err!( "Cant convert object to tombstone if it wasnt deleted" @@ -185,10 +185,6 @@ fn create_tombstone( } } -pub trait ToTombstone { - fn to_tombstone(&self) -> Result; -} - pub trait FromApub { type ApubType; fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result @@ -199,7 +195,7 @@ pub trait FromApub { pub trait ApubObjectType { fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; } pub trait ApubLikeableType { @@ -238,7 +234,7 @@ pub trait ActorType { Err(format_err!("Accept not implemented.")) } - fn send_delete(&self, conn: &PgConnection) -> Result<(), Error>; + fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; // TODO default because there is no user following yet. #[allow(unused_variables)] diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 356141819..2c8bce722 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -57,11 +57,14 @@ impl ToApub for Post { Ok(page) } -} -impl ToTombstone for Post { fn to_tombstone(&self) -> Result { - create_tombstone(self.deleted, &self.ap_id, self.published, self.updated, PageType.to_string()) + create_tombstone( + self.deleted, + &self.ap_id, + self.updated, + PageType.to_string(), + ) } } @@ -174,12 +177,22 @@ impl ApubObjectType for Post { Ok(()) } - fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error> { + fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let page = self.to_apub(conn)?; + let community = Community::read(conn, self.community_id)?; + let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let mut delete = Delete::default(); + + populate_object_props( + &mut delete.object_props, + &community.get_followers_url(), + &id, + )?; + delete .delete_props - .set_actor_xsd_any_uri(actor.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?; + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(page)?; // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { @@ -193,8 +206,8 @@ impl ApubObjectType for Post { let community = Community::read(conn, self.community_id)?; send_activity( &delete, - &actor.private_key.to_owned().unwrap(), - &actor.actor_id, + &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 7d3826f22..ea03c9e6b 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -51,6 +51,9 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Dislike(d), Some("Page")) => { receive_dislike_post(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Delete(d), Some("Page")) => { + receive_delete_post(&d, &request, &conn, chat_server) + } (SharedAcceptedObjects::Create(c), Some("Note")) => { receive_create_comment(&c, &request, &conn, chat_server) } @@ -63,8 +66,11 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Dislike(d), Some("Note")) => { receive_dislike_comment(&d, &request, &conn, chat_server) } - (SharedAcceptedObjects::Delete(d), Some("Tombstone")) => { - receive_delete(&d, &request, &conn, chat_server) + (SharedAcceptedObjects::Delete(d), Some("Note")) => { + receive_delete_comment(&d, &request, &conn, chat_server) + } + (SharedAcceptedObjects::Delete(d), Some("Group")) => { + receive_delete_community(&d, &request, &conn, chat_server) } _ => Err(format_err!("Unknown incoming activity type.")), } @@ -508,58 +514,60 @@ fn receive_dislike_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_delete( +fn receive_delete_community( delete: &Delete, request: &HttpRequest, conn: &PgConnection, chat_server: ChatServerParam, ) -> Result { - let tombstone = delete + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let group = delete .delete_props .get_object_base_box() .to_owned() .unwrap() .to_owned() - .into_concrete::()?; - let former_type = tombstone.tombstone_props.get_former_type_xsd_string().unwrap().to_string(); - // TODO: handle these - match former_type.as_str() { - "Group" => {}, - d => return Err(format_err!("Delete type {} not supported", d)), - } - let community_apub_id = tombstone.object_props.get_id().unwrap().to_string(); + .into_concrete::()?; - let community = Community::read_from_actor_id(conn, &community_apub_id)?; - verify(request, &community.public_key.clone().unwrap())?; + 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: community.creator_id, + user_id: user.id, data: serde_json::to_value(&delete)?, 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: "".to_string(), - title: "".to_string(), - description: None, + 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: None, published: None, - updated: None, + updated: Some(naive_now()), deleted: Some(true), - nsfw: false, + nsfw: community.nsfw, actor_id: community.actor_id, - local: false, - private_key: None, + local: community.local, + private_key: community.private_key, public_key: community.public_key, - last_refreshed_at: Some(community.last_refreshed_at), + last_refreshed_at: None, }; - Community::update(conn, community.id, &community_form)?; + Community::update(&conn, community.id, &community_form)?; let res = CommunityResponse { community: CommunityView::read(&conn, community.id, None)?, @@ -574,3 +582,142 @@ fn receive_delete( Ok(HttpResponse::Ok().finish()) } + +fn receive_delete_post( + delete: &Delete, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let page = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + 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(&delete)?, + 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: None, + deleted: Some(true), + 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, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let note = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + 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(&delete)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let comment_ap_id = CommentForm::from_apub(¬e, &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: None, + deleted: Some(true), + 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()) +} diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index 7426efd5e..0d0bc8f2b 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -43,6 +43,9 @@ impl ToApub for User_ { Ok(person.extend(actor_props).extend(self.get_public_key_ext())) } + fn to_tombstone(&self) -> Result { + unimplemented!() + } } impl ActorType for User_ { @@ -88,7 +91,7 @@ impl ActorType for User_ { Ok(()) } - fn send_delete(&self, _conn: &PgConnection) -> Result<(), Error> { + fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { unimplemented!() } } diff --git a/server/src/db/community.rs b/server/src/db/community.rs index 301fce032..0f324de29 100644 --- a/server/src/db/community.rs +++ b/server/src/db/community.rs @@ -22,6 +22,7 @@ pub struct Community { pub last_refreshed_at: chrono::NaiveDateTime, } +// TODO add better delete, remove, lock actions here. #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)] #[table_name = "community"] pub struct CommunityForm { diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index ffc33888b..3e5546e5e 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -13,6 +13,9 @@ import { GetPostResponse, CommentForm, CommentResponse, + CommunityForm, + GetCommunityForm, + GetCommunityResponse, } from '../interfaces'; let lemmyAlphaUrl = 'http://localhost:8540'; @@ -324,6 +327,191 @@ describe('main', () => { expect(getPostRes.comments[1].creator_local).toBe(false); }); }); + + describe('delete community', () => { + test('/u/lemmy_beta deletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => { + // Create a test community + let communityName = 'test_community'; + 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 delete'; + 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 delete'; + 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 deletes the comment + let deleteCommentForm: CommentForm = { + content: commentContent, + edit_id: createCommentRes.comment.id, + post_id: createPostRes.post.id, + deleted: true, + auth: lemmyBetaAuth, + creator_id: createCommentRes.comment.creator_id, + }; + + let deleteCommentRes: CommentResponse = await fetch( + `${lemmyBetaApiUrl}/comment`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(deleteCommentForm), + } + ).then(d => d.json()); + expect(deleteCommentRes.comment.deleted).toBe(true); + + // lemmy_alpha sees that the comment is deleted + let getPostUrl = `${lemmyAlphaApiUrl}/post?id=3`; + let getPostRes: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostRes.comments[0].deleted).toBe(true); + + // lemmy_beta deletes the post + let deletePostForm: PostForm = { + name: postName, + edit_id: createPostRes.post.id, + auth: lemmyBetaAuth, + community_id: createPostRes.post.community_id, + creator_id: createPostRes.post.creator_id, + nsfw: false, + deleted: true, + }; + + let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(deletePostForm), + }).then(d => d.json()); + expect(deletePostRes.post.deleted).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.deleted).toBe(true); + + // lemmy_beta deletes the community + let deleteCommunityForm: CommunityForm = { + name: communityName, + title: communityName, + category_id: 1, + edit_id: createCommunityRes.community.id, + nsfw: false, + deleted: true, + auth: lemmyBetaAuth, + }; + + let deleteResponse: CommunityResponse = await fetch( + `${lemmyBetaApiUrl}/community`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(deleteCommunityForm), + } + ).then(d => d.json()); + + // Make sure the delete went through + expect(deleteResponse.community.deleted).toBe(true); + + // Re-get it from alpha, make sure its deleted 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.deleted).toBe(true); + }); + }); }); function wrapper(form: any): string { From 5366797a4b14634f1cfee183b8b8309efd457f46 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 1 May 2020 15:01:29 -0400 Subject: [PATCH 6/7] Add undos for delete community, post, and comment. --- server/src/api/comment.rs | 2 +- server/src/api/community.rs | 2 +- server/src/api/post.rs | 2 +- server/src/apub/comment.rs | 53 +++++++ server/src/apub/community.rs | 44 ++++++ server/src/apub/mod.rs | 5 +- server/src/apub/post.rs | 51 +++++++ server/src/apub/shared_inbox.rs | 243 ++++++++++++++++++++++++++++++++ server/src/apub/user.rs | 4 + ui/src/api_tests/api.spec.ts | 95 ++++++++++++- 10 files changed, 495 insertions(+), 6 deletions(-) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 961ef0c11..1ecedb2c8 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -341,7 +341,7 @@ impl Perform for Oper { if deleted { updated_comment.send_delete(&user, &conn)?; } else { - // TODO: undo delete + updated_comment.send_undo_delete(&user, &conn)?; } } else { updated_comment.send_update(&user, &conn)?; diff --git a/server/src/api/community.rs b/server/src/api/community.rs index a08424317..71da6712a 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -384,7 +384,7 @@ impl Perform for Oper { if deleted { updated_community.send_delete(&user, &conn)?; } else { - // TODO: undo delete + updated_community.send_undo_delete(&user, &conn)?; } } diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 3d2df4631..55e0612fe 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -545,7 +545,7 @@ impl Perform for Oper { if deleted { updated_post.send_delete(&user, &conn)?; } else { - // TODO: undo delete + updated_post.send_undo_delete(&user, &conn)?; } } else { updated_post.send_update(&user, &conn)?; diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index b30334772..65dd3c19a 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -202,6 +202,59 @@ impl ApubObjectType for Comment { )?; Ok(()) } + + fn send_undo_delete(&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)?; + + // Generate a fake delete activity, with the correct object + let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut delete = Delete::default(); + + populate_object_props( + &mut delete.object_props, + &community.get_followers_url(), + &id, + )?; + + delete + .delete_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(note)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/delete/{}", 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(delete)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.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(()) + } } impl ApubLikeableType for Comment { diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 336aa24f0..c4d9bf839 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -137,6 +137,50 @@ impl ActorType for Community { Ok(()) } + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let group = self.to_apub(conn)?; + let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); + + let mut delete = Delete::default(); + populate_object_props(&mut delete.object_props, &self.get_followers_url(), &id)?; + + delete + .delete_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(group)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/delete/{}", 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(creator.actor_id.to_owned())? + .set_object_base_box(delete)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_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 delete, the creator is the actor, and does the signing + send_activity( + &undo, + &creator.private_key.as_ref().unwrap(), + &creator.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, Error> { Ok( diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index b56d6744f..1d8605025 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -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, Update}, + activity::{Accept, Create, Delete, Dislike, Follow, Like, Undo, Update}, actor::{properties::ApActorProperties, Actor, Group, Person}, collection::UnorderedCollection, context, @@ -196,11 +196,13 @@ pub trait ApubObjectType { fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; 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>; } 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 } pub fn get_shared_inbox(actor_id: &str) -> String { @@ -235,6 +237,7 @@ pub trait ActorType { } fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; // TODO default because there is no user following yet. #[allow(unused_variables)] diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 2c8bce722..5a7383c0e 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -212,6 +212,57 @@ impl ApubObjectType for Post { )?; Ok(()) } + + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let page = self.to_apub(conn)?; + let community = Community::read(conn, self.community_id)?; + let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut delete = Delete::default(); + + populate_object_props( + &mut delete.object_props, + &community.get_followers_url(), + &id, + )?; + + delete + .delete_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(page)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/delete/{}", 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(delete)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_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, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } impl ApubLikeableType for Post { diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index ea03c9e6b..a9a610200 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -8,6 +8,7 @@ pub enum SharedAcceptedObjects { Like(Like), Dislike(Dislike), Delete(Delete), + Undo(Undo), } impl SharedAcceptedObjects { @@ -18,6 +19,7 @@ impl SharedAcceptedObjects { SharedAcceptedObjects::Like(l) => l.like_props.get_object_base_box(), 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(), } } } @@ -72,6 +74,9 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Delete(d), Some("Group")) => { receive_delete_community(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Undo(u), Some("Delete")) => { + receive_undo_delete(&u, &request, &conn, chat_server) + } _ => Err(format_err!("Unknown incoming activity type.")), } } @@ -721,3 +726,241 @@ fn receive_delete_comment( Ok(HttpResponse::Ok().finish()) } + +fn receive_undo_delete( + undo: &Undo, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let delete = undo + .undo_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let type_ = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .kind() + .unwrap(); + + match type_ { + "Note" => receive_undo_delete_comment(&delete, &request, &conn, chat_server), + "Page" => receive_undo_delete_post(&delete, &request, &conn, chat_server), + "Group" => receive_undo_delete_community(&delete, &request, &conn, chat_server), + d => Err(format_err!("Undo Delete type {} not supported", d)), + } +} + +fn receive_undo_delete_comment( + delete: &Delete, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let note = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + 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(&delete)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let comment_ap_id = CommentForm::from_apub(¬e, &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: None, + deleted: Some(false), + 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, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let page = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + 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(&delete)?, + 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: None, + deleted: Some(false), + 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, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let group = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + 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(&delete)?, + 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: None, + published: None, + updated: Some(naive_now()), + deleted: Some(false), + 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()) +} diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index 0d0bc8f2b..b5f47e251 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -94,6 +94,10 @@ impl ActorType for User_ { fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { unimplemented!() } + + fn send_undo_delete(&self, _creator: &User_, _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 3e5546e5e..e6f7bd864 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -328,8 +328,8 @@ describe('main', () => { }); }); - describe('delete community', () => { - test('/u/lemmy_beta deletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => { + describe('delete things', () => { + test('/u/lemmy_beta deletes and undeletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => { // Create a test community let communityName = 'test_community'; let communityForm: CommunityForm = { @@ -452,6 +452,34 @@ describe('main', () => { }).then(d => d.json()); expect(getPostRes.comments[0].deleted).toBe(true); + // lemmy_beta undeletes the comment + let undeleteCommentForm: CommentForm = { + content: commentContent, + edit_id: createCommentRes.comment.id, + post_id: createPostRes.post.id, + deleted: false, + auth: lemmyBetaAuth, + creator_id: createCommentRes.comment.creator_id, + }; + + let undeleteCommentRes: CommentResponse = await fetch( + `${lemmyBetaApiUrl}/comment`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(undeleteCommentForm), + } + ).then(d => d.json()); + expect(undeleteCommentRes.comment.deleted).toBe(false); + + // lemmy_alpha sees that the comment is undeleted + let getPostUndeleteRes: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostUndeleteRes.comments[0].deleted).toBe(false); + // lemmy_beta deletes the post let deletePostForm: PostForm = { name: postName, @@ -478,6 +506,35 @@ describe('main', () => { }).then(d => d.json()); expect(getPostResAgain.post.deleted).toBe(true); + // lemmy_beta undeletes the post + let undeletePostForm: PostForm = { + name: postName, + edit_id: createPostRes.post.id, + auth: lemmyBetaAuth, + community_id: createPostRes.post.community_id, + creator_id: createPostRes.post.creator_id, + nsfw: false, + deleted: false, + }; + + let undeletePostRes: PostResponse = await fetch( + `${lemmyBetaApiUrl}/post`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(undeletePostForm), + } + ).then(d => d.json()); + expect(undeletePostRes.post.deleted).toBe(false); + + // Make sure lemmy_alpha sees the post is undeleted + let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostResAgainTwo.post.deleted).toBe(false); + // lemmy_beta deletes the community let deleteCommunityForm: CommunityForm = { name: communityName, @@ -510,6 +567,40 @@ describe('main', () => { }).then(d => d.json()); expect(getCommunityRes.community.deleted).toBe(true); + + // lemmy_beta undeletes the community + let undeleteCommunityForm: CommunityForm = { + name: communityName, + title: communityName, + category_id: 1, + edit_id: createCommunityRes.community.id, + nsfw: false, + deleted: false, + auth: lemmyBetaAuth, + }; + + let undeleteCommunityRes: CommunityResponse = await fetch( + `${lemmyBetaApiUrl}/community`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(undeleteCommunityForm), + } + ).then(d => d.json()); + + // Make sure the delete went through + expect(undeleteCommunityRes.community.deleted).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.deleted).toBe(false); }); }); }); From 211ef795e900a632ed4794097b09fcb1d25d8600 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 3 May 2020 10:22:25 -0400 Subject: [PATCH 7/7] Some additional notes, reorg. --- server/src/apub/comment.rs | 1 + server/src/apub/community.rs | 2 +- server/src/apub/mod.rs | 3 ++- server/src/apub/post.rs | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 65dd3c19a..823b9d6ae 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -223,6 +223,7 @@ impl ApubObjectType for Comment { .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/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let mut undo = Undo::default(); diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index c4d9bf839..3a1dccb27 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -1,5 +1,4 @@ use super::*; -use activitystreams::actor::kind::GroupType; #[derive(Deserialize)] pub struct CommunityQuery { @@ -149,6 +148,7 @@ impl ActorType for Community { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(group)?; + // TODO // Undo that fake activity let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let mut undo = Undo::default(); diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 1d8605025..5b65411a3 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -14,7 +14,7 @@ use crate::websocket::server::SendCommunityRoomMessage; use activitystreams::object::kind::{NoteType, PageType}; use activitystreams::{ activity::{Accept, Create, Delete, Dislike, Follow, Like, Undo, Update}, - actor::{properties::ApActorProperties, Actor, Group, Person}, + actor::{kind::GroupType, properties::ApActorProperties, Actor, Group, Person}, collection::UnorderedCollection, context, endpoint::EndpointProperties, @@ -158,6 +158,7 @@ pub trait ToApub { fn to_tombstone(&self) -> Result; } +/// Updated is actually the deletion time fn create_tombstone( deleted: bool, object_id: &str, diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 5a7383c0e..c01283fcc 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -230,6 +230,7 @@ impl ApubObjectType for Post { .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/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let mut undo = Undo::default();