whatsapp_rust/features/
stanza.rs1use thiserror::Error;
4
5use crate::cache::Freshness;
6use crate::client::ClientError;
7use wacore_binary::Jid;
8use waproto::whatsapp as wa;
9
10pub(crate) fn required_stanza_attr<'node, 'data>(
11 node: &'node wacore_binary::NodeRef<'data>,
12 name: &'static str,
13) -> Result<&'node wacore_binary::node::ValueRef<'data>, StanzaResponseError> {
14 match node.get_attr(name) {
15 Some(value) if value != "" => Ok(value),
16 _ => Err(StanzaResponseError::MissingAttribute(name)),
17 }
18}
19
20pub use wacore::protocol::nack::NackReason;
21pub use wacore::protocol::retry::RetryReason;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct StanzaRejection {
26 reason: NackReason,
27 failure_reason: Option<i32>,
28}
29
30impl StanzaRejection {
31 pub const fn new(reason: NackReason) -> Self {
33 Self {
34 reason,
35 failure_reason: None,
36 }
37 }
38
39 pub const fn invalid_protobuf(failure_reason: Option<i32>) -> Self {
41 Self {
42 reason: NackReason::InvalidProtobuf,
43 failure_reason,
44 }
45 }
46
47 pub const fn reason(self) -> NackReason {
49 self.reason
50 }
51
52 pub const fn failure_reason(self) -> Option<i32> {
54 self.failure_reason
55 }
56}
57
58impl From<NackReason> for StanzaRejection {
59 fn from(reason: NackReason) -> Self {
60 Self::new(reason)
61 }
62}
63
64#[derive(Debug, Error)]
66#[non_exhaustive]
67pub enum StanzaResponseError {
68 #[error("stanza is missing required '{0}' attribute")]
69 MissingAttribute(&'static str),
70 #[error("the local device identity is unavailable")]
71 MissingLocalIdentity,
72 #[error("the stanza class does not support this response")]
73 UnsupportedStanzaClass,
74 #[error("failed to encode stanza response")]
75 Encoding(#[from] wacore_binary::error::BinaryError),
76 #[error("{0}")]
77 Client(#[from] ClientError),
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[non_exhaustive]
83pub struct RetryRequestOptions {
84 reason: RetryReason,
85 force_include_keys: bool,
86}
87
88impl RetryRequestOptions {
89 pub const fn new() -> Self {
91 Self {
92 reason: RetryReason::UnknownError,
93 force_include_keys: false,
94 }
95 }
96
97 pub const fn with_reason(mut self, reason: RetryReason) -> Self {
99 self.reason = reason;
100 self
101 }
102
103 pub const fn with_force_include_keys(mut self, force_include_keys: bool) -> Self {
105 self.force_include_keys = force_include_keys;
106 self
107 }
108
109 pub const fn reason(self) -> RetryReason {
111 self.reason
112 }
113
114 pub const fn force_include_keys(self) -> bool {
116 self.force_include_keys
117 }
118}
119
120impl Default for RetryRequestOptions {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128#[non_exhaustive]
129pub enum RetryRequestOutcome {
130 Sent {
132 retry_count: u8,
134 included_keys: bool,
136 },
137 Suppressed {
139 retry_count: u8,
141 },
142 LimitReached,
144}
145
146#[derive(Debug, Error)]
148#[non_exhaustive]
149pub enum RetryRequestError {
150 #[error("retry requests require a message stanza")]
151 UnsupportedStanzaClass,
152 #[error("stanza is missing required '{0}' attribute")]
153 MissingAttribute(&'static str),
154 #[error("the local device identity is unavailable")]
155 MissingLocalIdentity,
156 #[error("invalid message stanza")]
157 InvalidStanza(#[source] anyhow::Error),
158 #[error("{0}")]
159 Client(#[from] ClientError),
160 #[error("failed to prepare retry request")]
161 Internal(#[from] anyhow::Error),
162}
163
164#[derive(Debug)]
170#[non_exhaustive]
171pub struct MessageRetransmission {
172 pub(crate) chat: Jid,
173 pub(crate) requester: Jid,
174 pub(crate) message: wa::Message,
175 pub(crate) message_id: String,
176 pub(crate) retry_count: u8,
177 pub(crate) recipient: Option<Jid>,
178 pub(crate) group_metadata_freshness: Freshness,
179}
180
181impl MessageRetransmission {
182 pub fn new(
184 chat: Jid,
185 requester: Jid,
186 message: wa::Message,
187 message_id: String,
188 retry_count: u8,
189 ) -> Self {
190 Self {
191 chat,
192 requester,
193 message,
194 message_id,
195 retry_count,
196 recipient: None,
197 group_metadata_freshness: Freshness::CachePreferred,
198 }
199 }
200
201 pub fn with_recipient(mut self, recipient: Jid) -> Self {
203 self.recipient = Some(recipient);
204 self
205 }
206
207 pub fn with_group_metadata_freshness(mut self, freshness: Freshness) -> Self {
209 self.group_metadata_freshness = freshness;
210 self
211 }
212
213 pub fn chat(&self) -> &Jid {
214 &self.chat
215 }
216
217 pub fn requester(&self) -> &Jid {
218 &self.requester
219 }
220
221 pub fn message(&self) -> &wa::Message {
222 &self.message
223 }
224
225 pub fn message_id(&self) -> &str {
226 &self.message_id
227 }
228
229 pub const fn retry_count(&self) -> u8 {
230 self.retry_count
231 }
232
233 pub fn recipient(&self) -> Option<&Jid> {
234 self.recipient.as_ref()
235 }
236
237 pub const fn group_metadata_freshness(&self) -> Freshness {
238 self.group_metadata_freshness
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn invalid_protobuf_is_the_only_rejection_with_failure_detail() {
248 let rejection = StanzaRejection::invalid_protobuf(Some(17));
249 assert_eq!(rejection.reason(), NackReason::InvalidProtobuf);
250 assert_eq!(rejection.failure_reason(), Some(17));
251
252 let rejection = StanzaRejection::new(NackReason::ParsingError);
253 assert_eq!(rejection.reason(), NackReason::ParsingError);
254 assert_eq!(rejection.failure_reason(), None);
255 }
256
257 #[test]
258 fn retry_request_options_have_protocol_safe_defaults() {
259 let defaults = RetryRequestOptions::default();
260 assert_eq!(defaults.reason(), RetryReason::UnknownError);
261 assert!(!defaults.force_include_keys());
262
263 let configured = defaults
264 .with_reason(RetryReason::BadMac)
265 .with_force_include_keys(true);
266 assert_eq!(configured.reason(), RetryReason::BadMac);
267 assert!(configured.force_include_keys());
268 }
269
270 #[test]
271 fn internal_retry_error_preserves_its_source() {
272 use std::error::Error as _;
273
274 let error = RetryRequestError::from(anyhow::anyhow!("storage sentinel"));
275 assert_eq!(
276 error.source().map(ToString::to_string).as_deref(),
277 Some("storage sentinel")
278 );
279 }
280
281 #[test]
282 fn response_encoding_error_preserves_its_source() {
283 use std::error::Error as _;
284
285 let error = StanzaResponseError::from(wacore_binary::error::BinaryError::MissingAttr(
286 "sentinel".to_owned(),
287 ));
288 assert!(
289 error
290 .source()
291 .is_some_and(|source| source.to_string().contains("sentinel"))
292 );
293 }
294}