enigma_node_client/
client.rs

1use std::time::Duration;
2
3use enigma_node_types::{CheckUserResponse, NodesPayload, Presence, RegisterRequest, RegisterResponse, ResolveResponse, SyncRequest, SyncResponse, UserId};
4use reqwest::Response;
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7
8use crate::config::NodeClientConfig;
9use crate::error::{EnigmaNodeClientError, Result};
10use crate::urls;
11
12pub struct NodeClient {
13    base_url: String,
14    http: reqwest::Client,
15    cfg: NodeClientConfig,
16}
17
18impl NodeClient {
19    pub fn new(base_url: impl Into<String>, cfg: NodeClientConfig) -> Result<NodeClient> {
20        if cfg.timeout_ms == 0 {
21            return Err(EnigmaNodeClientError::InvalidInput("timeout_ms"));
22        }
23        if cfg.connect_timeout_ms == 0 {
24            return Err(EnigmaNodeClientError::InvalidInput("connect_timeout_ms"));
25        }
26        if cfg.max_response_bytes == 0 {
27            return Err(EnigmaNodeClientError::InvalidInput("max_response_bytes"));
28        }
29        if cfg.user_agent.trim().is_empty() {
30            return Err(EnigmaNodeClientError::InvalidInput("user_agent"));
31        }
32        let base_raw: String = base_url.into();
33        let base = urls::validated_base(base_raw.as_str())?;
34        let http = reqwest::Client::builder()
35            .user_agent(cfg.user_agent.clone())
36            .timeout(Duration::from_millis(cfg.timeout_ms))
37            .connect_timeout(Duration::from_millis(cfg.connect_timeout_ms))
38            .build()?;
39        Ok(NodeClient {
40            base_url: base,
41            http,
42            cfg,
43        })
44    }
45
46    pub fn base_url(&self) -> &str {
47        &self.base_url
48    }
49
50    pub async fn register(&self, req: RegisterRequest) -> Result<RegisterResponse> {
51        req.identity
52            .validate()
53            .map_err(|_| EnigmaNodeClientError::InvalidInput("identity"))?;
54        let url = urls::register(&self.base_url)?;
55        self.post_json(url, req).await
56    }
57
58    pub async fn resolve(&self, user_id_hex: &str) -> Result<ResolveResponse> {
59        let validated_hex = validate_user_id_hex(user_id_hex)?;
60        let url = urls::resolve(&self.base_url, &validated_hex)?;
61        self.get_json(url).await
62    }
63
64    pub async fn check_user(&self, user_id_hex: &str) -> Result<CheckUserResponse> {
65        let validated_hex = validate_user_id_hex(user_id_hex)?;
66        let url = urls::check_user(&self.base_url, &validated_hex)?;
67        self.get_json(url).await
68    }
69
70    pub async fn announce(&self, presence: Presence) -> Result<serde_json::Value> {
71        presence
72            .validate()
73            .map_err(|_| EnigmaNodeClientError::InvalidInput("presence"))?;
74        let url = urls::announce(&self.base_url)?;
75        self.post_value(url, presence).await
76    }
77
78    pub async fn sync(&self, req: SyncRequest) -> Result<SyncResponse> {
79        for identity in &req.identities {
80            identity
81                .validate()
82                .map_err(|_| EnigmaNodeClientError::InvalidInput("identities"))?;
83        }
84        let url = urls::sync(&self.base_url)?;
85        self.post_json(url, req).await
86    }
87
88    pub async fn nodes(&self) -> Result<NodesPayload> {
89        let url = urls::nodes_get(&self.base_url)?;
90        self.get_json(url).await
91    }
92
93    pub async fn add_nodes(&self, payload: NodesPayload) -> Result<serde_json::Value> {
94        payload
95            .validate()
96            .map_err(|_| EnigmaNodeClientError::InvalidInput("nodes"))?;
97        let url = urls::nodes_post(&self.base_url)?;
98        self.post_value(url, payload).await
99    }
100
101    async fn get_json<T: DeserializeOwned>(&self, url: String) -> Result<T> {
102        let resp = self.http.get(url).send().await?;
103        self.handle_json_response(resp).await
104    }
105
106    async fn post_json<TReq: Serialize, TResp: DeserializeOwned>(
107        &self,
108        url: String,
109        payload: TReq,
110    ) -> Result<TResp> {
111        let resp = self.http.post(url).json(&payload).send().await?;
112        self.handle_json_response(resp).await
113    }
114
115    async fn post_value<TReq: Serialize>(&self, url: String, payload: TReq) -> Result<serde_json::Value> {
116        let resp = self.http.post(url).json(&payload).send().await?;
117        self.handle_value_response(resp).await
118    }
119
120    async fn handle_json_response<T: DeserializeOwned>(&self, resp: Response) -> Result<T> {
121        let status = resp.status();
122        if !status.is_success() {
123            return Err(EnigmaNodeClientError::Status(status.as_u16()));
124        }
125        let body = resp.bytes().await?;
126        if body.len() > self.cfg.max_response_bytes {
127            return Err(EnigmaNodeClientError::ResponseTooLarge);
128        }
129        Ok(serde_json::from_slice(&body)?)
130    }
131
132    async fn handle_value_response(&self, resp: Response) -> Result<serde_json::Value> {
133        let status = resp.status();
134        if !status.is_success() {
135            return Err(EnigmaNodeClientError::Status(status.as_u16()));
136        }
137        let body = resp.bytes().await?;
138        if body.len() > self.cfg.max_response_bytes {
139            return Err(EnigmaNodeClientError::ResponseTooLarge);
140        }
141        Ok(serde_json::from_slice(&body)?)
142    }
143}
144
145fn validate_user_id_hex(user_id_hex: &str) -> Result<String> {
146    let trimmed = user_id_hex.trim();
147    if trimmed.len() != 64 {
148        return Err(EnigmaNodeClientError::InvalidUserIdHex);
149    }
150    UserId::from_hex(trimmed)
151        .map(|id| id.to_hex())
152        .map_err(|_| EnigmaNodeClientError::InvalidUserIdHex)
153}