diff --git a/src/bin/mitractl.rs b/src/bin/mitractl.rs index 348575d..78f5337 100644 --- a/src/bin/mitractl.rs +++ b/src/bin/mitractl.rs @@ -1,3 +1,4 @@ +use chrono::{Duration, Utc}; use clap::Clap; use uuid::Uuid; @@ -7,7 +8,7 @@ use mitra::database::migrate::apply_migrations; use mitra::ethereum::signatures::generate_ecdsa_key; use mitra::ethereum::utils::key_to_ethereum_address; use mitra::logger::configure_logger; -use mitra::models::posts::queries::delete_post; +use mitra::models::posts::queries::{delete_post, find_extraneous_posts}; use mitra::models::profiles::queries::delete_profile; use mitra::models::users::queries::{ create_invite_code, @@ -31,6 +32,7 @@ enum SubCommand { ListInviteCodes(ListInviteCodes), DeleteProfile(DeleteProfile), DeletePost(DeletePost), + DeleteExtraneousPosts(DeleteExtraneousPosts), } /// Generate RSA private key @@ -71,6 +73,16 @@ struct DeletePost { id: Uuid, } +/// Delete old remote posts +#[derive(Clap)] +struct DeleteExtraneousPosts { + #[clap(short)] + days: i64, + + #[clap(long)] + dry_run: bool, +} + #[tokio::main] async fn main() { let opts: Opts = Opts::parse(); @@ -103,10 +115,10 @@ async fn main() { if invite_codes.is_empty() { println!("no invite codes found"); return; - } + }; for code in invite_codes { println!("{}", code); - } + }; }, SubCommand::DeleteProfile(subopts) => { let deletion_queue = delete_profile(db_client, &subopts.id).await.unwrap(); @@ -118,6 +130,17 @@ async fn main() { deletion_queue.process(&config).await; println!("post deleted"); }, + SubCommand::DeleteExtraneousPosts(subopts) => { + let created_before = Utc::now() - Duration::days(subopts.days); + let posts = find_extraneous_posts(db_client, &created_before).await.unwrap(); + for post_id in posts { + if !subopts.dry_run { + let deletion_queue = delete_post(db_client, &post_id).await.unwrap(); + deletion_queue.process(&config).await; + }; + println!("post {} deleted", post_id); + }; + }, _ => panic!(), }; }, diff --git a/src/models/posts/queries.rs b/src/models/posts/queries.rs index a4cbe1e..e497324 100644 --- a/src/models/posts/queries.rs +++ b/src/models/posts/queries.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; -use chrono::Utc; +use chrono::{DateTime, Utc}; use tokio_postgres::GenericClient; use uuid::Uuid; @@ -727,6 +727,68 @@ pub async fn get_token_waitlist( Ok(waitlist) } +/// Finds all contexts (identified by top-level post) +/// created before the specified date +/// that do not contain local posts, reposts, mentions or reactions. +pub async fn find_extraneous_posts( + db_client: &impl GenericClient, + created_before: &DateTime, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + WITH RECURSIVE context (id, post_id) AS ( + SELECT post.id, post.id FROM post + WHERE + post.in_reply_to_id IS NULL + AND post.repost_of_id IS NULL + AND post.created_at < $1 + UNION + SELECT context.id, post.id FROM post + JOIN context ON ( + post.in_reply_to_id = context.post_id + OR post.repost_of_id = context.post_id + ) + ) + SELECT context_agg.id + FROM ( + SELECT context.id, array_agg(context.post_id) AS posts + FROM context + GROUP BY context.id + ) AS context_agg + WHERE + NOT EXISTS ( + SELECT 1 + FROM post + JOIN actor_profile ON post.author_id = actor_profile.id + WHERE + post.id = ANY(context_agg.posts) + AND actor_profile.actor_json IS NULL + ) + AND NOT EXISTS ( + SELECT 1 + FROM mention + JOIN actor_profile ON mention.profile_id = actor_profile.id + WHERE + mention.post_id = ANY(context_agg.posts) + AND actor_profile.actor_json IS NULL + ) + AND NOT EXISTS ( + SELECT 1 + FROM post_reaction + JOIN actor_profile ON post_reaction.author_id = actor_profile.id + WHERE + post_reaction.post_id = ANY(context_agg.posts) + AND actor_profile.actor_json IS NULL + ) + ", + &[&created_before], + ).await?; + let ids: Vec = rows.iter() + .map(|row| row.try_get("id")) + .collect::>()?; + Ok(ids) +} + /// Deletes post from database and returns collection of orphaned objects. pub async fn delete_post( db_client: &mut impl GenericClient,