use crate::api::MyceliumClient; use crate::error::{AppError, AppResult}; use serde::{Deserialize, Serialize}; /// Destination of an outgoing message: either a fully resolved overlay IPv6 /// or the recipient's public key. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum MessageDestination { #[serde(rename = "ip")] Ip(String), #[serde(rename = "pk")] PublicKey(String), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PushMessageBody { pub dst: MessageDestination, /// base64-encoded topic bytes (≤ 340 chars). pub topic: String, /// base64-encoded payload bytes. pub payload: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PushMessageReceipt { /// 16-char hex id assigned by the daemon. pub id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IncomingMessage { pub id: String, pub src_ip: String, pub src_pk: String, pub dst_ip: String, pub dst_pk: String, pub topic: String, pub payload: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageStatus { /// Pass-through of the daemon's response. We deliberately keep it as a /// JSON Value because the upstream schema isn't pinned in the spec and /// fields can be added between releases. #[serde(flatten)] pub raw: serde_json::Value, } impl MyceliumClient { pub async fn send_message(&self, body: &PushMessageBody) -> AppResult { let resp = self .http() .post(self.url("/messages")) .json(body) .send() .await?; Self::parse(resp).await } /// Long-poll the daemon for an incoming message. `timeout` is seconds and /// must be ≥ 0; the daemon returns 204/empty when nothing arrives within /// the window. Caller is responsible for swallowing the resulting Err. pub async fn pop_message( &self, peek: bool, timeout: u64, topic: Option<&str>, ) -> AppResult> { let mut req = self.http().get(self.url("/messages")); // The daemon expects query params; we hand-build to avoid url crate. let mut q: Vec<(&str, String)> = vec![("peek", peek.to_string()), ("timeout", timeout.to_string())]; if let Some(t) = topic { q.push(("topic", t.to_string())); } req = req.query(&q); // The long-poll can run nearly as long as `timeout`; loosen the // client-default request timeout for this single call. req = req.timeout(std::time::Duration::from_secs( timeout.saturating_add(5).max(15), )); let resp = req.send().await?; let status = resp.status(); if status == reqwest::StatusCode::NO_CONTENT { return Ok(None); } if !status.is_success() { let body = resp.text().await.unwrap_or_default(); return Err(AppError::DaemonStatus { status: status.as_u16(), body, }); } // Some implementations also return 200 with empty body to signal // "nothing to read". Try to parse and tolerate empty. let bytes = resp.bytes().await?; if bytes.is_empty() { return Ok(None); } let msg: IncomingMessage = serde_json::from_slice(&bytes) .map_err(|e| AppError::Other(format!("failed to parse incoming message: {e}")))?; Ok(Some(msg)) } pub async fn reply_message( &self, id: &str, body: &PushMessageBody, ) -> AppResult { let resp = self .http() .post(self.url(&format!("/messages/reply/{id}"))) .json(body) .send() .await?; Self::parse(resp).await } pub async fn message_status(&self, id: &str) -> AppResult { let resp = self .http() .get(self.url(&format!("/messages/status/{id}"))) .send() .await?; Self::parse(resp).await } }