Add CLI command that deletes old remote posts

This commit is contained in:
silverpill 2022-02-19 00:32:30 +00:00
parent 76106e4214
commit 8b4325ad96
2 changed files with 89 additions and 4 deletions

View file

@ -1,3 +1,4 @@
use chrono::{Duration, Utc};
use clap::Clap; use clap::Clap;
use uuid::Uuid; use uuid::Uuid;
@ -7,7 +8,7 @@ use mitra::database::migrate::apply_migrations;
use mitra::ethereum::signatures::generate_ecdsa_key; use mitra::ethereum::signatures::generate_ecdsa_key;
use mitra::ethereum::utils::key_to_ethereum_address; use mitra::ethereum::utils::key_to_ethereum_address;
use mitra::logger::configure_logger; 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::profiles::queries::delete_profile;
use mitra::models::users::queries::{ use mitra::models::users::queries::{
create_invite_code, create_invite_code,
@ -31,6 +32,7 @@ enum SubCommand {
ListInviteCodes(ListInviteCodes), ListInviteCodes(ListInviteCodes),
DeleteProfile(DeleteProfile), DeleteProfile(DeleteProfile),
DeletePost(DeletePost), DeletePost(DeletePost),
DeleteExtraneousPosts(DeleteExtraneousPosts),
} }
/// Generate RSA private key /// Generate RSA private key
@ -71,6 +73,16 @@ struct DeletePost {
id: Uuid, id: Uuid,
} }
/// Delete old remote posts
#[derive(Clap)]
struct DeleteExtraneousPosts {
#[clap(short)]
days: i64,
#[clap(long)]
dry_run: bool,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let opts: Opts = Opts::parse(); let opts: Opts = Opts::parse();
@ -103,10 +115,10 @@ async fn main() {
if invite_codes.is_empty() { if invite_codes.is_empty() {
println!("no invite codes found"); println!("no invite codes found");
return; return;
} };
for code in invite_codes { for code in invite_codes {
println!("{}", code); println!("{}", code);
} };
}, },
SubCommand::DeleteProfile(subopts) => { SubCommand::DeleteProfile(subopts) => {
let deletion_queue = delete_profile(db_client, &subopts.id).await.unwrap(); let deletion_queue = delete_profile(db_client, &subopts.id).await.unwrap();
@ -118,6 +130,17 @@ async fn main() {
deletion_queue.process(&config).await; deletion_queue.process(&config).await;
println!("post deleted"); 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!(), _ => panic!(),
}; };
}, },

View file

@ -1,6 +1,6 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use chrono::Utc; use chrono::{DateTime, Utc};
use tokio_postgres::GenericClient; use tokio_postgres::GenericClient;
use uuid::Uuid; use uuid::Uuid;
@ -727,6 +727,68 @@ pub async fn get_token_waitlist(
Ok(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<Utc>,
) -> Result<Vec<Uuid>, 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<Uuid> = rows.iter()
.map(|row| row.try_get("id"))
.collect::<Result<_, _>>()?;
Ok(ids)
}
/// Deletes post from database and returns collection of orphaned objects. /// Deletes post from database and returns collection of orphaned objects.
pub async fn delete_post( pub async fn delete_post(
db_client: &mut impl GenericClient, db_client: &mut impl GenericClient,