use std::fs::OpenOptions;
use std::io::{BufRead as _, BufReader, Write as _};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seq: Option<u64>,
pub ts: i64,
pub direction: String,
pub counterparty_pubkey: String,
pub counterparty_handle: Option<String>,
pub kind: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub saved_to: Option<String>,
}
pub fn log_dir(home: &Path) -> PathBuf {
home.join("messages")
}
fn channel_log_path(home: &Path, channel_id_b64url: &str) -> PathBuf {
log_dir(home).join(format!("{channel_id_b64url}.jsonl"))
}
pub fn append(home: &Path, channel_id_b64url: &str, entry: &LogEntry) -> Result<()> {
let dir = log_dir(home);
std::fs::create_dir_all(&dir).with_context(|| format!("mkdir {}", dir.display()))?;
let path = channel_log_path(home, channel_id_b64url);
let line = serde_json::to_string(entry)?;
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("open {}", path.display()))?;
writeln!(f, "{line}").with_context(|| format!("write {}", path.display()))?;
Ok(())
}
pub fn read(home: &Path, channel_id_b64url: &str) -> Result<Vec<LogEntry>> {
let path = channel_log_path(home, channel_id_b64url);
if !path.exists() {
return Ok(Vec::new());
}
let f = std::fs::File::open(&path).with_context(|| format!("open {}", path.display()))?;
let mut out = Vec::new();
for line in BufReader::new(f).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let entry: LogEntry =
serde_json::from_str(&line).with_context(|| format!("parse {}", path.display()))?;
out.push(entry);
}
Ok(out)
}
pub fn read_all(home: &Path) -> Result<Vec<LogEntry>> {
let dir = log_dir(home);
if !dir.exists() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for entry in std::fs::read_dir(&dir)? {
let p = entry?.path();
if p.extension().and_then(|s| s.to_str()) != Some("jsonl") {
continue;
}
let f = std::fs::File::open(&p)?;
for line in BufReader::new(f).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if let Ok(e) = serde_json::from_str::<LogEntry>(&line) {
out.push(e);
}
}
}
out.sort_by_key(|e| e.ts);
Ok(out)
}
pub fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
.unwrap_or(0)
}