diff --git a/.woodpecker.yml b/.woodpecker.yml index 0c7efbd85..88bf3be0c 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -10,9 +10,9 @@ variables: - "**/Cargo.toml" - "Cargo.lock" # database migrations - - "migrations" + - "migrations/**" # typescript tests - - "api_tests" + - "api_tests/**" # config files and scripts used by ci - ".woodpecker.yml" - ".rustfmt.toml" diff --git a/api_tests/package.json b/api_tests/package.json index 1810b8d2f..61c11d00a 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -19,7 +19,7 @@ "eslint": "^8.40.0", "eslint-plugin-prettier": "^4.0.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", "ts-jest": "^29.1.0", "typescript": "^5.0.4" diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 6110f1e34..bc760fd33 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -707,6 +707,7 @@ export async function getPersonDetails( export async function deleteUser(api: API): Promise { let form: DeleteAccount = { auth: api.auth, + delete_content: true, password, }; return api.client.deleteAccount(form); diff --git a/api_tests/yarn.lock b/api_tests/yarn.lock index 275dcd067..b243c1b65 100644 --- a/api_tests/yarn.lock +++ b/api_tests/yarn.lock @@ -2174,10 +2174,10 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lemmy-js-client@0.18.3-rc.3: - version "0.18.3-rc.3" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.3-rc.3.tgz#fc6489eb141bd09558bca38d9e46b40771a29f37" - integrity sha512-njixgXk4uMU4gGifnljwhSe9Kf445C4wAXcXhtpTtwPPLXpHQgxA1RASMb9Uq4zblfE6nC2JbrAka8y8N2N/Bw== +lemmy-js-client@0.19.0-rc.2: + version "0.19.0-rc.2" + resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-rc.2.tgz#c3cb511b27f92538909a2b91a0f8527b1abad958" + integrity sha512-FXuf8s7bpBVkHL/OGWDb/0aGIrJ7uv3d4Xt1h6zmNDhw6MmmuD8RXgCHiS2jqhxjAEp96Dpl1NFXbpmKpix7tQ== dependencies: cross-fetch "^3.1.5" form-data "^4.0.0" diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 490a51448..3a3a240da 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -47,13 +47,7 @@ pub async fn ban_from_site( // Remove their data if that's desired let remove_data = data.remove_data.unwrap_or(false); if remove_data { - remove_user_data( - person.id, - &mut context.pool(), - context.settings(), - context.client(), - ) - .await?; + remove_user_data(person.id, &context).await?; } // Mod tables diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index bd8d9d386..7c59fd76b 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -33,24 +33,14 @@ impl Perform for PurgeCommunity { let community = Community::read(&mut context.pool(), community_id).await?; if let Some(banner) = community.banner { - purge_image_from_pictrs(context.client(), context.settings(), &banner) - .await - .ok(); + purge_image_from_pictrs(&banner, context).await.ok(); } if let Some(icon) = community.icon { - purge_image_from_pictrs(context.client(), context.settings(), &icon) - .await - .ok(); + purge_image_from_pictrs(&icon, context).await.ok(); } - purge_image_posts_for_community( - community_id, - &mut context.pool(), - context.settings(), - context.client(), - ) - .await?; + purge_image_posts_for_community(community_id, context).await?; Community::delete(&mut context.pool(), community_id).await?; diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 838b36070..1100a0db9 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -32,24 +32,14 @@ impl Perform for PurgePerson { let person = Person::read(&mut context.pool(), person_id).await?; if let Some(banner) = person.banner { - purge_image_from_pictrs(context.client(), context.settings(), &banner) - .await - .ok(); + purge_image_from_pictrs(&banner, context).await.ok(); } if let Some(avatar) = person.avatar { - purge_image_from_pictrs(context.client(), context.settings(), &avatar) - .await - .ok(); + purge_image_from_pictrs(&avatar, context).await.ok(); } - purge_image_posts_for_person( - person_id, - &mut context.pool(), - context.settings(), - context.client(), - ) - .await?; + purge_image_posts_for_person(person_id, context).await?; Person::delete(&mut context.pool(), person_id).await?; diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index ee0a3af09..b267f3518 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -34,15 +34,11 @@ impl Perform for PurgePost { // Purge image if let Some(url) = post.url { - purge_image_from_pictrs(context.client(), context.settings(), &url) - .await - .ok(); + purge_image_from_pictrs(&url, context).await.ok(); } // Purge thumbnail if let Some(thumbnail_url) = post.thumbnail_url { - purge_image_from_pictrs(context.client(), context.settings(), &thumbnail_url) - .await - .ok(); + purge_image_from_pictrs(&thumbnail_url, context).await.ok(); } let community_id = post.community_id; diff --git a/crates/api/src/sitemap.rs b/crates/api/src/sitemap.rs index cb47e8e85..438a8b8e8 100644 --- a/crates/api/src/sitemap.rs +++ b/crates/api/src/sitemap.rs @@ -3,22 +3,20 @@ use actix_web::{ web::Data, HttpResponse, }; -use chrono::{DateTime, FixedOffset}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{newtypes::DbUrl, source::post::Post}; use lemmy_utils::error::LemmyResult; use sitemap_rs::{url::Url, url_set::UrlSet}; use tracing::info; -async fn generate_urlset(posts: Vec<(DbUrl, chrono::NaiveDateTime)>) -> LemmyResult { +async fn generate_urlset( + posts: Vec<(DbUrl, chrono::DateTime)>, +) -> LemmyResult { let urls = posts .into_iter() .map_while(|post| { Url::builder(post.0.to_string()) - .last_modified(DateTime::from_utc( - post.1, - FixedOffset::east_opt(0).expect("Error setting timezone offset"), // TODO what is the proper timezone offset here? - )) + .last_modified(post.1.into()) .build() .ok() }) @@ -48,27 +46,29 @@ pub(crate) mod tests { #![allow(clippy::unwrap_used)] use crate::sitemap::generate_urlset; - use chrono::{NaiveDate, NaiveDateTime}; + use chrono::{DateTime, NaiveDate, Utc}; use elementtree::Element; use lemmy_db_schema::newtypes::DbUrl; use url::Url; #[tokio::test] async fn test_generate_urlset() { - let posts: Vec<(DbUrl, NaiveDateTime)> = vec![ + let posts: Vec<(DbUrl, DateTime)> = vec![ ( Url::parse("https://example.com").unwrap().into(), NaiveDate::from_ymd_opt(2022, 12, 1) .unwrap() .and_hms_opt(9, 10, 11) - .unwrap(), + .unwrap() + .and_utc(), ), ( Url::parse("https://lemmy.ml").unwrap().into(), NaiveDate::from_ymd_opt(2023, 1, 1) .unwrap() .and_hms_opt(1, 2, 3) - .unwrap(), + .unwrap() + .and_utc(), ), ]; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index ef970f1bc..695486a5a 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -365,6 +365,7 @@ pub struct CommentReplyResponse { /// Delete your account. pub struct DeleteAccount { pub password: Sensitive, + pub delete_content: bool, pub auth: Sensitive, } diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index e06afacaa..90364a978 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -1,4 +1,4 @@ -use crate::post::SiteMetadata; +use crate::{context::LemmyContext, post::SiteMetadata}; use encoding::{all::encodings, DecoderTrap}; use lemmy_db_schema::newtypes::DbUrl; use lemmy_utils::{ @@ -150,12 +150,11 @@ pub(crate) async fn fetch_pictrs( /// - It might not be an image /// - Pictrs might not be set up pub async fn purge_image_from_pictrs( - client: &ClientWithMiddleware, - settings: &Settings, image_url: &Url, + context: &LemmyContext, ) -> Result<(), LemmyError> { - let pictrs_config = settings.pictrs_config()?; - is_image_content_type(client, image_url).await?; + let pictrs_config = context.settings().pictrs_config()?; + is_image_content_type(context.client(), image_url).await?; let alias = image_url .path_segments() @@ -168,7 +167,8 @@ pub async fn purge_image_from_pictrs( let pictrs_api_key = pictrs_config .api_key .ok_or(LemmyErrorType::PictrsApiKeyNotProvided)?; - let response = client + let response = context + .client() .post(&purge_url) .timeout(REQWEST_TIMEOUT) .header("x-api-token", pictrs_api_key) diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index 1ba9aa646..45109d5c5 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -58,7 +58,7 @@ pub enum SendActivityData { CreatePrivateMessage(PrivateMessageView), UpdatePrivateMessage(PrivateMessageView), DeletePrivateMessage(Person, PrivateMessage, bool), - DeleteUser(Person), + DeleteUser(Person, bool), CreateReport(Url, Person, Community, String), } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 8563fb879..951be63c0 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -42,7 +42,6 @@ use lemmy_utils::{ utils::slurs::build_slur_regex, }; use regex::Regex; -use reqwest_middleware::ClientWithMiddleware; use rosetta_i18n::{Language, LanguageId}; use tracing::warn; use url::{ParseError, Url}; @@ -534,19 +533,16 @@ pub fn check_private_instance_and_federation_enabled( pub async fn purge_image_posts_for_person( banned_person_id: PersonId, - pool: &mut DbPool<'_>, - settings: &Settings, - client: &ClientWithMiddleware, + context: &LemmyContext, ) -> Result<(), LemmyError> { + let pool = &mut context.pool(); let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?; for post in posts { 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 { - purge_image_from_pictrs(client, settings, &thumbnail_url) - .await - .ok(); + purge_image_from_pictrs(&thumbnail_url, context).await.ok(); } } @@ -557,19 +553,16 @@ pub async fn purge_image_posts_for_person( pub async fn purge_image_posts_for_community( banned_community_id: CommunityId, - pool: &mut DbPool<'_>, - settings: &Settings, - client: &ClientWithMiddleware, + context: &LemmyContext, ) -> Result<(), LemmyError> { + let pool = &mut context.pool(); let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?; for post in posts { 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 { - purge_image_from_pictrs(client, settings, &thumbnail_url) - .await - .ok(); + purge_image_from_pictrs(&thumbnail_url, context).await.ok(); } } @@ -580,21 +573,16 @@ pub async fn purge_image_posts_for_community( pub async fn remove_user_data( banned_person_id: PersonId, - pool: &mut DbPool<'_>, - settings: &Settings, - client: &ClientWithMiddleware, + context: &LemmyContext, ) -> Result<(), LemmyError> { + let pool = &mut context.pool(); // Purge user images let person = Person::read(pool, banned_person_id).await?; if let Some(avatar) = person.avatar { - purge_image_from_pictrs(client, settings, &avatar) - .await - .ok(); + purge_image_from_pictrs(&avatar, context).await.ok(); } if let Some(banner) = person.banner { - purge_image_from_pictrs(client, settings, &banner) - .await - .ok(); + purge_image_from_pictrs(&banner, context).await.ok(); } // Update the fields to None @@ -613,7 +601,7 @@ pub async fn remove_user_data( Post::update_removed_for_creator(pool, banned_person_id, None, true).await?; // 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 // Remove all communities where they're the top mod @@ -640,12 +628,10 @@ pub async fn remove_user_data( // Delete the community images 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 { - purge_image_from_pictrs(client, settings, &banner) - .await - .ok(); + purge_image_from_pictrs(&banner, context).await.ok(); } // Update the fields to None Community::update( @@ -700,23 +686,18 @@ pub async fn remove_user_data_in_community( Ok(()) } -pub async fn delete_user_account( +pub async fn purge_user_account( person_id: PersonId, - pool: &mut DbPool<'_>, - settings: &Settings, - client: &ClientWithMiddleware, + context: &LemmyContext, ) -> Result<(), LemmyError> { + let pool = &mut context.pool(); // Delete their images let person = Person::read(pool, person_id).await?; if let Some(avatar) = person.avatar { - purge_image_from_pictrs(client, settings, &avatar) - .await - .ok(); + purge_image_from_pictrs(&avatar, context).await.ok(); } if let Some(banner) = person.banner { - purge_image_from_pictrs(client, settings, &banner) - .await - .ok(); + purge_image_from_pictrs(&banner, context).await.ok(); } // No need to update avatar and banner, those are handled in Person::delete_account @@ -731,7 +712,7 @@ pub async fn delete_user_account( .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; // 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 CommunityModerator::leave_all_communities(pool, person_id).await?; diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index aae4ca28f..bf1dcdab1 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -5,8 +5,9 @@ use lemmy_api_common::{ context::LemmyContext, person::{DeleteAccount, DeleteAccountResponse}, 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}; #[tracing::instrument(skip(context))] @@ -26,8 +27,14 @@ pub async fn delete_account( 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( - SendActivityData::DeleteUser(local_user_view.person), + SendActivityData::DeleteUser(local_user_view.person, data.delete_content), &context, ) .await?; diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 163aca038..7b14213ca 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -165,13 +165,7 @@ impl ActivityHandler for BlockUser { ) .await?; if self.remove_data.unwrap_or(false) { - remove_user_data( - blocked_person.id, - &mut context.pool(), - context.settings(), - context.client(), - ) - .await?; + remove_user_data(blocked_person.id, context).await?; } // write mod log diff --git a/crates/apub/src/activities/deletion/delete_user.rs b/crates/apub/src/activities/deletion/delete_user.rs index cf37dc5ab..66a402161 100644 --- a/crates/apub/src/activities/deletion/delete_user.rs +++ b/crates/apub/src/activities/deletion/delete_user.rs @@ -10,20 +10,17 @@ use activitypub_federation::{ protocol::verification::verify_urls_match, 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_utils::error::LemmyError; use url::Url; -pub async fn delete_user(person: Person, context: Data) -> Result<(), LemmyError> { +pub async fn delete_user( + person: Person, + delete_content: bool, + context: Data, +) -> Result<(), LemmyError> { let actor: ApubPerson = person.into(); - delete_user_account( - actor.id, - &mut context.pool(), - context.settings(), - context.client(), - ) - .await?; let id = generate_activity_id( DeleteType::Delete, @@ -36,6 +33,7 @@ pub async fn delete_user(person: Person, context: Data) -> Result< kind: DeleteType::Delete, id: id.clone(), cc: vec![], + remove_data: Some(delete_content), }; let inboxes = remote_instance_inboxes(&mut context.pool()).await?; @@ -68,13 +66,11 @@ impl ActivityHandler for DeleteUser { async fn receive(self, context: &Data) -> Result<(), LemmyError> { let actor = self.actor.dereference(context).await?; - delete_user_account( - actor.id, - &mut context.pool(), - context.settings(), - context.client(), - ) - .await?; + if self.remove_data.unwrap_or(false) { + purge_user_account(actor.id, context).await?; + } else { + Person::delete_account(&mut context.pool(), actor.id).await?; + } Ok(()) } } diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index f9663f211..c6696725e 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -338,7 +338,7 @@ pub async fn match_outgoing_activities( DeletePrivateMessage(person, pm, deleted) => { 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) => { Report::send(ObjectId::from(url), actor, community, reason, context).await } diff --git a/crates/apub/src/protocol/activities/deletion/delete_user.rs b/crates/apub/src/protocol/activities/deletion/delete_user.rs index f5f9908d5..46b070fab 100644 --- a/crates/apub/src/protocol/activities/deletion/delete_user.rs +++ b/crates/apub/src/protocol/activities/deletion/delete_user.rs @@ -23,4 +23,6 @@ pub struct DeleteUser { #[serde(deserialize_with = "deserialize_one_or_many", default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub(crate) cc: Vec, + /// Nonstandard field. If present, all content from the user should be deleted along with the account + pub(crate) remove_data: Option, } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 630eac426..514aceaba 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -101,16 +101,16 @@ impl Post { pub async fn list_for_sitemap( pool: &mut DbPool<'_>, - ) -> Result, Error> { + ) -> Result)>, Error> { let conn = &mut get_conn(pool).await?; post .select((ap_id, coalesce(updated, published))) - .filter(local) + .filter(local.eq(true)) .filter(deleted.eq(false)) .filter(removed.eq(false)) .filter(published.ge(Utc::now().naive_utc() - Duration::days(1))) .order(published.desc()) - .load::<(DbUrl, chrono::NaiveDateTime)>(conn) + .load::<(DbUrl, chrono::DateTime)>(conn) .await }