chat-store/src/main.rs

138 lines
4.4 KiB
Rust
Raw Normal View History

//! Testnet KeyPackage Registry service.
//!
//! Throwaway service for issue #110 — replaced by λLEZ in v0.3. No libchat-core
//! dependency; the embedded logos-delivery node comes from libchat's transport
//! crate, pulled in as a pinned git dependency.
//!
//! Submissions arrive on either write path — both feed the same verification +
//! storage pipeline (`bundle`):
//! - HTTP POST (below), synchronous and acknowledged;
//! - logos-delivery subscription (`delivery`, disable with `--no-delivery`):
//! clients publish protobuf submissions on the store content topics.
//!
//! HTTP wire:
//! POST /v0/keypackage — submit a signed keypackage bundle
//! GET /v0/keypackage/{device_id} — fetch the latest stored keypackage bundle
//! POST /v0/account — upsert a signed account device-list bundle
//! GET /v0/account/{account_pub} — fetch the account device-list bundle
mod bundle;
mod delivery;
mod handlers;
mod store;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::Parser;
use tracing_subscriber::EnvFilter;
use store::Store;
#[derive(Parser, Debug)]
#[command(
name = "chat-store",
about = "Testnet Chat Store (KeyPackage + account directory)"
)]
struct Cli {
/// Address to bind the HTTP server.
#[arg(long, default_value = "0.0.0.0:8080")]
bind: SocketAddr,
/// SQLite database path.
#[arg(long, default_value = "chat-store.db")]
db: PathBuf,
/// Maximum number of keypackage bundles retained per device_id.
#[arg(long, default_value_t = 100)]
max_per_identity: usize,
/// Retention window in days; older bundles are pruned.
#[arg(long, default_value_t = 30)]
retention_days: u64,
/// How often the prune task runs.
#[arg(long, default_value_t = 3600)]
prune_interval_secs: u64,
/// Disable the logos-delivery subscriber; submissions then arrive over
/// HTTP POST only.
#[arg(long)]
no_delivery: bool,
/// logos-delivery network preset the subscriber joins.
#[arg(long, default_value = "logos.dev")]
preset: String,
/// TCP + discv5 UDP port for the embedded logos-delivery node
/// (0 = OS-assigned).
#[arg(long, default_value_t = 0)]
p2p_port: u16,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
let store = Arc::new(Store::open(&cli.db).await.context("failed to open store")?);
let prune_store = store.clone();
let max_per_id = cli.max_per_identity;
let retention = Duration::from_secs(cli.retention_days * 24 * 3600);
let interval = Duration::from_secs(cli.prune_interval_secs);
tokio::spawn(async move {
let mut ticker = tokio::time::interval(interval);
loop {
ticker.tick().await;
if let Err(e) = prune_store.prune_key_packages(max_per_id, retention).await {
tracing::warn!("prune (keypackages) failed: {e}");
}
if let Err(e) = prune_store.prune_accounts(retention).await {
tracing::warn!("prune (accounts) failed: {e}");
}
}
});
if !cli.no_delivery {
// Blocks for a few seconds while the node starts and finds peers;
// deliberately before serving HTTP so a subscriber failure is a
// startup error, not a silent half-running store.
delivery::start(
store.clone(),
delivery::P2pConfig {
preset: cli.preset.clone(),
port: cli.p2p_port,
log_level: "ERROR".into(),
},
tokio::runtime::Handle::current(),
)
.context("failed to start logos-delivery ingestion")?;
tracing::info!("logos-delivery ingestion running (preset={})", cli.preset);
}
let app = handlers::router(store);
let listener = tokio::net::TcpListener::bind(cli.bind)
.await
.with_context(|| format!("failed to bind {}", cli.bind))?;
tracing::info!("chat-store listening on {}", cli.bind);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.context("server error")?;
Ok(())
}
async fn shutdown_signal() {
let _ = tokio::signal::ctrl_c().await;
tracing::info!("shutdown signal received");
}