From f5fac720026c911a39bd89cfd489ae9d077f004e Mon Sep 17 00:00:00 2001 From: syui Date: Wed, 4 Feb 2026 22:31:54 +0900 Subject: [PATCH] fix notify --- src/commands/notify.rs | 88 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 12 ++++++ 2 files changed, 100 insertions(+) diff --git a/src/commands/notify.rs b/src/commands/notify.rs index 21a0026..98fac33 100644 --- a/src/commands/notify.rs +++ b/src/commands/notify.rs @@ -1,5 +1,6 @@ use anyhow::Result; use serde_json::Value; +use std::io::Write; use super::auth; use crate::lexicons::app_bsky_notification; @@ -65,3 +66,90 @@ pub async fn update_seen() -> Result<()> { println!("{}", serde_json::to_string_pretty(&result)?); Ok(()) } + +/// Poll notifications and output new mentions as NDJSON. +/// Runs until interrupted (Ctrl+C). +pub async fn listen(interval_secs: u64, reasons: &[String]) -> Result<()> { + let mut last_seen: Option = None; + let mut stdout = std::io::stdout(); + + loop { + let session = auth::refresh_session().await?; + let pds = session.pds.as_deref().unwrap_or("bsky.social"); + let client = XrpcClient::new(pds); + + let body: Value = client + .query_auth( + &app_bsky_notification::LIST_NOTIFICATIONS, + &[("limit", "50")], + &session.access_jwt, + ) + .await?; + + if let Some(notifications) = body["notifications"].as_array() { + let mut new_items: Vec<&Value> = Vec::new(); + + for notif in notifications { + // Stop at already-seen notifications + if let Some(ref seen) = last_seen { + if notif["indexedAt"].as_str() == Some(seen.as_str()) { + break; + } + } + + // Skip read notifications on first run + if last_seen.is_none() && notif["isRead"].as_bool() == Some(true) { + continue; + } + + // Filter by reason + let reason = notif["reason"].as_str().unwrap_or(""); + if !reasons.is_empty() && !reasons.iter().any(|r| r == reason) { + continue; + } + + new_items.push(notif); + } + + // Output in chronological order (oldest first) + for notif in new_items.iter().rev() { + let out = serde_json::json!({ + "reason": notif["reason"], + "uri": notif["uri"], + "author": { + "did": notif["author"]["did"], + "handle": notif["author"]["handle"], + }, + "text": notif["record"]["text"], + "indexedAt": notif["indexedAt"], + }); + writeln!(stdout, "{}", serde_json::to_string(&out)?)?; + stdout.flush()?; + } + + // Update last_seen to newest notification + if let Some(newest) = notifications.first() { + if let Some(ts) = newest["indexedAt"].as_str() { + last_seen = Some(ts.to_string()); + } + } + + // Mark as seen + if !new_items.is_empty() { + let now = chrono::Utc::now() + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string(); + let seen_body = serde_json::json!({ "seenAt": now }); + let _ = client + .call_no_response( + &app_bsky_notification::UPDATE_SEEN, + &seen_body, + &session.access_jwt, + ) + .await; + } + } + + tokio::time::sleep(std::time::Duration::from_secs(interval_secs)).await; + } +} diff --git a/src/main.rs b/src/main.rs index edfacd2..cbd8b69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -188,6 +188,15 @@ enum NotifyCommands { Count, /// Mark all notifications as seen Seen, + /// Poll for new notifications (runs continuously, NDJSON output) + Listen { + /// Poll interval in seconds + #[arg(short, long, default_value = "30")] + interval: u64, + /// Filter by reason (mention, reply, like, repost, follow, quote) + #[arg(short, long)] + reason: Vec, + }, } #[derive(Subcommand)] @@ -262,6 +271,9 @@ async fn main() -> Result<()> { NotifyCommands::Seen => { commands::notify::update_seen().await?; } + NotifyCommands::Listen { interval, reason } => { + commands::notify::listen(interval, &reason).await?; + } } } Commands::Pds { command } => {