]> Untitled Git - lemmy.git/commitdiff
Federate user account deletion (fixes #1284) (#2199)
authorNutomic <me@nutomic.com>
Thu, 7 Apr 2022 20:52:17 +0000 (20:52 +0000)
committerGitHub <noreply@github.com>
Thu, 7 Apr 2022 20:52:17 +0000 (20:52 +0000)
12 files changed:
api_tests/src/shared.ts
api_tests/src/user.spec.ts
crates/api/src/site.rs
crates/api_common/src/lib.rs
crates/api_crud/src/site/read.rs
crates/api_crud/src/user/delete.rs
crates/apub/assets/lemmy/activities/deletion/delete_user.json [new file with mode: 0644]
crates/apub/src/activities/deletion/delete_user.rs [new file with mode: 0644]
crates/apub/src/activities/deletion/mod.rs
crates/apub/src/activity_lists.rs
crates/apub/src/protocol/activities/deletion/delete_user.rs [new file with mode: 0644]
crates/apub/src/protocol/activities/deletion/mod.rs

index 4f305237ec908339866f7aed0ea9a147f211f013..c429a207a2b75bcf1bc30846d364eb78b1649f78 100644 (file)
@@ -58,6 +58,7 @@ import {
   CommentReportResponse,
   ListCommentReports,
   ListCommentReportsResponse,
+  DeleteAccount,
 } from 'lemmy-js-client';
 
 export interface API {
@@ -549,6 +550,16 @@ export async function saveUserSettings(
   return api.client.saveUserSettings(form);
 }
 
+export async function deleteUser(
+  api: API,
+): Promise<LoginResponse> {
+  let form: DeleteAccount = {
+    auth: api.auth,
+    password
+  };
+  return api.client.deleteAccount(form);
+}
+
 export async function getSite(
   api: API
 ): Promise<GetSiteResponse> {
index 788987e2c0bbb271346123bc827232c012f91571..29029ac1e9d231ad212e1bcb10f08024b5e50264 100644 (file)
@@ -6,6 +6,15 @@ import {
   resolvePerson,
   saveUserSettings,
   getSite,
+  createPost,
+  gamma,
+  resolveCommunity,
+  createComment,
+  resolveBetaCommunity,
+  deleteUser,
+  resolvePost,
+  API,
+  resolveComment,
 } from './shared';
 import {
   PersonViewSafe,
@@ -60,3 +69,33 @@ test('Set some user settings, check that they are federated', async () => {
   let betaPerson = (await resolvePerson(beta, apShortname)).person;
   assertUserFederation(alphaPerson, betaPerson);
 });
+
+test('Delete user', async () => {
+  let userRes = await registerUser(alpha);
+  expect(userRes.jwt).toBeDefined();
+  let user: API = {
+    client: alpha.client,
+    auth: userRes.jwt
+  }
+
+  // make a local post and comment
+  let alphaCommunity = (await resolveCommunity(user, '!main@lemmy-alpha:8541')).community;
+  let localPost = (await createPost(user, alphaCommunity.community.id)).post_view.post;
+  expect(localPost).toBeDefined();
+  let localComment = (await createComment(user, localPost.id)).comment_view.comment;
+  expect(localComment).toBeDefined();
+
+  // make a remote post and comment
+  let betaCommunity = (await resolveBetaCommunity(user)).community;
+  let remotePost = (await createPost(user, betaCommunity.community.id)).post_view.post;
+  expect(remotePost).toBeDefined();
+  let remoteComment = (await createComment(user, remotePost.id)).comment_view.comment;
+  expect(remoteComment).toBeDefined();
+
+  await deleteUser(user);
+
+  expect((await resolvePost(alpha, localPost)).post).toBeUndefined();
+  expect((await resolveComment(alpha, localComment)).comment).toBeUndefined();
+  expect((await resolvePost(alpha, remotePost)).post).toBeUndefined();
+  expect((await resolveComment(alpha, remoteComment)).comment).toBeUndefined();
+});
index 677576e1394a04603c55674c6fb84374fad23b7a..54b7e0c73fefacffac2e44603b25abdf998e5e4b 100644 (file)
@@ -508,12 +508,8 @@ impl Perform for LeaveAdmin {
     let site_view = blocking(context.pool(), SiteView::read_local).await??;
     let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
 
-    let federated_instances = build_federated_instances(
-      context.pool(),
-      &context.settings().federation,
-      &context.settings().hostname,
-    )
-    .await?;
+    let federated_instances =
+      build_federated_instances(context.pool(), &context.settings()).await?;
 
     Ok(GetSiteResponse {
       site_view: Some(site_view),
index 0d6789e7b0bad91b3fb50e20ae2cd7ea06bc9955..c37d40e9aa0d5668846f7af897aabfe6c96f28f1 100644 (file)
@@ -13,6 +13,7 @@ use lemmy_db_schema::{
     community::Community,
     email_verification::{EmailVerification, EmailVerificationForm},
     password_reset_request::PasswordResetRequest,
+    person::Person,
     person_block::PersonBlock,
     post::{Post, PostRead, PostReadForm},
     registration_application::RegistrationApplication,
@@ -34,7 +35,7 @@ use lemmy_db_views_actor::{
 use lemmy_utils::{
   claims::Claims,
   email::{send_email, translations::Lang},
-  settings::structs::{FederationConfig, Settings},
+  settings::structs::Settings,
   utils::generate_random_string,
   LemmyError,
   Sensitive,
@@ -295,9 +296,10 @@ pub async fn check_private_instance(
 #[tracing::instrument(skip_all)]
 pub async fn build_federated_instances(
   pool: &DbPool,
-  federation_config: &FederationConfig,
-  hostname: &str,
+  settings: &Settings,
 ) -> Result<Option<FederatedInstances>, LemmyError> {
+  let federation_config = &settings.federation;
+  let hostname = &settings.hostname;
   let federation = federation_config.to_owned();
   if federation.enabled {
     let distinct_communities = blocking(pool, move |conn| {
@@ -579,6 +581,24 @@ pub async fn remove_user_data_in_community(
   Ok(())
 }
 
+pub async fn delete_user_account(person_id: PersonId, pool: &DbPool) -> Result<(), LemmyError> {
+  // Comments
+  let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id);
+  blocking(pool, permadelete)
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
+
+  // Posts
+  let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id);
+  blocking(pool, permadelete)
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?;
+
+  blocking(pool, move |conn| Person::delete_account(conn, person_id)).await??;
+
+  Ok(())
+}
+
 pub fn check_image_has_local_domain(url: &Option<DbUrl>) -> Result<(), LemmyError> {
   if let Some(url) = url {
     let settings = Settings::get();
index 2e8269de8b58149b310c5ca28e24f92f530f51a7..9131c3ecfa9bb99fda3a7b290978aef65db48c12 100644 (file)
@@ -134,12 +134,8 @@ impl PerformCrud for GetSite {
       None
     };
 
-    let federated_instances = build_federated_instances(
-      context.pool(),
-      &context.settings().federation,
-      &context.settings().hostname,
-    )
-    .await?;
+    let federated_instances =
+      build_federated_instances(context.pool(), &context.settings()).await?;
 
     Ok(GetSiteResponse {
       site_view,
index ae92c12e32ad9bf9bda1bc4acdf84de4fdce8048..ea1cbcff5fca744b6e1b208ed47764348c976798 100644 (file)
@@ -1,8 +1,8 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
 use bcrypt::verify;
-use lemmy_api_common::{blocking, get_local_user_view_from_jwt, person::*};
-use lemmy_db_schema::source::{comment::Comment, person::Person, post::Post};
+use lemmy_api_common::{delete_user_account, get_local_user_view_from_jwt, person::*};
+use lemmy_apub::protocol::activities::deletion::delete_user::DeleteUser;
 use lemmy_utils::{ConnectionId, LemmyError};
 use lemmy_websocket::LemmyContext;
 
@@ -30,23 +30,8 @@ impl PerformCrud for DeleteAccount {
       return Err(LemmyError::from_message("password_incorrect"));
     }
 
-    // Comments
-    let person_id = local_user_view.person.id;
-    let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id);
-    blocking(context.pool(), permadelete)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
-
-    // Posts
-    let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id);
-    blocking(context.pool(), permadelete)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?;
-
-    blocking(context.pool(), move |conn| {
-      Person::delete_account(conn, person_id)
-    })
-    .await??;
+    delete_user_account(local_user_view.person.id, context.pool()).await?;
+    DeleteUser::send(&local_user_view.person.into(), context).await?;
 
     Ok(DeleteAccountResponse {})
   }
diff --git a/crates/apub/assets/lemmy/activities/deletion/delete_user.json b/crates/apub/assets/lemmy/activities/deletion/delete_user.json
new file mode 100644 (file)
index 0000000..1e000c1
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "actor": "http://ds9.lemmy.ml/u/lemmy_alpha",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "object": "http://ds9.lemmy.ml/u/lemmy_alpha",
+  "cc": [
+    "http://enterprise.lemmy.ml/c/main"
+  ],
+  "type": "Delete",
+  "id": "http://ds9.lemmy.ml/activities/delete/f2abee48-c7bb-41d5-9e27-8775ff32db12"
+}
\ No newline at end of file
diff --git a/crates/apub/src/activities/deletion/delete_user.rs b/crates/apub/src/activities/deletion/delete_user.rs
new file mode 100644 (file)
index 0000000..0cd2c50
--- /dev/null
@@ -0,0 +1,74 @@
+use crate::{
+  activities::{generate_activity_id, send_lemmy_activity, verify_is_public, verify_person},
+  objects::person::ApubPerson,
+  protocol::activities::deletion::delete_user::DeleteUser,
+};
+use activitystreams_kinds::{activity::DeleteType, public};
+use lemmy_api_common::{blocking, delete_user_account};
+use lemmy_apub_lib::{
+  data::Data,
+  object_id::ObjectId,
+  traits::ActivityHandler,
+  verify::verify_urls_match,
+};
+use lemmy_db_schema::source::site::Site;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+
+/// This can be separate from Delete activity because it doesn't need to be handled in shared inbox
+/// (cause instance actor doesn't have shared inbox).
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for DeleteUser {
+  type DataType = LemmyContext;
+
+  async fn verify(
+    &self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_is_public(&self.to, &[])?;
+    verify_person(&self.actor, context, request_counter).await?;
+    verify_urls_match(self.actor.inner(), self.object.inner())?;
+    Ok(())
+  }
+
+  async fn receive(
+    self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let actor = self
+      .actor
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    delete_user_account(actor.id, context.pool()).await?;
+    Ok(())
+  }
+}
+
+impl DeleteUser {
+  #[tracing::instrument(skip_all)]
+  pub async fn send(actor: &ApubPerson, context: &LemmyContext) -> Result<(), LemmyError> {
+    let actor_id = ObjectId::new(actor.actor_id.clone());
+    let id = generate_activity_id(
+      DeleteType::Delete,
+      &context.settings().get_protocol_and_hostname(),
+    )?;
+    let delete = DeleteUser {
+      actor: actor_id.clone(),
+      to: vec![public()],
+      object: actor_id,
+      kind: DeleteType::Delete,
+      id: id.clone(),
+      cc: vec![],
+    };
+
+    let remote_sites = blocking(context.pool(), Site::read_remote_sites).await??;
+    let inboxes = remote_sites
+      .into_iter()
+      .map(|s| s.inbox_url.into())
+      .collect();
+    send_lemmy_activity(context, &delete, &id, actor, inboxes, true).await?;
+    Ok(())
+  }
+}
index f0c3a541fb99d9f1c186def27391ee1d22b1a072..1ff8429aa9a74a7ef7ad16b711a3f7466afe8172 100644 (file)
@@ -49,6 +49,7 @@ use std::ops::Deref;
 use url::Url;
 
 pub mod delete;
+pub mod delete_user;
 pub mod undo_delete;
 
 /// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this
index 362d29afb9224015eb068f5bb96eae8879da0af8..80d37fc63c8d3e6349caf650ffa3a439136dfb70 100644 (file)
@@ -16,7 +16,7 @@ use crate::{
         post::CreateOrUpdatePost,
         private_message::CreateOrUpdatePrivateMessage,
       },
-      deletion::{delete::Delete, undo_delete::UndoDelete},
+      deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete},
       following::{
         accept::AcceptFollowCommunity,
         follow::FollowCommunity,
@@ -87,9 +87,11 @@ pub enum AnnouncableActivities {
 #[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
 #[serde(untagged)]
 #[activity_handler(LemmyContext)]
+#[allow(clippy::enum_variant_names)]
 pub enum SiteInboxActivities {
   BlockUser(BlockUser),
   UndoBlockUser(UndoBlockUser),
+  DeleteUser(DeleteUser),
 }
 
 #[async_trait::async_trait(?Send)]
diff --git a/crates/apub/src/protocol/activities/deletion/delete_user.rs b/crates/apub/src/protocol/activities/deletion/delete_user.rs
new file mode 100644 (file)
index 0000000..22d215e
--- /dev/null
@@ -0,0 +1,24 @@
+use crate::objects::person::ApubPerson;
+use activitystreams_kinds::activity::DeleteType;
+use lemmy_apub_lib::object_id::ObjectId;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use url::Url;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DeleteUser {
+  pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
+  pub(crate) to: Vec<Url>,
+  pub(crate) object: ObjectId<ApubPerson>,
+  #[serde(rename = "type")]
+  pub(crate) kind: DeleteType,
+  pub(crate) id: Url,
+
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
+  #[serde(default)]
+  #[serde(skip_serializing_if = "Vec::is_empty")]
+  pub(crate) cc: Vec<Url>,
+}
index 24f7ab16e93e0cd4e6d78ffa833c72abf937358f..fe22c001011b03d3a459d0827528ae9ed86cdebe 100644 (file)
@@ -1,10 +1,11 @@
 pub mod delete;
+pub mod delete_user;
 pub mod undo_delete;
 
 #[cfg(test)]
 mod tests {
   use crate::protocol::{
-    activities::deletion::{delete::Delete, undo_delete::UndoDelete},
+    activities::deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete},
     tests::test_parse_lemmy_item,
   };
 
@@ -23,5 +24,8 @@ mod tests {
       "assets/lemmy/activities/deletion/undo_delete_private_message.json",
     )
     .unwrap();
+
+    test_parse_lemmy_item::<DeleteUser>("assets/lemmy/activities/deletion/delete_user.json")
+      .unwrap();
   }
 }