2025-11-17 22:21:49 +00:00
|
|
|
use clap::{Parser, Subcommand};
|
2025-11-18 16:41:58 +00:00
|
|
|
use message_tools::BoxedMessage;
|
|
|
|
|
use std::{
|
|
|
|
|
fs::OpenOptions,
|
|
|
|
|
io::{BufRead, Write},
|
|
|
|
|
os::unix::process::ExitStatusExt,
|
|
|
|
|
process::{Command, ExitStatus},
|
|
|
|
|
};
|
2025-11-18 02:22:05 +00:00
|
|
|
|
|
|
|
|
const WEB_BUILD_DIR: &str = "web-build";
|
2025-11-17 22:21:49 +00:00
|
|
|
|
|
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
#[command(author, version, about, long_about = None)]
|
|
|
|
|
struct Cli {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: Commands,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
|
enum Commands {
|
|
|
|
|
/// Builds the website and server, including wasm files
|
|
|
|
|
Build,
|
|
|
|
|
|
2025-11-18 02:22:05 +00:00
|
|
|
/// Runs the server locally
|
|
|
|
|
Start {
|
|
|
|
|
/// Whether to build again or not
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
rebuild: bool,
|
|
|
|
|
},
|
2025-11-17 22:21:49 +00:00
|
|
|
|
|
|
|
|
/// Generates new encryption keys
|
|
|
|
|
GenKeys {
|
|
|
|
|
/// How many keypairs to generate
|
|
|
|
|
#[arg(short, long, default_value = "100")]
|
2025-11-18 02:22:05 +00:00
|
|
|
number: usize,
|
|
|
|
|
|
|
|
|
|
/// Directory to put the generated secret key files
|
|
|
|
|
#[arg(short, long, default_value = ".local/secrets.json")]
|
|
|
|
|
sk_out_dir: String,
|
|
|
|
|
|
|
|
|
|
/// Directory to put the generated pubklic key pool
|
|
|
|
|
#[arg(long, default_value = "target/public_keys.json")]
|
|
|
|
|
pk_out_dir: String,
|
2025-11-17 22:21:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Reads fetched messages
|
|
|
|
|
ReadMessages {
|
2025-11-18 02:22:05 +00:00
|
|
|
/// Source file path
|
|
|
|
|
#[arg(short, long, default_value = ".local/messages")]
|
|
|
|
|
source: String,
|
2025-11-17 22:21:49 +00:00
|
|
|
|
2025-11-18 16:41:58 +00:00
|
|
|
/// Where to find the secret keys for decrypting boxed messages
|
|
|
|
|
#[arg(long, default_value = ".local/secrets.json")]
|
|
|
|
|
sk_dir: String,
|
|
|
|
|
|
2025-11-18 02:22:05 +00:00
|
|
|
/// Optional output file path for decoded messages
|
|
|
|
|
/// Default: stdout
|
2025-11-17 22:21:49 +00:00
|
|
|
#[arg(short, long)]
|
|
|
|
|
out_dir: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let cli = Cli::parse();
|
|
|
|
|
|
|
|
|
|
match &cli.command {
|
|
|
|
|
Commands::Build => {
|
|
|
|
|
println!("Executing build command...");
|
|
|
|
|
build();
|
|
|
|
|
}
|
2025-11-18 02:22:05 +00:00
|
|
|
|
|
|
|
|
Commands::Start { rebuild } => {
|
|
|
|
|
println!("Starting server...");
|
|
|
|
|
if *rebuild {
|
|
|
|
|
println!("Rebuilding web pages...");
|
|
|
|
|
build();
|
|
|
|
|
} // Change the current working directory of the running Rust program
|
|
|
|
|
println!("Changing directory to {WEB_BUILD_DIR}...");
|
|
|
|
|
let cd_result = std::env::set_current_dir(WEB_BUILD_DIR);
|
|
|
|
|
|
|
|
|
|
// Use the existing helper to check the result
|
|
|
|
|
handle_command_status(
|
|
|
|
|
cd_result.map(|_| std::process::ExitStatus::from_raw(0)),
|
|
|
|
|
"cd build dir",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
println!("Running server executable...");
|
|
|
|
|
// Now run the server executable from within the new current directory
|
|
|
|
|
handle_command_status(Command::new("./server").status(), "run server");
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|
2025-11-18 02:22:05 +00:00
|
|
|
Commands::GenKeys {
|
|
|
|
|
number,
|
|
|
|
|
sk_out_dir,
|
|
|
|
|
pk_out_dir,
|
|
|
|
|
} => {
|
2025-11-17 22:21:49 +00:00
|
|
|
println!("Executing gen-keys command...");
|
2025-11-18 02:22:05 +00:00
|
|
|
message_tools::gen_keys::generate_keys_to_file(pk_out_dir, sk_out_dir, *number)
|
|
|
|
|
.expect("write key files");
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|
2025-11-18 16:41:58 +00:00
|
|
|
Commands::ReadMessages {
|
|
|
|
|
source,
|
|
|
|
|
sk_dir,
|
|
|
|
|
out_dir,
|
|
|
|
|
} => {
|
2025-11-17 22:21:49 +00:00
|
|
|
println!("Executing read-messages command...");
|
2025-11-18 16:41:58 +00:00
|
|
|
let kps = load_kps(sk_dir);
|
|
|
|
|
let msgs = {
|
|
|
|
|
let ms = fetch_all_messages_from_dir(source).expect("failed to read messages");
|
|
|
|
|
|
|
|
|
|
let mut unboxed: Vec<UnboxedMessage> = vec![];
|
|
|
|
|
// for loop go brrr, its less than 1000 keys
|
|
|
|
|
for m in ms {
|
|
|
|
|
if let Some(sk) =
|
|
|
|
|
fetch_sk(&kps, &m.remote_pk().expect("failed to parse pk bytes"))
|
|
|
|
|
{
|
|
|
|
|
let who = m.tag();
|
|
|
|
|
let message =
|
|
|
|
|
message_tools::unbox_message(m, sk).expect("failed to unbox message");
|
|
|
|
|
unboxed.push(UnboxedMessage { who, message });
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!("No secret key found for message {m:?}");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unboxed
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(dir) = out_dir {
|
|
|
|
|
let mut f = OpenOptions::new()
|
|
|
|
|
.create(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.append(true)
|
|
|
|
|
.open(dir)
|
|
|
|
|
.expect("failed to create message output file");
|
|
|
|
|
|
|
|
|
|
for msg in msgs {
|
|
|
|
|
writeln!(f, "{:?}", msg).expect("failed to write message");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
println!("{msgs:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fetch_sk(
|
|
|
|
|
kps: &Vec<KeyPair>,
|
|
|
|
|
pk: &message_tools::PublicKey,
|
|
|
|
|
) -> Option<message_tools::StaticSecret> {
|
|
|
|
|
for kp in kps {
|
|
|
|
|
if &kp.pk == pk {
|
|
|
|
|
return Some(kp.sk.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(dead_code)] // struct for easy display using debug
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct UnboxedMessage {
|
|
|
|
|
who: String,
|
|
|
|
|
message: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct KeyPair {
|
|
|
|
|
pub pk: message_tools::PublicKey,
|
|
|
|
|
pub sk: message_tools::StaticSecret,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load_kps(path: impl AsRef<std::path::Path>) -> Vec<KeyPair> {
|
|
|
|
|
let sks_bytes: Vec<Vec<u8>> = process_json_file(path).expect("failed to parse sk file");
|
|
|
|
|
|
|
|
|
|
sks_bytes
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|sk| {
|
|
|
|
|
let arr: [u8; 32] = sk.try_into().expect("invalid secret");
|
|
|
|
|
message_tools::StaticSecret::from(arr)
|
|
|
|
|
})
|
|
|
|
|
.map(|sk| KeyPair {
|
|
|
|
|
pk: message_tools::PublicKey::from(&sk),
|
|
|
|
|
sk,
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fetch_all_messages_from_dir(
|
|
|
|
|
dir_path: impl AsRef<std::path::Path>,
|
|
|
|
|
) -> std::io::Result<Vec<BoxedMessage>> {
|
|
|
|
|
let mut all_messages: Vec<BoxedMessage> = Vec::new();
|
|
|
|
|
|
|
|
|
|
for entry in std::fs::read_dir(dir_path)? {
|
|
|
|
|
let entry = entry?;
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
|
|
|
|
|
if path.is_file() && path.extension().unwrap_or_default() == "json" {
|
|
|
|
|
let file_messages = process_json_file(&path)?;
|
|
|
|
|
all_messages.extend(file_messages);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(all_messages)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn process_json_file<D: serde::de::DeserializeOwned>(
|
|
|
|
|
file_path: impl AsRef<std::path::Path>,
|
|
|
|
|
) -> std::io::Result<Vec<D>> {
|
|
|
|
|
let f = std::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(false)
|
|
|
|
|
.open(file_path.as_ref())
|
|
|
|
|
.expect(&format!("failed to open file at {:?}", file_path.as_ref()));
|
|
|
|
|
|
|
|
|
|
let reader = std::io::BufReader::new(f);
|
|
|
|
|
let mut ds: Vec<D> = vec![];
|
|
|
|
|
|
|
|
|
|
for line in reader.lines() {
|
|
|
|
|
let line = line.expect("failed to read line");
|
|
|
|
|
let line = line.trim();
|
|
|
|
|
|
|
|
|
|
if line.is_empty() {
|
|
|
|
|
continue;
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|
2025-11-18 16:41:58 +00:00
|
|
|
|
|
|
|
|
let d: D = serde_json::from_str::<D>(line).expect(&format!(
|
|
|
|
|
"failed to parse message from file {:?}",
|
|
|
|
|
file_path.as_ref()
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
ds.push(d);
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|
2025-11-18 16:41:58 +00:00
|
|
|
|
|
|
|
|
Ok(ds)
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 02:22:05 +00:00
|
|
|
// A helper function to process the Result<ExitStatus, io::Error>
|
|
|
|
|
fn handle_command_status(status_result: std::io::Result<ExitStatus>, command_name: &str) {
|
|
|
|
|
match status_result {
|
|
|
|
|
Ok(exit_status) => {
|
|
|
|
|
if !exit_status.success() {
|
|
|
|
|
eprintln!(
|
|
|
|
|
"\nCommand '{}' failed with exit code: {:?}",
|
|
|
|
|
command_name,
|
|
|
|
|
exit_status.code()
|
|
|
|
|
);
|
|
|
|
|
std::process::exit(exit_status.code().unwrap_or(1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("\nFailed to execute command '{}': {}", command_name, e);
|
|
|
|
|
std::process::exit(1); // Exit with an error code
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 22:21:49 +00:00
|
|
|
fn build() {
|
2026-02-06 04:44:23 +00:00
|
|
|
println!("Building wasm package (message-tools)...");
|
2025-11-18 02:22:05 +00:00
|
|
|
// build the wasm package
|
2025-11-17 22:21:49 +00:00
|
|
|
let status = Command::new("wasm-pack")
|
|
|
|
|
.arg("build")
|
|
|
|
|
.arg("--out-dir")
|
|
|
|
|
.arg("../../routes/contact/pkg")
|
|
|
|
|
.arg("crates/message-tools")
|
|
|
|
|
.arg("--target")
|
|
|
|
|
.arg("no-modules")
|
|
|
|
|
.status();
|
2026-02-06 04:44:23 +00:00
|
|
|
handle_command_status(status, "wasm-pack build message-tools");
|
|
|
|
|
|
|
|
|
|
println!("Building wasm package (fractal-engine)...");
|
|
|
|
|
let status = Command::new("wasm-pack")
|
|
|
|
|
.arg("build")
|
|
|
|
|
.arg("--out-dir")
|
|
|
|
|
.arg("../../routes/root/pkg")
|
|
|
|
|
.arg("crates/fractal-engine")
|
|
|
|
|
.arg("--target")
|
|
|
|
|
.arg("no-modules")
|
|
|
|
|
.status();
|
|
|
|
|
handle_command_status(status, "wasm-pack build fractal-engine");
|
2025-11-17 22:21:49 +00:00
|
|
|
|
2025-11-18 02:22:05 +00:00
|
|
|
println!("Building server executable...");
|
|
|
|
|
// build the server executable
|
|
|
|
|
let status = Command::new("cargo")
|
|
|
|
|
.arg("build")
|
|
|
|
|
.arg("--release")
|
|
|
|
|
.args(["-p", "server"])
|
|
|
|
|
.args(["--target", "x86_64-unknown-linux-musl"])
|
|
|
|
|
.status();
|
|
|
|
|
handle_command_status(status, "cargo build server");
|
|
|
|
|
|
|
|
|
|
let server_bin = "target/x86_64-unknown-linux-musl/release/server";
|
|
|
|
|
|
|
|
|
|
println!("Creating build directory...");
|
|
|
|
|
// make webpage build directory if it doesn't already exist
|
|
|
|
|
let status = Command::new("mkdir").args(["-p", WEB_BUILD_DIR]).status();
|
|
|
|
|
handle_command_status(status, "mkdir web-build");
|
|
|
|
|
|
|
|
|
|
println!("Copying files...");
|
2026-02-06 04:44:23 +00:00
|
|
|
// copy relevant files (use trailing slash to merge into existing directory)
|
2025-11-18 02:22:05 +00:00
|
|
|
let status = Command::new("cp")
|
2025-11-18 16:41:58 +00:00
|
|
|
.args(["-R", "-f", "routes"])
|
2026-02-06 04:44:23 +00:00
|
|
|
.arg(WEB_BUILD_DIR)
|
2025-11-18 02:22:05 +00:00
|
|
|
.status();
|
|
|
|
|
handle_command_status(status, "cp routes");
|
|
|
|
|
|
|
|
|
|
let status = Command::new("cp")
|
2025-11-18 16:41:58 +00:00
|
|
|
.arg("-f")
|
2025-11-18 02:22:05 +00:00
|
|
|
.arg(server_bin)
|
|
|
|
|
.arg(format!("{WEB_BUILD_DIR}"))
|
|
|
|
|
.status();
|
|
|
|
|
handle_command_status(status, "cp server bin");
|
|
|
|
|
|
|
|
|
|
let pubkey_file = "target/public_keys.json";
|
|
|
|
|
|
|
|
|
|
if !std::path::Path::new(pubkey_file).exists() {
|
|
|
|
|
panic!("No public key file. Please generate keys first.");
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|
2025-11-18 02:22:05 +00:00
|
|
|
|
|
|
|
|
let status = Command::new("mkdir")
|
|
|
|
|
.args(["-p", &format!("{WEB_BUILD_DIR}/pubkeys")])
|
|
|
|
|
.status();
|
|
|
|
|
handle_command_status(status, "mkdir web-build");
|
|
|
|
|
|
|
|
|
|
let status = Command::new("cp")
|
2025-11-18 16:41:58 +00:00
|
|
|
.arg("-f")
|
2025-11-18 02:22:05 +00:00
|
|
|
.arg("target/public_keys.json")
|
|
|
|
|
.arg(format!("{WEB_BUILD_DIR}/pubkeys/"))
|
|
|
|
|
.status();
|
|
|
|
|
handle_command_status(status, "cp pubkeys");
|
|
|
|
|
|
|
|
|
|
println!("\nBuild successful!");
|
2025-11-17 22:21:49 +00:00
|
|
|
}
|