Make remove content optional during account deletion (fixes #1617) (#3817)

* Make remove content optional during account deletion (fixes #1617)

* simplify purge params by passing context

* update js client

* add delete content

* update woodpecker config

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Nutomic 2023-08-28 12:23:45 +02:00 committed by GitHub
parent 26d125cc63
commit 7fd14b3d2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 71 additions and 119 deletions

View File

@ -10,9 +10,9 @@ variables:
- "**/Cargo.toml" - "**/Cargo.toml"
- "Cargo.lock" - "Cargo.lock"
# database migrations # database migrations
- "migrations" - "migrations/**"
# typescript tests # typescript tests
- "api_tests" - "api_tests/**"
# config files and scripts used by ci # config files and scripts used by ci
- ".woodpecker.yml" - ".woodpecker.yml"
- ".rustfmt.toml" - ".rustfmt.toml"

View File

@ -19,7 +19,7 @@
"eslint": "^8.40.0", "eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.18.3-rc.3", "lemmy-js-client": "0.19.0-rc.2",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.0.4" "typescript": "^5.0.4"

View File

@ -707,6 +707,7 @@ export async function getPersonDetails(
export async function deleteUser(api: API): Promise<DeleteAccountResponse> { export async function deleteUser(api: API): Promise<DeleteAccountResponse> {
let form: DeleteAccount = { let form: DeleteAccount = {
auth: api.auth, auth: api.auth,
delete_content: true,
password, password,
}; };
return api.client.deleteAccount(form); return api.client.deleteAccount(form);

View File

@ -2174,10 +2174,10 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
lemmy-js-client@0.18.3-rc.3: lemmy-js-client@0.19.0-rc.2:
version "0.18.3-rc.3" version "0.19.0-rc.2"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.3-rc.3.tgz#fc6489eb141bd09558bca38d9e46b40771a29f37" resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-rc.2.tgz#c3cb511b27f92538909a2b91a0f8527b1abad958"
integrity sha512-njixgXk4uMU4gGifnljwhSe9Kf445C4wAXcXhtpTtwPPLXpHQgxA1RASMb9Uq4zblfE6nC2JbrAka8y8N2N/Bw== integrity sha512-FXuf8s7bpBVkHL/OGWDb/0aGIrJ7uv3d4Xt1h6zmNDhw6MmmuD8RXgCHiS2jqhxjAEp96Dpl1NFXbpmKpix7tQ==
dependencies: dependencies:
cross-fetch "^3.1.5" cross-fetch "^3.1.5"
form-data "^4.0.0" form-data "^4.0.0"

View File

@ -47,13 +47,7 @@ pub async fn ban_from_site(
// Remove their data if that's desired // Remove their data if that's desired
let remove_data = data.remove_data.unwrap_or(false); let remove_data = data.remove_data.unwrap_or(false);
if remove_data { if remove_data {
remove_user_data( remove_user_data(person.id, &context).await?;
person.id,
&mut context.pool(),
context.settings(),
context.client(),
)
.await?;
} }
// Mod tables // Mod tables

View File

@ -33,24 +33,14 @@ impl Perform for PurgeCommunity {
let community = Community::read(&mut context.pool(), community_id).await?; let community = Community::read(&mut context.pool(), community_id).await?;
if let Some(banner) = community.banner { if let Some(banner) = community.banner {
purge_image_from_pictrs(context.client(), context.settings(), &banner) purge_image_from_pictrs(&banner, context).await.ok();
.await
.ok();
} }
if let Some(icon) = community.icon { if let Some(icon) = community.icon {
purge_image_from_pictrs(context.client(), context.settings(), &icon) purge_image_from_pictrs(&icon, context).await.ok();
.await
.ok();
} }
purge_image_posts_for_community( purge_image_posts_for_community(community_id, context).await?;
community_id,
&mut context.pool(),
context.settings(),
context.client(),
)
.await?;
Community::delete(&mut context.pool(), community_id).await?; Community::delete(&mut context.pool(), community_id).await?;

View File

@ -32,24 +32,14 @@ impl Perform for PurgePerson {
let person = Person::read(&mut context.pool(), person_id).await?; let person = Person::read(&mut context.pool(), person_id).await?;
if let Some(banner) = person.banner { if let Some(banner) = person.banner {
purge_image_from_pictrs(context.client(), context.settings(), &banner) purge_image_from_pictrs(&banner, context).await.ok();
.await
.ok();
} }
if let Some(avatar) = person.avatar { if let Some(avatar) = person.avatar {
purge_image_from_pictrs(context.client(), context.settings(), &avatar) purge_image_from_pictrs(&avatar, context).await.ok();
.await
.ok();
} }
purge_image_posts_for_person( purge_image_posts_for_person(person_id, context).await?;
person_id,
&mut context.pool(),
context.settings(),
context.client(),
)
.await?;
Person::delete(&mut context.pool(), person_id).await?; Person::delete(&mut context.pool(), person_id).await?;

View File

@ -34,15 +34,11 @@ impl Perform for PurgePost {
// Purge image // Purge image
if let Some(url) = post.url { if let Some(url) = post.url {
purge_image_from_pictrs(context.client(), context.settings(), &url) purge_image_from_pictrs(&url, context).await.ok();
.await
.ok();
} }
// Purge thumbnail // Purge thumbnail
if let Some(thumbnail_url) = post.thumbnail_url { if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(context.client(), context.settings(), &thumbnail_url) purge_image_from_pictrs(&thumbnail_url, context).await.ok();
.await
.ok();
} }
let community_id = post.community_id; let community_id = post.community_id;

View File

@ -365,6 +365,7 @@ pub struct CommentReplyResponse {
/// Delete your account. /// Delete your account.
pub struct DeleteAccount { pub struct DeleteAccount {
pub password: Sensitive<String>, pub password: Sensitive<String>,
pub delete_content: bool,
pub auth: Sensitive<String>, pub auth: Sensitive<String>,
} }

View File

@ -1,4 +1,4 @@
use crate::post::SiteMetadata; use crate::{context::LemmyContext, post::SiteMetadata};
use encoding::{all::encodings, DecoderTrap}; use encoding::{all::encodings, DecoderTrap};
use lemmy_db_schema::newtypes::DbUrl; use lemmy_db_schema::newtypes::DbUrl;
use lemmy_utils::{ use lemmy_utils::{
@ -150,12 +150,11 @@ pub(crate) async fn fetch_pictrs(
/// - It might not be an image /// - It might not be an image
/// - Pictrs might not be set up /// - Pictrs might not be set up
pub async fn purge_image_from_pictrs( pub async fn purge_image_from_pictrs(
client: &ClientWithMiddleware,
settings: &Settings,
image_url: &Url, image_url: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let pictrs_config = settings.pictrs_config()?; let pictrs_config = context.settings().pictrs_config()?;
is_image_content_type(client, image_url).await?; is_image_content_type(context.client(), image_url).await?;
let alias = image_url let alias = image_url
.path_segments() .path_segments()
@ -168,7 +167,8 @@ pub async fn purge_image_from_pictrs(
let pictrs_api_key = pictrs_config let pictrs_api_key = pictrs_config
.api_key .api_key
.ok_or(LemmyErrorType::PictrsApiKeyNotProvided)?; .ok_or(LemmyErrorType::PictrsApiKeyNotProvided)?;
let response = client let response = context
.client()
.post(&purge_url) .post(&purge_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_api_key) .header("x-api-token", pictrs_api_key)

View File

@ -58,7 +58,7 @@ pub enum SendActivityData {
CreatePrivateMessage(PrivateMessageView), CreatePrivateMessage(PrivateMessageView),
UpdatePrivateMessage(PrivateMessageView), UpdatePrivateMessage(PrivateMessageView),
DeletePrivateMessage(Person, PrivateMessage, bool), DeletePrivateMessage(Person, PrivateMessage, bool),
DeleteUser(Person), DeleteUser(Person, bool),
CreateReport(Url, Person, Community, String), CreateReport(Url, Person, Community, String),
} }

View File

@ -42,7 +42,6 @@ use lemmy_utils::{
utils::slurs::build_slur_regex, utils::slurs::build_slur_regex,
}; };
use regex::Regex; use regex::Regex;
use reqwest_middleware::ClientWithMiddleware;
use rosetta_i18n::{Language, LanguageId}; use rosetta_i18n::{Language, LanguageId};
use tracing::warn; use tracing::warn;
use url::{ParseError, Url}; use url::{ParseError, Url};
@ -529,19 +528,16 @@ pub fn check_private_instance_and_federation_enabled(
pub async fn purge_image_posts_for_person( pub async fn purge_image_posts_for_person(
banned_person_id: PersonId, banned_person_id: PersonId,
pool: &mut DbPool<'_>, context: &LemmyContext,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let pool = &mut context.pool();
let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?; let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?;
for post in posts { for post in posts {
if let Some(url) = post.url { if let Some(url) = post.url {
purge_image_from_pictrs(client, settings, &url).await.ok(); purge_image_from_pictrs(&url, context).await.ok();
} }
if let Some(thumbnail_url) = post.thumbnail_url { if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(client, settings, &thumbnail_url) purge_image_from_pictrs(&thumbnail_url, context).await.ok();
.await
.ok();
} }
} }
@ -552,19 +548,16 @@ pub async fn purge_image_posts_for_person(
pub async fn purge_image_posts_for_community( pub async fn purge_image_posts_for_community(
banned_community_id: CommunityId, banned_community_id: CommunityId,
pool: &mut DbPool<'_>, context: &LemmyContext,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let pool = &mut context.pool();
let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?; let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?;
for post in posts { for post in posts {
if let Some(url) = post.url { if let Some(url) = post.url {
purge_image_from_pictrs(client, settings, &url).await.ok(); purge_image_from_pictrs(&url, context).await.ok();
} }
if let Some(thumbnail_url) = post.thumbnail_url { if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(client, settings, &thumbnail_url) purge_image_from_pictrs(&thumbnail_url, context).await.ok();
.await
.ok();
} }
} }
@ -575,21 +568,16 @@ pub async fn purge_image_posts_for_community(
pub async fn remove_user_data( pub async fn remove_user_data(
banned_person_id: PersonId, banned_person_id: PersonId,
pool: &mut DbPool<'_>, context: &LemmyContext,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let pool = &mut context.pool();
// Purge user images // Purge user images
let person = Person::read(pool, banned_person_id).await?; let person = Person::read(pool, banned_person_id).await?;
if let Some(avatar) = person.avatar { if let Some(avatar) = person.avatar {
purge_image_from_pictrs(client, settings, &avatar) purge_image_from_pictrs(&avatar, context).await.ok();
.await
.ok();
} }
if let Some(banner) = person.banner { if let Some(banner) = person.banner {
purge_image_from_pictrs(client, settings, &banner) purge_image_from_pictrs(&banner, context).await.ok();
.await
.ok();
} }
// Update the fields to None // Update the fields to None
@ -608,7 +596,7 @@ pub async fn remove_user_data(
Post::update_removed_for_creator(pool, banned_person_id, None, true).await?; Post::update_removed_for_creator(pool, banned_person_id, None, true).await?;
// Purge image posts // Purge image posts
purge_image_posts_for_person(banned_person_id, pool, settings, client).await?; purge_image_posts_for_person(banned_person_id, context).await?;
// Communities // Communities
// Remove all communities where they're the top mod // Remove all communities where they're the top mod
@ -635,12 +623,10 @@ pub async fn remove_user_data(
// Delete the community images // Delete the community images
if let Some(icon) = first_mod_community.community.icon { if let Some(icon) = first_mod_community.community.icon {
purge_image_from_pictrs(client, settings, &icon).await.ok(); purge_image_from_pictrs(&icon, context).await.ok();
} }
if let Some(banner) = first_mod_community.community.banner { if let Some(banner) = first_mod_community.community.banner {
purge_image_from_pictrs(client, settings, &banner) purge_image_from_pictrs(&banner, context).await.ok();
.await
.ok();
} }
// Update the fields to None // Update the fields to None
Community::update( Community::update(
@ -695,23 +681,18 @@ pub async fn remove_user_data_in_community(
Ok(()) Ok(())
} }
pub async fn delete_user_account( pub async fn purge_user_account(
person_id: PersonId, person_id: PersonId,
pool: &mut DbPool<'_>, context: &LemmyContext,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let pool = &mut context.pool();
// Delete their images // Delete their images
let person = Person::read(pool, person_id).await?; let person = Person::read(pool, person_id).await?;
if let Some(avatar) = person.avatar { if let Some(avatar) = person.avatar {
purge_image_from_pictrs(client, settings, &avatar) purge_image_from_pictrs(&avatar, context).await.ok();
.await
.ok();
} }
if let Some(banner) = person.banner { if let Some(banner) = person.banner {
purge_image_from_pictrs(client, settings, &banner) purge_image_from_pictrs(&banner, context).await.ok();
.await
.ok();
} }
// No need to update avatar and banner, those are handled in Person::delete_account // No need to update avatar and banner, those are handled in Person::delete_account
@ -726,7 +707,7 @@ pub async fn delete_user_account(
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
// Purge image posts // Purge image posts
purge_image_posts_for_person(person_id, pool, settings, client).await?; purge_image_posts_for_person(person_id, context).await?;
// Leave communities they mod // Leave communities they mod
CommunityModerator::leave_all_communities(pool, person_id).await?; CommunityModerator::leave_all_communities(pool, person_id).await?;

View File

@ -5,8 +5,9 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{DeleteAccount, DeleteAccountResponse}, person::{DeleteAccount, DeleteAccountResponse},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::local_user_view_from_jwt, utils::{local_user_view_from_jwt, purge_user_account},
}; };
use lemmy_db_schema::source::person::Person;
use lemmy_utils::error::{LemmyError, LemmyErrorType}; use lemmy_utils::error::{LemmyError, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -26,8 +27,14 @@ pub async fn delete_account(
return Err(LemmyErrorType::IncorrectLogin)?; return Err(LemmyErrorType::IncorrectLogin)?;
} }
if data.delete_content {
purge_user_account(local_user_view.person.id, &context).await?;
} else {
Person::delete_account(&mut context.pool(), local_user_view.person.id).await?;
}
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::DeleteUser(local_user_view.person), SendActivityData::DeleteUser(local_user_view.person, data.delete_content),
&context, &context,
) )
.await?; .await?;

View File

@ -165,13 +165,7 @@ impl ActivityHandler for BlockUser {
) )
.await?; .await?;
if self.remove_data.unwrap_or(false) { if self.remove_data.unwrap_or(false) {
remove_user_data( remove_user_data(blocked_person.id, context).await?;
blocked_person.id,
&mut context.pool(),
context.settings(),
context.client(),
)
.await?;
} }
// write mod log // write mod log

View File

@ -10,20 +10,17 @@ use activitypub_federation::{
protocol::verification::verify_urls_match, protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor}, traits::{ActivityHandler, Actor},
}; };
use lemmy_api_common::{context::LemmyContext, utils::delete_user_account}; use lemmy_api_common::{context::LemmyContext, utils::purge_user_account};
use lemmy_db_schema::source::person::Person; use lemmy_db_schema::source::person::Person;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use url::Url; use url::Url;
pub async fn delete_user(person: Person, context: Data<LemmyContext>) -> Result<(), LemmyError> { pub async fn delete_user(
person: Person,
delete_content: bool,
context: Data<LemmyContext>,
) -> Result<(), LemmyError> {
let actor: ApubPerson = person.into(); let actor: ApubPerson = person.into();
delete_user_account(
actor.id,
&mut context.pool(),
context.settings(),
context.client(),
)
.await?;
let id = generate_activity_id( let id = generate_activity_id(
DeleteType::Delete, DeleteType::Delete,
@ -36,6 +33,7 @@ pub async fn delete_user(person: Person, context: Data<LemmyContext>) -> Result<
kind: DeleteType::Delete, kind: DeleteType::Delete,
id: id.clone(), id: id.clone(),
cc: vec![], cc: vec![],
remove_data: Some(delete_content),
}; };
let inboxes = remote_instance_inboxes(&mut context.pool()).await?; let inboxes = remote_instance_inboxes(&mut context.pool()).await?;
@ -68,13 +66,11 @@ impl ActivityHandler for DeleteUser {
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> { async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
let actor = self.actor.dereference(context).await?; let actor = self.actor.dereference(context).await?;
delete_user_account( if self.remove_data.unwrap_or(false) {
actor.id, purge_user_account(actor.id, context).await?;
&mut context.pool(), } else {
context.settings(), Person::delete_account(&mut context.pool(), actor.id).await?;
context.client(), }
)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -335,7 +335,7 @@ pub async fn match_outgoing_activities(
DeletePrivateMessage(person, pm, deleted) => { DeletePrivateMessage(person, pm, deleted) => {
send_apub_delete_private_message(&person.into(), pm, deleted, context).await send_apub_delete_private_message(&person.into(), pm, deleted, context).await
} }
DeleteUser(person) => delete_user(person, context).await, DeleteUser(person, delete_content) => delete_user(person, delete_content, context).await,
CreateReport(url, actor, community, reason) => { CreateReport(url, actor, community, reason) => {
Report::send(ObjectId::from(url), actor, community, reason, context).await Report::send(ObjectId::from(url), actor, community, reason, context).await
} }

View File

@ -23,4 +23,6 @@ pub struct DeleteUser {
#[serde(deserialize_with = "deserialize_one_or_many", default)] #[serde(deserialize_with = "deserialize_one_or_many", default)]
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) cc: Vec<Url>, pub(crate) cc: Vec<Url>,
/// Nonstandard field. If present, all content from the user should be deleted along with the account
pub(crate) remove_data: Option<bool>,
} }