use axum::{ Json, Router, http::StatusCode, response::Html, routing::{get, post}, }; use message_tools::BoxedMessage; use serde::Serialize; use std::{ fs::OpenOptions, io::{BufRead, Read}, path::{Path, PathBuf}, }; use tokio::io::AsyncWriteExt; use tower_http::services::ServeDir; const PUBKEY_PATH: &str = "./pubkeys/public_keys.json"; const MESSAGE_STORAGE: &str = "./messages"; lazy_static::lazy_static! { static ref PUBLIC_KEYS: Vec> = load_public_keys(PUBKEY_PATH).expect("static path"); } type Error = Box; type Result = std::result::Result; fn load_public_keys(path: impl AsRef) -> std::io::Result>> { // Open the file let f = OpenOptions::new().read(true).write(false).open(path)?; // Wrap it in a BufReader for efficient line-by-line reading let reader = std::io::BufReader::new(f); let mut public_keys = Vec::new(); // Iterate over each line in the file for line_result in reader.lines() { let line = line_result?; // Handle potential IO errors during reading // Skip empty lines or lines with only whitespace let trimmed_line = line.trim(); if trimmed_line.is_empty() { continue; } // Deserialize the line (which is a single JSON array) into a Vec let key: Vec = serde_json::from_str(trimmed_line).map_err(|e| { std::io::Error::new( std::io::ErrorKind::InvalidData, format!("Failed to parse JSON line: {}", e), ) })?; public_keys.push(key); } Ok(public_keys) } #[tokio::main] async fn main() -> Result<()> { // build our application with a single route let app = Router::new() .route("/", get(serve_path("./routes/root/index.html")?)) .route("/who", get(serve_path("./routes/who/index.html")?)) .route("/api/pubkey", get(async || select_key(&PUBLIC_KEYS))) .route("/contact", get(serve_path("./routes/contact/index.html")?)) .route("/contact/message", get(serve_path("./routes/contact/message/index.html")?)) .route("/api/publish", post(publish_message)) .nest_service("/routes", ServeDir::new("./routes")); let addr = "0.0.0.0:3000"; let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); println!("Server running at http://{addr}"); axum::serve(listener, app).await.unwrap(); Ok(()) } // Define a struct for the response #[derive(Serialize)] struct UserCreated { tag: String, } // FIXME: sanitation of boxed messages async fn publish_message(Json(msg): Json) -> (StatusCode, Json) { let tag = msg.tag(); // Capture tag early // 1. Define the full *file* path: e.g., "MESSAGE_STORAGE/some_tag/.json" let file_path = PathBuf::from(MESSAGE_STORAGE) .join(format!("{tag}.json")); // The file inside that directory // 3. Create all necessary parent directories recursively (async operation) if let Err(e) = tokio::fs::create_dir_all(&MESSAGE_STORAGE).await { eprintln!( "Failed to create directory {}: {}", MESSAGE_STORAGE, e ); return (StatusCode::INTERNAL_SERVER_ERROR, Json(UserCreated { tag })); } // 4. Open the file asynchronously let mut f = match tokio::fs::OpenOptions::new() .create(true) .write(true) .append(true) .open(&file_path) .await { Ok(file) => file, Err(e) => { eprintln!("Failed to open file {}: {}", file_path.display(), e); return (StatusCode::INTERNAL_SERVER_ERROR, Json(UserCreated { tag })); } }; // 5. Serialize and write the data asynchronously // We append a newline character for standard text file formatting let s = serde_json::to_string(&msg).expect("failed to serialize boxed message"); if let Err(e) = f.write_all(format!("{}\n", s).as_bytes()).await { eprintln!("Failed to write message to file: {}", e); return (StatusCode::INTERNAL_SERVER_ERROR, Json(UserCreated { tag })); } (StatusCode::CREATED, Json(UserCreated { tag })) } fn select_key(pks: &Vec>) -> String { let idx = getrandom::u64().expect("random integer") as usize % pks.len(); serde_json::to_string(&pks[idx]).expect("serialize vec of bytes") } fn serve_path(path: impl AsRef) -> Result> { let mut s = String::new(); OpenOptions::new() .read(true) .write(false) .open(path)? .read_to_string(&mut s)?; Ok(Html(s)) }