//! HTTP client for talking to warzone-server. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use warzone_protocol::prekey::PreKeyBundle; #[derive(Clone)] pub struct ServerClient { pub base_url: String, pub client: reqwest::Client, } #[derive(Serialize)] struct RegisterRequest { fingerprint: String, bundle: Vec, #[serde(skip_serializing_if = "Option::is_none")] eth_address: Option, } #[derive(Serialize)] struct SendRequest { to: String, from: Option, message: Vec, } #[derive(Deserialize)] #[allow(dead_code)] struct BundleResponse { fingerprint: String, bundle: String, // base64 } impl ServerClient { pub fn new(base_url: &str) -> Self { ServerClient { base_url: base_url.trim_end_matches('/').to_string(), client: reqwest::Client::new(), } } /// Register our pre-key bundle with the server. pub async fn register_bundle( &self, fingerprint: &str, bundle: &PreKeyBundle, eth_address: Option, ) -> Result<()> { let encoded = bincode::serialize(bundle).context("failed to serialize bundle")?; self.client .post(format!("{}/v1/keys/register", self.base_url)) .json(&RegisterRequest { fingerprint: fingerprint.to_string(), bundle: encoded, eth_address, }) .send() .await .context("failed to register bundle")?; Ok(()) } /// Fetch a user's pre-key bundle from the server. pub async fn fetch_bundle(&self, fingerprint: &str) -> Result { let fp_clean: String = fingerprint.chars().filter(|c| c.is_ascii_hexdigit()).collect(); let response = self .client .get(format!( "{}/v1/keys/{}", self.base_url, fp_clean )) .send() .await .context("failed to fetch bundle")?; if !response.status().is_success() { anyhow::bail!( "server returned {} — user {} may not be registered", response.status(), fingerprint ); } let resp: BundleResponse = response .json() .await .context("failed to parse bundle response")?; let bytes = base64::Engine::decode( &base64::engine::general_purpose::STANDARD, &resp.bundle, ) .context("failed to decode base64 bundle")?; bincode::deserialize(&bytes).context("failed to deserialize bundle") } /// Send an encrypted message to the server for delivery. pub async fn send_message(&self, to: &str, from: Option<&str>, message: &[u8]) -> Result<()> { let to_clean: String = to.chars().filter(|c| c.is_ascii_hexdigit()).collect(); self.client .post(format!("{}/v1/messages/send", self.base_url)) .json(&SendRequest { to: to_clean, from: from.map(|f| f.chars().filter(|c| c.is_ascii_hexdigit()).collect()), message: message.to_vec(), }) .send() .await .context("failed to send message")?; Ok(()) } /// Check how many one-time pre-keys remain on the server. pub async fn otpk_count(&self, fingerprint: &str) -> Result { let fp_clean: String = fingerprint.chars().filter(|c| c.is_ascii_hexdigit()).collect(); let resp: serde_json::Value = self.client .get(format!("{}/v1/keys/{}/otpk-count", self.base_url, fp_clean)) .send() .await .context("failed to check OTPK count")? .json() .await .context("failed to parse OTPK count")?; Ok(resp.get("count").and_then(|v| v.as_u64()).unwrap_or(0)) } /// Upload additional one-time pre-keys. pub async fn replenish_otpks(&self, fingerprint: &str, keys: Vec<(u32, [u8; 32])>) -> Result<()> { let fp_clean: String = fingerprint.chars().filter(|c| c.is_ascii_hexdigit()).collect(); let otpks: Vec = keys.iter().map(|(id, pubkey)| { serde_json::json!({"id": id, "public_key": hex::encode(pubkey)}) }).collect(); self.client .post(format!("{}/v1/keys/replenish", self.base_url)) .json(&serde_json::json!({"fingerprint": fp_clean, "one_time_pre_keys": otpks})) .send() .await .context("failed to replenish OTPKs")?; Ok(()) } /// Poll for messages addressed to us. pub async fn poll_messages(&self, fingerprint: &str) -> Result>> { let fp_clean: String = fingerprint.chars().filter(|c| c.is_ascii_hexdigit()).collect(); let resp: Vec = self .client .get(format!( "{}/v1/messages/poll/{}", self.base_url, fp_clean )) .send() .await .context("failed to poll messages")? .json() .await .context("failed to parse poll response")?; let mut messages = Vec::new(); for b64 in resp { if let Ok(bytes) = base64::Engine::decode( &base64::engine::general_purpose::STANDARD, &b64, ) { messages.push(bytes); } } Ok(messages) } }