aboutsummaryrefslogtreecommitdiff
path: root/src/bin/delete-recent.rs
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2026-10-25 09:08:01 +0100
committerMartin Fischer <martin@push-f.com>2026-10-25 19:10:47 +0100
commit41212a7a15198e53d205252a02069a7dd6ddadeb (patch)
tree38a24eebf956b2453675ac1cefec925faf09de02 /src/bin/delete-recent.rs
initial commit
Diffstat (limited to 'src/bin/delete-recent.rs')
-rw-r--r--src/bin/delete-recent.rs128
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}"))
+}