diff options
| author | Martin Fischer <martin@push-f.com> | 2026-10-25 09:08:01 +0100 |
|---|---|---|
| committer | Martin Fischer <martin@push-f.com> | 2026-10-25 19:10:47 +0100 |
| commit | 41212a7a15198e53d205252a02069a7dd6ddadeb (patch) | |
| tree | 38a24eebf956b2453675ac1cefec925faf09de02 /src/bin/delete-recent.rs | |
initial commit
Diffstat (limited to 'src/bin/delete-recent.rs')
| -rw-r--r-- | src/bin/delete-recent.rs | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/src/bin/delete-recent.rs b/src/bin/delete-recent.rs new file mode 100644 index 0000000..5486555 --- /dev/null +++ b/src/bin/delete-recent.rs @@ -0,0 +1,128 @@ +use std::{env, fs}; + +use atrium_api::{ + app::bsky::feed::post::RecordData, + com::atproto::repo::list_records::{OutputData, ParametersData}, + types::{ + TryFromUnknown, + string::{AtIdentifier, Datetime}, + }, +}; +use bsky_sdk::BskyAgent; +use chrono::{Duration, Utc}; +use clap::Parser; +use serde::Deserialize; + +#[derive(Debug, Parser)] +#[command(about = "Delete posts created by the account during the last hour")] +struct Args { + /// Print matching posts without deleting them. + #[arg(long)] + dry: bool, +} + +#[derive(Deserialize)] +struct Credentials { + identifier: String, + password: String, +} + +#[tokio::main] +async fn main() -> Result<(), String> { + tracing_subscriber::fmt::init(); + + let args = Args::parse(); + let credentials = load_credentials()?; + let repo: AtIdentifier = credentials + .identifier + .parse() + .map_err(|error| format!("invalid Bluesky identifier: {error}"))?; + let cutoff = Datetime::new((Utc::now() - Duration::hours(1)).fixed_offset()); + + let agent = BskyAgent::builder() + .build() + .await + .map_err(|error| format!("failed to create Bluesky client: {error}"))?; + agent + .login(credentials.identifier, credentials.password) + .await + .map_err(|error| format!("failed to log in to Bluesky: {error}"))?; + + let mut cursor = None; + let mut posts = Vec::new(); + let mut reached_cutoff = false; + + while !reached_cutoff { + let output = agent + .api + .com + .atproto + .repo + .list_records( + ParametersData { + collection: "app.bsky.feed.post".parse().unwrap(), + cursor, + limit: Some(100.try_into().unwrap()), + repo: repo.clone(), + // listRecords is newest-first by default. + reverse: None, + } + .into(), + ) + .await + .map_err(|error| format!("failed to list posts: {error}"))?; + + let OutputData { + records, + cursor: next_cursor, + } = output.data; + for record in records { + let record = record.data; + let post = RecordData::try_from_unknown(record.value) + .map_err(|error| format!("failed to parse post {}: {error}", record.uri))?; + if post.created_at < cutoff { + reached_cutoff = true; + break; + } + posts.push((record.uri, post.text)); + } + + cursor = next_cursor; + if cursor.is_none() { + break; + } + } + + if posts.is_empty() { + println!("No posts from the last hour."); + return Ok(()); + } + + for (uri, text) in &posts { + if args.dry { + println!("Would delete {uri}: {text}"); + } else { + tracing::info!(%uri, "deleting post"); + agent + .delete_record(uri) + .await + .map_err(|error| format!("failed to delete {uri}: {error}"))?; + println!("Deleted {uri}: {text}"); + } + } + + if args.dry { + println!("{} post(s) would be deleted.", posts.len()); + } else { + println!("Deleted {} post(s).", posts.len()); + } + + Ok(()) +} + +fn load_credentials() -> Result<Credentials, String> { + let path = env::var("BSKY_CREDENTIALS_FILE") + .map_err(|_| "BSKY_CREDENTIALS_FILE is not set".to_owned())?; + let data = fs::read(&path).map_err(|error| format!("failed to read {path}: {error}"))?; + serde_json::from_slice(&data).map_err(|error| format!("failed to parse {path}: {error}")) +} |
