1use crate::{Client, GifList, Result, models::*};
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use uuid::Uuid;
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
10#[serde(tag = "operation", rename_all = "snake_case")]
11pub enum Operation {
12 Me,
13 ListConversations {
14 #[serde(default)]
15 page: PageRequest,
16 },
17 CreateConversation {
18 participant_ids: Vec<String>,
19 idempotency_key: String,
20 },
21 ListMessages {
22 conversation_id: Uuid,
23 #[serde(default)]
24 page: PageRequest,
25 #[serde(default)]
26 include_bundled_members: bool,
27 },
28 GetMessage {
29 conversation_id: Uuid,
30 message_id: Uuid,
31 },
32 SendMessage {
33 conversation_id: Uuid,
34 message: MessageCreate,
35 idempotency_key: String,
36 },
37 EditMessage {
38 conversation_id: Uuid,
39 message_id: Uuid,
40 message: MessageCreate,
41 version: i64,
42 idempotency_key: String,
43 },
44 DeleteMessage {
45 conversation_id: Uuid,
46 message_id: Uuid,
47 version: i64,
48 idempotency_key: String,
49 },
50 Receipt {
51 conversation_id: Uuid,
52 message_id: Uuid,
53 status: ReceiptStatus,
54 device_id: String,
55 },
56 GetDraft {
57 conversation_id: Uuid,
58 },
59 PutDraft {
60 conversation_id: Uuid,
61 draft: DraftInput,
62 version: i64,
63 },
64 DeleteDraft {
65 conversation_id: Uuid,
66 },
67 CreateBundle {
68 conversation_id: Uuid,
69 bundle: BundleCreate,
70 idempotency_key: String,
71 },
72 GetBundle {
73 conversation_id: Uuid,
74 bundle_id: Uuid,
75 },
76 GetPresence {
77 actor_id: String,
78 },
79 SetPresence {
81 activity: Option<Activity>,
82 },
83 TrendingGifs,
84 SearchGifs {
85 query: String,
86 },
87 RecentGifs,
88}
89impl Operation {
90 pub fn is_mutation(&self) -> bool {
92 matches!(
93 self,
94 Self::CreateConversation { .. }
95 | Self::SendMessage { .. }
96 | Self::EditMessage { .. }
97 | Self::DeleteMessage { .. }
98 | Self::Receipt { .. }
99 | Self::PutDraft { .. }
100 | Self::DeleteDraft { .. }
101 | Self::CreateBundle { .. }
102 | Self::SetPresence { .. }
103 )
104 }
105 pub async fn execute(&self, client: &Client) -> Result<Value> {
107 Ok(match self {
108 Self::Me => serde_json::to_value(client.me().await?)?,
109 Self::ListConversations { page } => {
110 serde_json::to_value(client.conversations(page).await?)?
111 }
112 Self::CreateConversation {
113 participant_ids,
114 idempotency_key,
115 } => serde_json::to_value(
116 client
117 .create_conversation(participant_ids, idempotency_key)
118 .await?,
119 )?,
120 Self::ListMessages {
121 conversation_id,
122 page,
123 include_bundled_members,
124 } => serde_json::to_value(
125 client
126 .messages(*conversation_id, page, *include_bundled_members)
127 .await?,
128 )?,
129 Self::GetMessage {
130 conversation_id,
131 message_id,
132 } => serde_json::to_value(client.message(*conversation_id, *message_id).await?)?,
133 Self::SendMessage {
134 conversation_id,
135 message,
136 idempotency_key,
137 } => serde_json::to_value(
138 client
139 .send_message(*conversation_id, message, idempotency_key)
140 .await?,
141 )?,
142 Self::EditMessage {
143 conversation_id,
144 message_id,
145 message,
146 version,
147 idempotency_key,
148 } => serde_json::to_value(
149 client
150 .edit_message(
151 *conversation_id,
152 *message_id,
153 message,
154 *version,
155 idempotency_key,
156 )
157 .await?,
158 )?,
159 Self::DeleteMessage {
160 conversation_id,
161 message_id,
162 version,
163 idempotency_key,
164 } => serde_json::to_value(
165 client
166 .delete_message(*conversation_id, *message_id, *version, idempotency_key)
167 .await?,
168 )?,
169 Self::Receipt {
170 conversation_id,
171 message_id,
172 status,
173 device_id,
174 } => serde_json::to_value(
175 client
176 .record_receipt(*conversation_id, *message_id, *status, device_id)
177 .await?,
178 )?,
179 Self::GetDraft { conversation_id } => {
180 serde_json::to_value(client.draft(*conversation_id).await?)?
181 }
182 Self::PutDraft {
183 conversation_id,
184 draft,
185 version,
186 } => serde_json::to_value(client.put_draft(*conversation_id, draft, *version).await?)?,
187 Self::DeleteDraft { conversation_id } => {
188 client.delete_draft(*conversation_id).await?;
189 json!({"deleted":true})
190 }
191 Self::CreateBundle {
192 conversation_id,
193 bundle,
194 idempotency_key,
195 } => serde_json::to_value(
196 client
197 .create_bundle(*conversation_id, bundle, idempotency_key)
198 .await?,
199 )?,
200 Self::GetBundle {
201 conversation_id,
202 bundle_id,
203 } => serde_json::to_value(client.bundle(*conversation_id, *bundle_id).await?)?,
204 Self::GetPresence { actor_id } => {
205 serde_json::to_value(client.presence(actor_id).await?)?
206 }
207 Self::SetPresence { .. } => {
208 return Err(crate::Error::Configuration(
209 "presence changes require the local relay daemon".into(),
210 ));
211 }
212 Self::TrendingGifs => serde_json::to_value(client.gifs(GifList::Trending).await?)?,
213 Self::SearchGifs { query } => {
214 serde_json::to_value(client.gifs(GifList::Search(query)).await?)?
215 }
216 Self::RecentGifs => serde_json::to_value(client.gifs(GifList::Recent).await?)?,
217 })
218 }
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize)]
222pub struct RelayRequest {
223 pub request_id: Uuid,
224 pub profile: String,
226 #[serde(default)]
227 pub testing_environment_id: Option<Uuid>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub testing_generation: Option<i64>,
231 pub request: Operation,
232}
233#[derive(Clone, Debug, Serialize, Deserialize)]
234pub struct RelayAcknowledgement {
235 pub acknowledged: bool,
236 pub request_id: Uuid,
237 pub request: Value,
239}
240#[derive(Clone, Debug, Serialize, Deserialize)]
241pub struct RelayResult {
242 pub request_id: Uuid,
243 pub state: String,
244 #[serde(default)]
245 pub result: Option<Value>,
246 #[serde(default)]
247 pub error: Option<Value>,
248 pub request: Value,
249}
250#[derive(Clone, Debug, Serialize, Deserialize)]
253pub struct RelayRequestStatus {
254 pub request_id: Uuid,
255 pub state: String,
256}
257#[derive(Clone)]
259pub struct RelayClient {
260 http: reqwest::Client,
261 base: url::Url,
262 token: String,
263}
264impl RelayClient {
265 pub fn new(base: &str, token: impl Into<String>) -> Result<Self> {
266 let base = url::Url::parse(base).map_err(|e| crate::Error::Configuration(e.to_string()))?;
267 crate::validate_endpoint(&base)?;
268 if !matches!(
269 base.host_str(),
270 Some("127.0.0.1" | "localhost" | "dm.localhost" | "[::1]" | "::1")
271 ) {
272 return Err(crate::Error::Configuration(
273 "relay must use a loopback URL".into(),
274 ));
275 }
276 Ok(Self {
277 http: reqwest::Client::builder()
278 .timeout(std::time::Duration::from_secs(15))
279 .no_proxy()
280 .build()?,
281 base,
282 token: token.into(),
283 })
284 }
285 pub async fn submit(&self, request: &RelayRequest) -> Result<RelayAcknowledgement> {
286 self.submit_json(&crate::Envelope::new("request", request))
287 .await
288 }
289 pub async fn submit_value(&self, request: &Value) -> Result<RelayAcknowledgement> {
291 self.submit_json(request).await
292 }
293 async fn submit_json<T: Serialize + ?Sized>(
294 &self,
295 request: &T,
296 ) -> Result<RelayAcknowledgement> {
297 let url = self
298 .base
299 .join("requests")
300 .map_err(|e| crate::Error::Configuration(e.to_string()))?;
301 Ok(crate::checked(
302 self.http
303 .post(url)
304 .bearer_auth(&self.token)
305 .json(request)
306 .send()
307 .await?,
308 )
309 .await?
310 .json::<crate::Envelope<RelayAcknowledgement>>()
311 .await?
312 .data)
313 }
314 pub async fn result(&self, id: Uuid) -> Result<RelayResult> {
315 let url = self
316 .base
317 .join(&format!("requests/{id}"))
318 .map_err(|e| crate::Error::Configuration(e.to_string()))?;
319 Ok(
320 crate::checked(self.http.get(url).bearer_auth(&self.token).send().await?)
321 .await?
322 .json::<crate::Envelope<RelayResult>>()
323 .await?
324 .data,
325 )
326 }
327 pub async fn request_status(&self, id: Uuid) -> Result<RelayRequestStatus> {
331 let url = self
332 .base
333 .join(&format!("requests/{id}/status"))
334 .map_err(|e| crate::Error::Configuration(e.to_string()))?;
335 Ok(
336 crate::checked(self.http.get(url).bearer_auth(&self.token).send().await?)
337 .await?
338 .json::<crate::Envelope<RelayRequestStatus>>()
339 .await?
340 .data,
341 )
342 }
343 pub async fn status(&self) -> Result<Value> {
344 let url = self
345 .base
346 .join("status")
347 .map_err(|e| crate::Error::Configuration(e.to_string()))?;
348 Ok(
349 crate::checked(self.http.get(url).bearer_auth(&self.token).send().await?)
350 .await?
351 .json::<crate::Envelope<Value>>()
352 .await?
353 .data,
354 )
355 }
356 pub async fn stop(&self) -> Result<()> {
357 let url = self
358 .base
359 .join("shutdown")
360 .map_err(|e| crate::Error::Configuration(e.to_string()))?;
361 crate::checked(self.http.post(url).bearer_auth(&self.token).send().await?).await?;
362 Ok(())
363 }
364}