use std::sync::atomic::{AtomicBool, Ordering};
const KEY_PREFIX: &str = "self_destruct:";
const SWEEP_MAX_SECS: u64 = 8;
pub fn chat_duration_secs(chat_id: &str) -> Option<u64> {
crate::db::settings::get_sql_setting(format!("{KEY_PREFIX}{chat_id}"))
.ok()
.flatten()
.and_then(|v| v.parse::<u64>().ok())
.filter(|d| *d > 0)
}
pub fn set_chat_duration_secs(chat_id: &str, secs: Option<u64>) -> Result<(), String> {
let key = format!("{KEY_PREFIX}{chat_id}");
match secs {
Some(d) if d > 0 => crate::db::settings::set_sql_setting(key, d.to_string()),
_ => crate::db::settings::remove_setting(&key),
}
}
pub fn resolve_send_expiry(chat_id: &str) -> Option<u64> {
let duration = chat_duration_secs(chat_id)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
Some(now + duration)
}
struct SweepGuard;
impl Drop for SweepGuard {
fn drop(&mut self) {
SWEEP_RUNNING.store(false, Ordering::Release);
}
}
static SWEEP_RUNNING: AtomicBool = AtomicBool::new(false);
pub async fn sweep_expired() -> Option<u64> {
if SWEEP_RUNNING.swap(true, Ordering::AcqRel) {
return None; }
let _guard = SweepGuard;
let session = crate::state::SessionGuard::capture();
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(_) => return None,
};
let mut soonest: Option<u64> = None;
let expired_ids: Vec<String> = {
let state = crate::state::STATE.lock().await;
let mut ids = Vec::new();
for chat in &state.chats {
for msg in chat.messages.iter() {
let exp = msg.expiration_secs;
if exp == 0 {
continue;
}
let exp = exp as u64;
if exp <= now {
ids.push(msg.id_hex());
} else {
soonest = Some(soonest.map_or(exp, |s| s.min(exp)));
}
}
}
ids
};
if !session.is_valid() {
return None;
}
if expired_ids.is_empty() {
return soonest;
}
let client = crate::state::nostr_client();
for id in expired_ids {
let removed = {
let mut state = crate::state::STATE.lock().await;
state.remove_message(&id)
};
let (chat_id, msg) = match removed {
Some(pair) => pair,
None => continue, };
if !msg.attachments.is_empty() {
let mine = msg.mine;
let unique = crate::deletion::filter_unreferenced_attachments(&id, msg.attachments).await;
crate::deletion::delete_cached_attachment_files_pub(&unique);
if mine {
let urls: Vec<String> = unique
.iter()
.map(|a| a.url.to_string())
.filter(|u| !u.is_empty())
.collect();
if !urls.is_empty() {
if let Some(ref client) = client {
if let Ok(signer) = client.signer().await {
crate::blossom::delete_blobs_best_effort(signer, urls);
}
}
}
}
}
let _ = crate::db::events::delete_event(&id).await;
crate::traits::emit_event(
"message_removed",
&serde_json::json!({ "id": id, "chat_id": chat_id, "reason": "self-destruct" }),
);
}
soonest
}
fn next_sweep_delay(soonest: Option<u64>) -> std::time::Duration {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let secs = match soonest {
Some(exp) => exp.saturating_sub(now).clamp(1, SWEEP_MAX_SECS),
None => SWEEP_MAX_SECS,
};
std::time::Duration::from_secs(secs)
}
pub async fn run_sweeper_loop() {
loop {
let soonest = sweep_expired().await;
tokio::time::sleep(next_sweep_delay(soonest)).await;
}
}
pub fn start_sweeper() {
static STARTED: AtomicBool = AtomicBool::new(false);
if STARTED.swap(true, Ordering::AcqRel) {
return;
}
tokio::spawn(run_sweeper_loop());
}