diff options
-rw-r--r-- | server/Cargo.lock | 8 | ||||
-rw-r--r-- | server/Cargo.toml | 2 | ||||
-rw-r--r-- | server/src/api/comment.rs | 16 | ||||
-rw-r--r-- | server/src/api/community.rs | 21 | ||||
-rw-r--r-- | server/src/api/post.rs | 16 | ||||
-rw-r--r-- | server/src/apub/comment.rs | 187 | ||||
-rw-r--r-- | server/src/apub/community.rs | 194 | ||||
-rw-r--r-- | server/src/apub/fetcher.rs | 39 | ||||
-rw-r--r-- | server/src/apub/mod.rs | 65 | ||||
-rw-r--r-- | server/src/apub/post.rs | 188 | ||||
-rw-r--r-- | server/src/apub/shared_inbox.rs | 946 | ||||
-rw-r--r-- | server/src/apub/user.rs | 19 | ||||
-rw-r--r-- | server/src/db/community.rs | 1 | ||||
-rw-r--r-- | ui/src/api_tests/api.spec.ts | 555 |
14 files changed, 2212 insertions, 45 deletions
diff --git a/server/Cargo.lock b/server/Cargo.lock index 56e81b47..714422b6 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 2777dd2c..ab76d06f 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/api/comment.rs b/server/src/api/comment.rs index a6742e4c..2853beb3 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -337,7 +337,21 @@ impl Perform for Oper<EditComment> { 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 { + 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)?; + } let mut recipient_ids = Vec::new(); diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 296a77ea..9659469b 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -321,7 +321,8 @@ impl Perform for Oper<EditCommunity> { 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()); } @@ -358,7 +359,7 @@ impl Perform for Oper<EditCommunity> { 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()), }; @@ -379,6 +380,20 @@ impl Perform for Oper<EditCommunity> { ModRemoveCommunity::create(&conn, &form)?; } + if let Some(deleted) = data.deleted.to_owned() { + if deleted { + updated_community.send_delete(&user, &conn)?; + } 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))?; let res = CommunityResponse { @@ -701,7 +716,7 @@ impl Perform for Oper<TransferCommunity> { 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 306365fa..b9c4c083 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -541,7 +541,21 @@ impl Perform for Oper<EditPost> { 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 { + 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)?; + } 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 6cede17b..87258275 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -35,6 +35,15 @@ impl ToApub for Comment { Ok(comment) } + + fn to_tombstone(&self) -> Result<Tombstone, Error> { + create_tombstone( + self.deleted, + &self.ap_id, + self.updated, + NoteType.to_string(), + ) + } } impl FromApub for CommentForm { @@ -157,6 +166,184 @@ impl ApubObjectType for Comment { )?; Ok(()) } + + 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(creator.actor_id.to_owned())? + .set_object_base_box(note)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: creator.id, + data: serde_json::to_value(&delete)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &delete, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + 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: 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(()) + } + + 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 { diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 46bd9024..3510fbff 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 { @@ -13,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))?; @@ -45,6 +54,15 @@ impl ToApub for Community { Ok(group.extend(actor_props).extend(self.get_public_key_ext())) } + + fn to_tombstone(&self) -> Result<Tombstone, Error> { + create_tombstone( + self.deleted, + &self.actor_id, + self.updated, + GroupType.to_string(), + ) + } } impl ActorType for Community { @@ -94,10 +112,162 @@ impl ActorType for Community { Ok(()) } + 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(creator.actor_id.to_owned())? + .set_object_base_box(group)?; + + // 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)?; + + // 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, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + self.get_follower_inboxes(&conn)?, + )?; + 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(()) + } + + 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> { - debug!("got here."); - Ok( CommunityFollowerView::for_community(conn, self.id)? .into_iter() @@ -135,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(), @@ -170,8 +343,13 @@ pub async fn get_apub_community_http( db: DbPoolParam, ) -> Result<HttpResponse<Body>, 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/fetcher.rs b/server/src/apub/fetcher.rs index 8e82a2f7..8a4b8855 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -111,10 +111,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)?)?; @@ -143,10 +143,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)?)?; @@ -161,7 +161,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)), } diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 4b08c53a..3c18a013 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -9,14 +9,17 @@ 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, Dislike, Follow, Like, Update}, + activity::{Accept, Create, Delete, Dislike, Follow, Like, Remove, Undo, 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; @@ -44,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::{ @@ -60,6 +66,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}; @@ -87,6 +94,15 @@ where .json(data) } +fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body> +where + T: Serialize, +{ + HttpResponse::Gone() + .content_type(APUB_JSON_CONTENT_TYPE) + .json(data) +} + /// Generates the ActivityPub ID for a given object type and name. /// /// TODO: we will probably need to change apub endpoint urls so that html and activity+json content @@ -142,6 +158,34 @@ fn is_apub_id_valid(apub_id: &Url) -> bool { pub trait ToApub { type Response; fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>; + fn to_tombstone(&self) -> Result<Tombstone, Error>; +} + +fn create_tombstone( + deleted: bool, + object_id: &str, + updated: Option<NaiveDateTime>, + former_type: String, +) -> Result<Tombstone, Error> { + if deleted { + if let Some(updated) = updated { + let mut tombstone = Tombstone::default(); + tombstone.object_props.set_id(object_id)?; + tombstone + .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." + )) + } + } else { + Err(format_err!( + "Cant convert object to tombstone if it wasnt deleted" + )) + } } pub trait FromApub { @@ -154,11 +198,16 @@ 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_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 { 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 { @@ -192,6 +241,12 @@ pub trait ActorType { Err(format_err!("Accept not implemented.")) } + 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. diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index af8ee599..2d1f1c71 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -12,7 +12,11 @@ pub async fn get_apub_post( ) -> Result<HttpResponse<Body>, Error> { let id = info.post_id.parse::<i32>()?; 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 { @@ -53,6 +57,15 @@ impl ToApub for Post { Ok(page) } + + fn to_tombstone(&self) -> Result<Tombstone, Error> { + create_tombstone( + self.deleted, + &self.ap_id, + self.updated, + PageType.to_string(), + ) + } } impl FromApub for PostForm { @@ -163,6 +176,179 @@ impl ApubObjectType for Post { )?; Ok(()) } + + 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(creator.actor_id.to_owned())? + .set_object_base_box(page)?; + + // 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, + &creator.private_key.as_ref().unwrap(), + &creator.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + 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(()) + } + + 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(); + + p |