use thiserror::Error;
use crate::cache::Freshness;
use crate::client::ClientError;
use wacore_binary::Jid;
use waproto::whatsapp as wa;
pub(crate) fn required_stanza_attr<'node, 'data>(
node: &'node wacore_binary::NodeRef<'data>,
name: &'static str,
) -> Result<&'node wacore_binary::node::ValueRef<'data>, StanzaResponseError> {
match node.get_attr(name) {
Some(value) if value != "" => Ok(value),
_ => Err(StanzaResponseError::MissingAttribute(name)),
}
}
pub use wacore::protocol::nack::NackReason;
pub use wacore::protocol::retry::RetryReason;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StanzaRejection {
reason: NackReason,
failure_reason: Option<i32>,
}
impl StanzaRejection {
pub const fn new(reason: NackReason) -> Self {
Self {
reason,
failure_reason: None,
}
}
pub const fn invalid_protobuf(failure_reason: Option<i32>) -> Self {
Self {
reason: NackReason::InvalidProtobuf,
failure_reason,
}
}
pub const fn reason(self) -> NackReason {
self.reason
}
pub const fn failure_reason(self) -> Option<i32> {
self.failure_reason
}
}
impl From<NackReason> for StanzaRejection {
fn from(reason: NackReason) -> Self {
Self::new(reason)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StanzaResponseError {
#[error("stanza is missing required '{0}' attribute")]
MissingAttribute(&'static str),
#[error("the local device identity is unavailable")]
MissingLocalIdentity,
#[error("the stanza class does not support this response")]
UnsupportedStanzaClass,
#[error("failed to encode stanza response")]
Encoding(#[from] wacore_binary::error::BinaryError),
#[error("{0}")]
Client(#[from] ClientError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RetryRequestOptions {
reason: RetryReason,
force_include_keys: bool,
}
impl RetryRequestOptions {
pub const fn new() -> Self {
Self {
reason: RetryReason::UnknownError,
force_include_keys: false,
}
}
pub const fn with_reason(mut self, reason: RetryReason) -> Self {
self.reason = reason;
self
}
pub const fn with_force_include_keys(mut self, force_include_keys: bool) -> Self {
self.force_include_keys = force_include_keys;
self
}
pub const fn reason(self) -> RetryReason {
self.reason
}
pub const fn force_include_keys(self) -> bool {
self.force_include_keys
}
}
impl Default for RetryRequestOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RetryRequestOutcome {
Sent {
retry_count: u8,
included_keys: bool,
},
Suppressed {
retry_count: u8,
},
LimitReached,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RetryRequestError {
#[error("retry requests require a message stanza")]
UnsupportedStanzaClass,
#[error("stanza is missing required '{0}' attribute")]
MissingAttribute(&'static str),
#[error("the local device identity is unavailable")]
MissingLocalIdentity,
#[error("invalid message stanza")]
InvalidStanza(#[source] anyhow::Error),
#[error("{0}")]
Client(#[from] ClientError),
#[error("failed to prepare retry request")]
Internal(#[from] anyhow::Error),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct MessageRetransmission {
pub(crate) chat: Jid,
pub(crate) requester: Jid,
pub(crate) message: wa::Message,
pub(crate) message_id: String,
pub(crate) retry_count: u8,
pub(crate) recipient: Option<Jid>,
pub(crate) group_metadata_freshness: Freshness,
}
impl MessageRetransmission {
pub fn new(
chat: Jid,
requester: Jid,
message: wa::Message,
message_id: String,
retry_count: u8,
) -> Self {
Self {
chat,
requester,
message,
message_id,
retry_count,
recipient: None,
group_metadata_freshness: Freshness::CachePreferred,
}
}
pub fn with_recipient(mut self, recipient: Jid) -> Self {
self.recipient = Some(recipient);
self
}
pub fn with_group_metadata_freshness(mut self, freshness: Freshness) -> Self {
self.group_metadata_freshness = freshness;
self
}
pub fn chat(&self) -> &Jid {
&self.chat
}
pub fn requester(&self) -> &Jid {
&self.requester
}
pub fn message(&self) -> &wa::Message {
&self.message
}
pub fn message_id(&self) -> &str {
&self.message_id
}
pub const fn retry_count(&self) -> u8 {
self.retry_count
}
pub fn recipient(&self) -> Option<&Jid> {
self.recipient.as_ref()
}
pub const fn group_metadata_freshness(&self) -> Freshness {
self.group_metadata_freshness
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_protobuf_is_the_only_rejection_with_failure_detail() {
let rejection = StanzaRejection::invalid_protobuf(Some(17));
assert_eq!(rejection.reason(), NackReason::InvalidProtobuf);
assert_eq!(rejection.failure_reason(), Some(17));
let rejection = StanzaRejection::new(NackReason::ParsingError);
assert_eq!(rejection.reason(), NackReason::ParsingError);
assert_eq!(rejection.failure_reason(), None);
}
#[test]
fn retry_request_options_have_protocol_safe_defaults() {
let defaults = RetryRequestOptions::default();
assert_eq!(defaults.reason(), RetryReason::UnknownError);
assert!(!defaults.force_include_keys());
let configured = defaults
.with_reason(RetryReason::BadMac)
.with_force_include_keys(true);
assert_eq!(configured.reason(), RetryReason::BadMac);
assert!(configured.force_include_keys());
}
#[test]
fn internal_retry_error_preserves_its_source() {
use std::error::Error as _;
let error = RetryRequestError::from(anyhow::anyhow!("storage sentinel"));
assert_eq!(
error.source().map(ToString::to_string).as_deref(),
Some("storage sentinel")
);
}
#[test]
fn response_encoding_error_preserves_its_source() {
use std::error::Error as _;
let error = StanzaResponseError::from(wacore_binary::error::BinaryError::MissingAttr(
"sentinel".to_owned(),
));
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("sentinel"))
);
}
}