enigma_node_client/
client.rs1use 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}