use nostr::{Keys, ToBech32};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tempfile::TempDir;
struct CrosslangServer {
_data_dir: TempDir,
process: Child,
#[allow(dead_code)]
pubkey_hex: String,
}
impl CrosslangServer {
fn new(
port: u16,
htree_bin: &str,
keys: &Keys,
test_content: &[u8],
follow_pubkeys: &[String],
) -> Self {
let data_dir = TempDir::new().expect("Failed to create temp dir");
let data_path = data_dir.path();
let home_dir = data_dir.path();
let config_dir = home_dir.join(".hashtree");
std::fs::create_dir_all(&config_dir).expect("Failed to create config dir");
let relay_url =
std::env::var("LOCAL_RELAY").unwrap_or_else(|_| "wss://temp.iris.to".to_string());
println!("CROSSLANG_RELAY:{}", relay_url);
println!("CROSSLANG_CONFIG_DIR:{}", config_dir.display());
let config_content = format!(
r#"
[server]
enable_auth = false
stun_port = 0
[nostr]
relays = ["{}"]
social_graph_crawl_depth = 0
"#,
relay_url
);
let config_path = config_dir.join("config.toml");
std::fs::write(&config_path, &config_content).expect("Failed to write config");
println!("CROSSLANG_CONFIG_WRITTEN:{}", config_path.display());
println!(
"CROSSLANG_CONFIG_CONTENT:{}",
config_content.replace('\n', " ")
);
let nsec = keys
.secret_key()
.to_bech32()
.expect("Failed to encode nsec");
std::fs::write(config_dir.join("keys"), &nsec).expect("Failed to write keys");
if !follow_pubkeys.is_empty() {
let contacts_json =
serde_json::to_string(&follow_pubkeys).expect("Failed to serialize contacts");
std::fs::write(data_path.join("contacts.json"), &contacts_json)
.expect("Failed to write contacts.json");
println!("Following pubkeys: {:?}", follow_pubkeys);
}
let pubkey_hex = keys.public_key().to_hex();
let content_file = data_path.join("test-content.txt");
std::fs::write(&content_file, test_content).expect("Failed to write test content");
let process = Command::new(htree_bin)
.arg("--data-dir")
.arg(data_path)
.arg("start")
.arg("--addr")
.arg(format!("127.0.0.1:{}", port))
.arg("--relays")
.arg(&relay_url)
.env("HOME", home_dir)
.env(
"RUST_LOG",
std::env::var("RUST_LOG")
.unwrap_or_else(|_| "warn,hashtree_cli::webrtc=info".to_string()),
)
.stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .spawn()
.expect("Failed to start htree server");
CrosslangServer {
_data_dir: data_dir,
process,
pubkey_hex,
}
}
}
impl Drop for CrosslangServer {
fn drop(&mut self) {
let _ = self.process.kill();
let _ = self.process.wait();
}
}
fn find_htree_binary() -> PathBuf {
if let Some(bin) = std::env::var_os("CARGO_BIN_EXE_htree") {
let path = PathBuf::from(bin);
if path.exists() {
return path;
}
}
if let Ok(current_exe) = std::env::current_exe() {
if let Some(profile_dir) = current_exe.parent().and_then(|deps| deps.parent()) {
let profile_bin = profile_dir.join("htree");
if profile_bin.exists() {
return profile_bin;
}
}
}
if let Some(target_dir) = std::env::var_os("CARGO_TARGET_DIR") {
let target_dir = PathBuf::from(target_dir);
let debug_bin = target_dir.join("debug/htree");
let release_bin = target_dir.join("release/htree");
if debug_bin.exists() {
return debug_bin;
}
if release_bin.exists() {
return release_bin;
}
}
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = PathBuf::from(manifest_dir)
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let debug_bin = workspace_root.join("target/debug/htree");
let release_bin = workspace_root.join("target/release/htree");
if debug_bin.exists() {
debug_bin
} else if release_bin.exists() {
release_bin
} else {
panic!(
"htree binary not found. Run `cargo build --bin htree` first.\n\
Looked in:\n - {:?}\n - {:?}",
debug_bin, release_bin
);
}
}
#[test]
#[ignore = "long-running network test (120s) - run manually with --ignored"]
fn test_crosslang_peer() {
let htree_bin = find_htree_binary();
let htree_bin_str = htree_bin.to_str().unwrap();
let keys = if let Ok(secret_hex) = std::env::var("CROSSLANG_SECRET_KEY") {
Keys::parse(&secret_hex).expect("Failed to parse CROSSLANG_SECRET_KEY")
} else {
Keys::generate()
};
let pubkey_hex = keys.public_key().to_hex();
let npub = keys
.public_key()
.to_bech32()
.expect("Failed to encode npub");
let test_content = b"Hello from rust! This content was synced cross-language.";
let follow_pubkeys: Vec<String> = std::env::var("CROSSLANG_FOLLOW_PUBKEY")
.ok()
.map(|pk| vec![pk])
.unwrap_or_default();
println!("\n=== Cross-Language Peer Test ===");
println!("CROSSLANG_NPUB:{}", npub);
println!("CROSSLANG_PUBKEY:{}", pubkey_hex);
if !follow_pubkeys.is_empty() {
println!("CROSSLANG_FOLLOWING:{}", follow_pubkeys[0]);
}
let port: u16 = std::env::var("CROSSLANG_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(18090);
println!("CROSSLANG_PORT:{}", port);
let server = CrosslangServer::new(port, htree_bin_str, &keys, test_content, &follow_pubkeys);
println!("Server started with pubkey: {}", &server.pubkey_hex[..16]);
std::thread::sleep(Duration::from_secs(3));
let upload_output = Command::new("curl")
.arg("-s")
.arg("-X")
.arg("POST")
.arg("-F")
.arg(format!(
"file=@{}",
server._data_dir.path().join("test-content.txt").display()
))
.arg(format!("http://127.0.0.1:{}/upload", port))
.output()
.expect("Failed to upload file");
let upload_stdout = String::from_utf8_lossy(&upload_output.stdout);
println!("Upload response: {}", upload_stdout);
let hash = upload_stdout
.split('"')
.find(|s| s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()))
.map(|s| s.to_string());
let hash = match hash {
Some(h) => h,
None => {
println!("Could not extract hash from upload response");
panic!("Upload failed: {}", upload_stdout);
}
};
println!("CROSSLANG_HASH:{}", hash);
let pin_output = Command::new("curl")
.arg("-s")
.arg("-X")
.arg("POST")
.arg(format!("http://127.0.0.1:{}/api/pin/{}", port, hash))
.output()
.expect("Failed to pin");
println!(
"Pin response: {}",
String::from_utf8_lossy(&pin_output.stdout)
);
println!("CROSSLANG_READY");
println!("\nServer running at http://127.0.0.1:{}", port);
println!("Waiting for cross-language peer connections...\n");
let start = std::time::Instant::now();
let timeout = Duration::from_secs(120);
while start.elapsed() < timeout {
std::thread::sleep(Duration::from_secs(5));
let peers_output = Command::new("curl")
.arg("-s")
.arg(format!("http://127.0.0.1:{}/api/peers", port))
.output();
if let Ok(output) = peers_output {
let peers_json = String::from_utf8_lossy(&output.stdout);
if peers_json.contains("\"pubkey\"") {
println!("Peers: {}", peers_json);
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&peers_json) {
if let Some(peers) = json.get("peers").and_then(|p| p.as_array()) {
for peer in peers {
if let Some(pk) = peer.get("pubkey").and_then(|p| p.as_str()) {
let has_dc = peer
.get("has_data_channel")
.and_then(|d| d.as_bool())
.unwrap_or(false);
if has_dc {
println!("CROSSLANG_CONNECTED:{}", pk);
}
}
}
}
}
}
}
println!(" {} seconds elapsed...", start.elapsed().as_secs());
}
println!("\n=== Cross-Language Peer Test Complete ===");
}