diff --git a/src/bin/mitractl.rs b/src/bin/mitractl.rs index 66fa313..2d208b8 100644 --- a/src/bin/mitractl.rs +++ b/src/bin/mitractl.rs @@ -1,178 +1,10 @@ -use anyhow::Error; -use chrono::{Duration, Utc}; use clap::Parser; -use tokio_postgres::GenericClient; -use uuid::Uuid; -use mitra::activitypub::builders::delete_note::prepare_delete_note; -use mitra::activitypub::builders::delete_person::prepare_delete_person; -use mitra::activitypub::fetcher::fetchers::fetch_actor; -use mitra::activitypub::handlers::update_person::update_remote_profile; -use mitra::config::{parse_config, Config}; +use mitra::cli::{Opts, SubCommand}; +use mitra::config::parse_config; use mitra::database::create_database_client; use mitra::database::migrate::apply_migrations; -use mitra::ethereum::signatures::generate_ecdsa_key; -use mitra::ethereum::sync::save_current_block_number; -use mitra::ethereum::utils::key_to_ethereum_address; use mitra::logger::configure_logger; -use mitra::models::attachments::queries::delete_unused_attachments; -use mitra::models::cleanup::find_orphaned_files; -use mitra::models::posts::queries::{delete_post, find_extraneous_posts, get_post_by_id}; -use mitra::models::profiles::queries::{ - delete_profile, - get_profile_by_actor_id, - get_profile_by_id, - reset_subscriptions, -}; -use mitra::models::users::queries::{ - create_invite_code, - get_invite_codes, - get_user_by_id, -}; -use mitra::utils::crypto::{generate_private_key, serialize_private_key}; -use mitra::utils::files::remove_files; - -/// Admin CLI tool -#[derive(Parser)] -struct Opts { - #[clap(subcommand)] - subcmd: SubCommand, -} - -#[derive(Parser)] -enum SubCommand { - GenerateRsaKey(GenerateRsaKey), - GenerateEthereumAddress(GenerateEthereumAddress), - - GenerateInviteCode(GenerateInviteCode), - ListInviteCodes(ListInviteCodes), - RefetchActor(RefetchActor), - DeleteProfile(DeleteProfile), - DeletePost(DeletePost), - DeleteExtraneousPosts(DeleteExtraneousPosts), - DeleteUnusedAttachments(DeleteUnusedAttachments), - UpdateCurrentBlock(UpdateCurrentBlock), - DeleteOrphanedFiles(DeleteOrphanedFiles), -} - -/// Generate RSA private key -#[derive(Parser)] -struct GenerateRsaKey; - -impl GenerateRsaKey { - fn execute(&self) -> () { - let private_key = generate_private_key().unwrap(); - let private_key_str = serialize_private_key(&private_key).unwrap(); - println!("{}", private_key_str); - } -} - -/// Generate ethereum address -#[derive(Parser)] -struct GenerateEthereumAddress; - -/// Generate invite code -#[derive(Parser)] -struct GenerateInviteCode; - -/// List invite codes -#[derive(Parser)] -struct ListInviteCodes; - -/// Re-fetch actor profile by actor ID -#[derive(Parser)] -struct RefetchActor { - id: String, -} - -impl RefetchActor { - async fn execute( - &self, - config: &Config, - db_client: &impl GenericClient, - ) -> Result<(), Error> { - let profile = get_profile_by_actor_id(db_client, &self.id).await?; - let actor = fetch_actor(&config.instance(), &self.id).await?; - update_remote_profile(db_client, &config.media_dir(), profile, actor).await?; - println!("profile updated"); - Ok(()) - } -} - -/// Delete profile -#[derive(Parser)] -struct DeleteProfile { - id: Uuid, -} - -/// Delete post -#[derive(Parser)] -struct DeletePost { - id: Uuid, -} - -/// Delete old remote posts -#[derive(Parser)] -struct DeleteExtraneousPosts { - days: i64, -} - -/// Delete attachments that don't belong to any post -#[derive(Parser)] -struct DeleteUnusedAttachments { - days: i64, -} - -/// Find and delete orphaned files -#[derive(Parser)] -struct DeleteOrphanedFiles; - -impl DeleteOrphanedFiles { - async fn execute( - &self, - config: &Config, - db_client: &impl GenericClient, - ) -> Result<(), Error> { - let media_dir = config.media_dir(); - let mut files = vec![]; - for maybe_path in std::fs::read_dir(&media_dir)? { - let file_name = maybe_path?.file_name() - .to_string_lossy().to_string(); - files.push(file_name); - }; - println!("found {} files", files.len()); - let orphaned = find_orphaned_files(db_client, files).await?; - if !orphaned.is_empty() { - remove_files(orphaned, &media_dir); - println!("orphaned files deleted"); - }; - Ok(()) - } -} - -/// Update blockchain synchronization starting block -#[derive(Parser)] -struct UpdateCurrentBlock { - number: u64, - - #[clap(long)] - reset_db: bool, -} - -impl UpdateCurrentBlock { - async fn execute( - &self, - config: &Config, - db_client: &impl GenericClient, - ) -> Result<(), Error> { - save_current_block_number(&config.storage_dir, self.number)?; - if self.reset_db { - reset_subscriptions(db_client).await?; - }; - println!("current block updated"); - Ok(()) - } -} #[tokio::main] async fn main() { @@ -180,14 +12,7 @@ async fn main() { match opts.subcmd { SubCommand::GenerateRsaKey(cmd) => cmd.execute(), - SubCommand::GenerateEthereumAddress(_) => { - let private_key = generate_ecdsa_key(); - let address = key_to_ethereum_address(&private_key); - println!( - "address {:?}; private key {}", - address, private_key.display_secret(), - ); - }, + SubCommand::GenerateEthereumAddress(cmd) => cmd.execute(), subcmd => { // Other commands require initialized app let config = parse_config(); @@ -198,68 +23,13 @@ async fn main() { apply_migrations(db_client).await; match subcmd { - SubCommand::GenerateInviteCode(_) => { - let invite_code = create_invite_code(db_client).await.unwrap(); - println!("generated invite code: {}", invite_code); - }, - SubCommand::ListInviteCodes(_) => { - let invite_codes = get_invite_codes(db_client).await.unwrap(); - if invite_codes.is_empty() { - println!("no invite codes found"); - return; - }; - for code in invite_codes { - println!("{}", code); - }; - }, + SubCommand::GenerateInviteCode(cmd) => cmd.execute(db_client).await.unwrap(), + SubCommand::ListInviteCodes(cmd) => cmd.execute(db_client).await.unwrap(), SubCommand::RefetchActor(cmd) => cmd.execute(&config, db_client).await.unwrap(), - SubCommand::DeleteProfile(subopts) => { - let profile = get_profile_by_id(db_client, &subopts.id).await.unwrap(); - let mut maybe_delete_person = None; - if profile.is_local() { - let user = get_user_by_id(db_client, &profile.id).await.unwrap(); - let activity = prepare_delete_person(db_client, config.instance(), &user) - .await.unwrap(); - maybe_delete_person = Some(activity); - }; - let deletion_queue = delete_profile(db_client, &profile.id).await.unwrap(); - deletion_queue.process(&config).await; - // Send Delete(Person) activities - if let Some(activity) = maybe_delete_person { - activity.deliver().await.unwrap(); - }; - println!("profile deleted"); - }, - SubCommand::DeletePost(subopts) => { - let post = get_post_by_id(db_client, &subopts.id).await.unwrap(); - let deletion_queue = delete_post(db_client, &post.id).await.unwrap(); - deletion_queue.process(&config).await; - if post.author.is_local() { - // Send Delete(Note) activity - let author = get_user_by_id(db_client, &post.author.id).await.unwrap(); - prepare_delete_note(db_client, config.instance(), &author, &post).await.unwrap() - .deliver().await.unwrap(); - }; - 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 { - let deletion_queue = delete_post(db_client, &post_id).await.unwrap(); - deletion_queue.process(&config).await; - println!("post {} deleted", post_id); - }; - }, - SubCommand::DeleteUnusedAttachments(subopts) => { - let created_before = Utc::now() - Duration::days(subopts.days); - let deletion_queue = delete_unused_attachments( - db_client, - &created_before, - ).await.unwrap(); - deletion_queue.process(&config).await; - println!("unused attachments deleted"); - }, + SubCommand::DeleteProfile(cmd) => cmd.execute(&config, db_client).await.unwrap(), + SubCommand::DeletePost(cmd) => cmd.execute(&config, db_client).await.unwrap(), + SubCommand::DeleteExtraneousPosts(cmd) => cmd.execute(&config, db_client).await.unwrap(), + SubCommand::DeleteUnusedAttachments(cmd) => cmd.execute(&config, db_client).await.unwrap(), SubCommand::DeleteOrphanedFiles(cmd) => cmd.execute(&config, db_client).await.unwrap(), SubCommand::UpdateCurrentBlock(cmd) => cmd.execute(&config, db_client).await.unwrap(), _ => panic!(), diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..4f702d0 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,291 @@ +use anyhow::Error; +use chrono::{Duration, Utc}; +use clap::Parser; +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::activitypub::builders::delete_note::prepare_delete_note; +use crate::activitypub::builders::delete_person::prepare_delete_person; +use crate::activitypub::fetcher::fetchers::fetch_actor; +use crate::activitypub::handlers::update_person::update_remote_profile; +use crate::config::Config; +use crate::ethereum::signatures::generate_ecdsa_key; +use crate::ethereum::sync::save_current_block_number; +use crate::ethereum::utils::key_to_ethereum_address; +use crate::models::attachments::queries::delete_unused_attachments; +use crate::models::cleanup::find_orphaned_files; +use crate::models::posts::queries::{delete_post, find_extraneous_posts, get_post_by_id}; +use crate::models::profiles::queries::{ + delete_profile, + get_profile_by_actor_id, + get_profile_by_id, + reset_subscriptions, +}; +use crate::models::users::queries::{ + create_invite_code, + get_invite_codes, + get_user_by_id, +}; +use crate::utils::crypto::{generate_private_key, serialize_private_key}; +use crate::utils::files::remove_files; + +/// Admin CLI tool +#[derive(Parser)] +pub struct Opts { + #[clap(subcommand)] + pub subcmd: SubCommand, +} + +#[derive(Parser)] +pub enum SubCommand { + GenerateRsaKey(GenerateRsaKey), + GenerateEthereumAddress(GenerateEthereumAddress), + + GenerateInviteCode(GenerateInviteCode), + ListInviteCodes(ListInviteCodes), + RefetchActor(RefetchActor), + DeleteProfile(DeleteProfile), + DeletePost(DeletePost), + DeleteExtraneousPosts(DeleteExtraneousPosts), + DeleteUnusedAttachments(DeleteUnusedAttachments), + UpdateCurrentBlock(UpdateCurrentBlock), + DeleteOrphanedFiles(DeleteOrphanedFiles), +} + +/// Generate RSA private key +#[derive(Parser)] +pub struct GenerateRsaKey; + +impl GenerateRsaKey { + pub fn execute(&self) -> () { + let private_key = generate_private_key().unwrap(); + let private_key_str = serialize_private_key(&private_key).unwrap(); + println!("{}", private_key_str); + } +} + +/// Generate ethereum address +#[derive(Parser)] +pub struct GenerateEthereumAddress; + +impl GenerateEthereumAddress { + pub fn execute(&self) -> () { + let private_key = generate_ecdsa_key(); + let address = key_to_ethereum_address(&private_key); + println!( + "address {:?}; private key {}", + address, private_key.display_secret(), + ); + } +} + +/// Generate invite code +#[derive(Parser)] +pub struct GenerateInviteCode; + +impl GenerateInviteCode { + pub async fn execute( + &self, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + let invite_code = create_invite_code(db_client).await?; + println!("generated invite code: {}", invite_code); + Ok(()) + } +} + +/// List invite codes +#[derive(Parser)] +pub struct ListInviteCodes; + +impl ListInviteCodes { + pub async fn execute( + &self, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + let invite_codes = get_invite_codes(db_client).await?; + if invite_codes.is_empty() { + println!("no invite codes found"); + return Ok(()); + }; + for code in invite_codes { + println!("{}", code); + }; + Ok(()) + } +} + +/// Re-fetch actor profile by actor ID +#[derive(Parser)] +pub struct RefetchActor { + id: String, +} + +impl RefetchActor { + pub async fn execute( + &self, + config: &Config, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + let profile = get_profile_by_actor_id(db_client, &self.id).await?; + let actor = fetch_actor(&config.instance(), &self.id).await?; + update_remote_profile(db_client, &config.media_dir(), profile, actor).await?; + println!("profile updated"); + Ok(()) + } +} + +/// Delete profile +#[derive(Parser)] +pub struct DeleteProfile { + id: Uuid, +} + +impl DeleteProfile { + pub async fn execute( + &self, + config: &Config, + db_client: &mut impl GenericClient, + ) -> Result<(), Error> { + let profile = get_profile_by_id(db_client, &self.id).await?; + let mut maybe_delete_person = None; + if profile.is_local() { + let user = get_user_by_id(db_client, &profile.id).await?; + let activity = + prepare_delete_person(db_client, config.instance(), &user).await?; + maybe_delete_person = Some(activity); + }; + let deletion_queue = delete_profile(db_client, &profile.id).await?; + deletion_queue.process(config).await; + // Send Delete(Person) activities + if let Some(activity) = maybe_delete_person { + activity.deliver().await?; + }; + println!("profile deleted"); + Ok(()) + } +} + +/// Delete post +#[derive(Parser)] +pub struct DeletePost { + id: Uuid, +} + +impl DeletePost { + pub async fn execute( + &self, + config: &Config, + db_client: &mut impl GenericClient, + ) -> Result<(), Error> { + let post = get_post_by_id(db_client, &self.id).await?; + let deletion_queue = delete_post(db_client, &post.id).await?; + deletion_queue.process(config).await; + if post.author.is_local() { + // Send Delete(Note) activity + let author = get_user_by_id(db_client, &post.author.id).await?; + prepare_delete_note(db_client, config.instance(), &author, &post) + .await? + .deliver().await?; + }; + println!("post deleted"); + Ok(()) + } +} + +/// Delete old remote posts +#[derive(Parser)] +pub struct DeleteExtraneousPosts { + days: i64, +} + +impl DeleteExtraneousPosts { + pub async fn execute( + &self, + config: &Config, + db_client: &mut impl GenericClient, + ) -> Result<(), Error> { + let created_before = Utc::now() - Duration::days(self.days); + let posts = find_extraneous_posts(db_client, &created_before).await?; + for post_id in posts { + let deletion_queue = delete_post(db_client, &post_id).await?; + deletion_queue.process(config).await; + println!("post {} deleted", post_id); + }; + Ok(()) + } +} + +/// Delete attachments that don't belong to any post +#[derive(Parser)] +pub struct DeleteUnusedAttachments { + days: i64, +} + +impl DeleteUnusedAttachments { + pub async fn execute( + &self, + config: &Config, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + let created_before = Utc::now() - Duration::days(self.days); + let deletion_queue = delete_unused_attachments( + db_client, + &created_before, + ).await?; + deletion_queue.process(config).await; + println!("unused attachments deleted"); + Ok(()) + } +} + +/// Find and delete orphaned files +#[derive(Parser)] +pub struct DeleteOrphanedFiles; + +impl DeleteOrphanedFiles { + pub async fn execute( + &self, + config: &Config, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + let media_dir = config.media_dir(); + let mut files = vec![]; + for maybe_path in std::fs::read_dir(&media_dir)? { + let file_name = maybe_path?.file_name() + .to_string_lossy().to_string(); + files.push(file_name); + }; + println!("found {} files", files.len()); + let orphaned = find_orphaned_files(db_client, files).await?; + if !orphaned.is_empty() { + remove_files(orphaned, &media_dir); + println!("orphaned files deleted"); + }; + Ok(()) + } +} + +/// Update blockchain synchronization starting block +#[derive(Parser)] +pub struct UpdateCurrentBlock { + number: u64, + + #[clap(long)] + reset_db: bool, +} + +impl UpdateCurrentBlock { + pub async fn execute( + &self, + config: &Config, + db_client: &impl GenericClient, + ) -> Result<(), Error> { + save_current_block_number(&config.storage_dir, self.number)?; + if self.reset_db { + reset_subscriptions(db_client).await?; + }; + println!("current block updated"); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 35f2d2b..95344fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod activitypub; pub mod atom; +pub mod cli; pub mod config; pub mod database; mod errors;