Skip to main content

wi_api/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::value::RawValue;
3
4// ─── Session ──────────────────────────────────────────────────────────────────
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct SessionStatus {
8    pub connected: bool,
9    pub phone: Option<String>,
10}
11
12#[derive(Debug, Clone, Deserialize)]
13pub struct QRCode {
14    pub qr: Option<String>,
15}
16
17#[derive(Debug, Clone, Deserialize)]
18pub struct PairPhoneResult {
19    pub pair_code: Option<String>,
20}
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct MessageResponse {
24    #[serde(rename = "messageId")]
25    pub message_id: String,
26    pub id: String,
27}
28
29// ─── Send params ──────────────────────────────────────────────────────────────
30
31#[derive(Debug, Clone, Serialize, Default)]
32pub struct SendTextParams {
33    pub to: String,
34    pub text: String,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub quoted: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub mentions: Option<Vec<String>>,
39}
40
41#[derive(Debug, Clone, Serialize, Default)]
42pub struct SendImageParams {
43    pub to: String,
44    pub url: String,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub caption: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub quoted: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Default)]
52pub struct SendAudioParams {
53    pub to: String,
54    pub url: String,
55    /// Send as voice note. Default: false.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub ptt: Option<bool>,
58}
59
60#[derive(Debug, Clone, Serialize, Default)]
61pub struct SendVideoParams {
62    pub to: String,
63    pub url: String,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub caption: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub quoted: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Default)]
71pub struct SendDocumentParams {
72    pub to: String,
73    pub url: String,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub filename: Option<String>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub caption: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Default)]
81pub struct SendLocationParams {
82    pub to: String,
83    pub latitude: f64,
84    pub longitude: f64,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub title: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub address: Option<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Default)]
92pub struct SendContactParams {
93    pub to: String,
94    pub contact_name: String,
95    pub contact_number: String,
96}
97
98#[derive(Debug, Clone, Serialize, Default)]
99pub struct SendStickerParams {
100    pub to: String,
101    pub url: String,
102}
103
104#[derive(Debug, Clone, Serialize, Default)]
105pub struct SendPollParams {
106    pub to: String,
107    pub question: String,
108    pub options: Vec<String>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub multiple_answers: Option<bool>,
111}
112
113#[derive(Debug, Clone, Serialize, Default)]
114pub struct SendReactionParams {
115    pub to: String,
116    pub message_id: String,
117    /// Emoji character, or empty string to remove the reaction.
118    pub emoji: String,
119}
120
121// ─── Chat params ──────────────────────────────────────────────────────────────
122
123#[derive(Debug, Clone, Serialize, Default)]
124pub struct MarkReadParams {
125    pub chat_id: String,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub last_message_id: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Default)]
131pub struct PresenceParams {
132    pub chat_id: String,
133    /// One of "composing", "recording", "paused"
134    pub presence: String,
135}
136
137// ─── Webhooks ─────────────────────────────────────────────────────────────────
138
139#[derive(Debug, Clone, Deserialize)]
140pub struct Event {
141    pub event: String,
142    pub session_id: String,
143    /// The session's `metadata` (labels set on `POST /sessions` or `PATCH /sessions/{id}`) when
144    /// the event was sent, so you can route on it without a lookup. Empty when the session has
145    /// none, and also empty on a server older than this field.
146    #[serde(default)]
147    pub session_metadata: std::collections::HashMap<String, String>,
148    pub data: Box<RawValue>,
149    pub timestamp: String,
150}
151
152#[derive(Debug, Clone, Deserialize)]
153pub struct IncomingMessage {
154    pub id: String,
155    pub chat: String,
156    pub from: String,
157    pub from_me: bool,
158    #[serde(rename = "type")]
159    pub msg_type: String,
160    pub text: Option<String>,
161    pub caption: Option<String>,
162    pub media_url: Option<String>,
163    pub mimetype: Option<String>,
164    pub filename: Option<String>,
165    pub latitude: Option<f64>,
166    pub longitude: Option<f64>,
167    pub timestamp: i64,
168}
169
170/// Why a session went offline. The set is closed, but a newer server may add a
171/// value: it decodes as [`DisconnectReason::Unknown`] instead of failing, so
172/// decide by [`Disconnected::will_reconnect`] when [`is_known`](Self::is_known)
173/// is false.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum DisconnectReason {
176    /// Network or WhatsApp drop the engine retries by itself.
177    Transient,
178    /// A disconnect you asked for.
179    Manual,
180    /// A durable pause you asked for; resume it explicitly.
181    Paused,
182    /// The link was removed; pair again.
183    LoggedOut,
184    /// Another client took over the session.
185    Replaced,
186    /// Temporary ban; the engine reconnects after `retry_after_seconds`.
187    TemporaryBan,
188    /// WhatsApp refused the engine's client version.
189    ClientOutdated,
190    /// A connect error WhatsApp reported (see `code`).
191    ConnectFailure,
192    /// A value this SDK version does not know.
193    Unknown(String),
194}
195
196impl DisconnectReason {
197    /// Every reason the server publishes today.
198    pub const KNOWN: [DisconnectReason; 8] = [
199        DisconnectReason::Transient,
200        DisconnectReason::Manual,
201        DisconnectReason::Paused,
202        DisconnectReason::LoggedOut,
203        DisconnectReason::Replaced,
204        DisconnectReason::TemporaryBan,
205        DisconnectReason::ClientOutdated,
206        DisconnectReason::ConnectFailure,
207    ];
208
209    /// The wire value.
210    pub fn as_str(&self) -> &str {
211        match self {
212            Self::Transient => "transient",
213            Self::Manual => "manual",
214            Self::Paused => "paused",
215            Self::LoggedOut => "logged_out",
216            Self::Replaced => "replaced",
217            Self::TemporaryBan => "temporary_ban",
218            Self::ClientOutdated => "client_outdated",
219            Self::ConnectFailure => "connect_failure",
220            Self::Unknown(other) => other,
221        }
222    }
223
224    /// False for a reason a newer server added.
225    pub fn is_known(&self) -> bool {
226        !matches!(self, Self::Unknown(_))
227    }
228}
229
230impl<'de> Deserialize<'de> for DisconnectReason {
231    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
232        let raw = String::deserialize(d)?;
233        Ok(match raw.as_str() {
234            "transient" => Self::Transient,
235            "manual" => Self::Manual,
236            "paused" => Self::Paused,
237            "logged_out" => Self::LoggedOut,
238            "replaced" => Self::Replaced,
239            "temporary_ban" => Self::TemporaryBan,
240            "client_outdated" => Self::ClientOutdated,
241            "connect_failure" => Self::ConnectFailure,
242            _ => Self::Unknown(raw),
243        })
244    }
245}
246
247/// Data payload of the `disconnected` event. `will_reconnect` is always
248/// present: `true` means the engine reconnects by itself, `false` means the
249/// session stays offline until you act (see `resume`).
250/// Decode with `serde_json::from_str::<Disconnected>(event.data.get())`.
251#[derive(Debug, Clone, Deserialize)]
252pub struct Disconnected {
253    pub reason: DisconnectReason,
254    #[serde(rename = "willReconnect")]
255    pub will_reconnect: bool,
256    /// Call that brings the session back (`connect` or `resume`). Only with
257    /// `will_reconnect: false`.
258    pub resume: Option<String>,
259    /// Code WhatsApp reported (`logged_out`, `temporary_ban`, `connect_failure`).
260    pub code: Option<i64>,
261    /// Diagnostic text; do not branch on it. `"local"` marks a logout you asked for.
262    pub detail: Option<String>,
263    /// `temporary_ban`: length of the ban, in seconds.
264    #[serde(rename = "expireSeconds")]
265    pub expire_seconds: Option<i64>,
266    /// `temporary_ban`: seconds until the engine reconnects by itself.
267    #[serde(rename = "retryAfterSeconds")]
268    pub retry_after_seconds: Option<i64>,
269}
270
271/// How a pairing attempt links the device. A value a newer server adds decodes
272/// as [`PairingMethod::Unknown`] instead of failing.
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub enum PairingMethod {
275    /// A scan of the QR code.
276    Qr,
277    /// A pairing code typed on the phone (`pair_phone`).
278    Code,
279    /// A value this SDK version does not know.
280    Unknown(String),
281}
282
283impl PairingMethod {
284    /// The wire value.
285    pub fn as_str(&self) -> &str {
286        match self {
287            Self::Qr => "qr",
288            Self::Code => "code",
289            Self::Unknown(other) => other,
290        }
291    }
292}
293
294impl<'de> Deserialize<'de> for PairingMethod {
295    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
296        let raw = String::deserialize(d)?;
297        Ok(match raw.as_str() {
298            "qr" => Self::Qr,
299            "code" => Self::Code,
300            _ => Self::Unknown(raw),
301        })
302    }
303}
304
305/// Why a pairing attempt ended without linking the number. The set is closed,
306/// but a newer server may add a value: it decodes as
307/// [`PairingFailureReason::Unknown`] instead of failing, so treat it like
308/// [`PairingFailureReason::Error`] when [`is_known`](Self::is_known) is false.
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum PairingFailureReason {
311    /// Nobody read the QR or typed the code in time (the QR channel lasts about 160 s).
312    Timeout,
313    /// WhatsApp refused the link.
314    Rejected,
315    /// `disconnect` or `pause` ended the attempt.
316    Cancelled,
317    /// WhatsApp refuses the connector's version.
318    ClientOutdated,
319    /// Any other failure.
320    Error,
321    /// A value this SDK version does not know.
322    Unknown(String),
323}
324
325impl PairingFailureReason {
326    /// Every reason the server publishes today.
327    pub const KNOWN: [PairingFailureReason; 5] = [
328        PairingFailureReason::Timeout,
329        PairingFailureReason::Rejected,
330        PairingFailureReason::Cancelled,
331        PairingFailureReason::ClientOutdated,
332        PairingFailureReason::Error,
333    ];
334
335    /// The wire value.
336    pub fn as_str(&self) -> &str {
337        match self {
338            Self::Timeout => "timeout",
339            Self::Rejected => "rejected",
340            Self::Cancelled => "cancelled",
341            Self::ClientOutdated => "client_outdated",
342            Self::Error => "error",
343            Self::Unknown(other) => other,
344        }
345    }
346
347    /// False for a reason a newer server added.
348    pub fn is_known(&self) -> bool {
349        !matches!(self, Self::Unknown(_))
350    }
351}
352
353impl<'de> Deserialize<'de> for PairingFailureReason {
354    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
355        let raw = String::deserialize(d)?;
356        Ok(match raw.as_str() {
357            "timeout" => Self::Timeout,
358            "rejected" => Self::Rejected,
359            "cancelled" => Self::Cancelled,
360            "client_outdated" => Self::ClientOutdated,
361            "error" => Self::Error,
362            _ => Self::Unknown(raw),
363        })
364    }
365}
366
367/// Data payload of the `pairing.failed` event: how a pairing attempt ended
368/// without linking the number. `at` is the end of the attempt, ISO 8601.
369/// Decode with `serde_json::from_str::<PairingFailed>(event.data.get())`.
370#[derive(Debug, Clone, Deserialize)]
371pub struct PairingFailed {
372    pub method: PairingMethod,
373    pub reason: PairingFailureReason,
374    pub at: String,
375}
376
377/// Data payload of the `pairing.succeeded` event, sent just before `connected`.
378/// Decode with `serde_json::from_str::<PairingSucceeded>(event.data.get())`.
379#[derive(Debug, Clone, Deserialize)]
380pub struct PairingSucceeded {
381    pub method: PairingMethod,
382    pub at: String,
383}
384
385/// Normalized delivery state of a [`Receipt`].
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
387#[serde(rename_all = "lowercase")]
388pub enum ReceiptStatus {
389    Delivered,
390    Read,
391    Played,
392}
393
394/// Data payload of the `receipt` event. One receipt can acknowledge several
395/// messages (`message_ids`); in groups there is one receipt per participant.
396/// Decode with `serde_json::from_str::<Receipt>(event.data.get())`.
397#[derive(Debug, Clone, Deserialize)]
398pub struct Receipt {
399    /// `None` for receipt types that say nothing about delivery
400    /// (`retry`, `server-error`, `inactive`, `peer_msg`, `hist_sync`).
401    pub status: Option<ReceiptStatus>,
402    /// Raw WhatsApp type: `""` (delivered), `sender`, `read`, `read-self`,
403    /// `played`, `played-self`, ... `type` carries the same value.
404    #[serde(rename = "rawType")]
405    pub raw_type: String,
406    #[serde(rename = "type")]
407    pub receipt_type: String,
408    pub chat: String,
409    /// Who produced the receipt: the contact, or a group participant.
410    pub sender: String,
411    #[serde(rename = "messageIds")]
412    pub message_ids: Vec<String>,
413    /// Unix seconds.
414    pub timestamp: i64,
415    /// The receipt came from another device of your own account.
416    #[serde(rename = "isFromMe")]
417    pub is_from_me: bool,
418    /// The acknowledged messages were sent by you: only these update the
419    /// status of sent messages.
420    #[serde(rename = "messageFromMe")]
421    pub message_from_me: bool,
422    /// Author of the acknowledged messages when it is not you (group reads).
423    #[serde(rename = "messageSender")]
424    pub message_sender: Option<String>,
425}