Skip to main content

whatsapp_rust/
mediaconn.rs

1//! Media connection management.
2//!
3//! Protocol types are defined in `wacore::iq::mediaconn`.
4
5use crate::client::Client;
6use crate::http::{HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNAUTHORIZED};
7use crate::request::IqError;
8use std::time::Duration;
9use wacore::iq::mediaconn::MediaConnSpec;
10use wacore::time::Instant;
11
12/// Re-export protocol types from wacore.
13pub use wacore::iq::mediaconn::{HostType, MediaConnHost};
14
15/// Number of retry attempts after a media auth error (401/403).
16/// On auth failure, the media connection is invalidated and refreshed before retrying.
17pub(crate) const MEDIA_AUTH_REFRESH_RETRY_ATTEMPTS: usize = 1;
18
19/// Returns `true` if the HTTP status code indicates a media auth error
20/// that should trigger a media connection refresh and retry.
21pub(crate) fn is_media_auth_error(status_code: u16) -> bool {
22    matches!(
23        status_code,
24        HTTP_STATUS_UNAUTHORIZED | HTTP_STATUS_FORBIDDEN
25    )
26}
27
28/// Media connection with runtime-specific fields.
29#[derive(Debug, Clone)]
30pub struct MediaConn {
31    /// Authentication token for media operations.
32    pub auth: String,
33    /// Time-to-live in seconds for route info.
34    pub ttl: u64,
35    /// Time-to-live in seconds for auth token (may differ from route TTL).
36    pub auth_ttl: Option<u64>,
37    /// Available media hosts (sorted: primary first, fallback second).
38    pub hosts: Vec<MediaConnHost>,
39    /// When this connection info was fetched (runtime-specific).
40    pub fetched_at: Instant,
41}
42
43impl MediaConn {
44    /// Check if this connection info has expired.
45    /// Uses the earlier of route TTL and auth TTL (auth may expire before routes).
46    pub fn is_expired(&self) -> bool {
47        let effective_ttl = self.auth_ttl.map_or(self.ttl, |at| self.ttl.min(at));
48        self.fetched_at.elapsed() > Duration::from_secs(effective_ttl)
49    }
50}
51
52impl Client {
53    pub(crate) async fn invalidate_media_conn(&self) {
54        *self.media_conn.write().await = None;
55    }
56
57    #[cfg_attr(
58        feature = "tracing",
59        tracing::instrument(
60            name = "wa.media.refresh_conn",
61            level = "debug",
62            skip_all,
63            fields(force),
64            err(Debug)
65        )
66    )]
67    pub async fn refresh_media_conn(&self, force: bool) -> Result<MediaConn, IqError> {
68        {
69            let guard = self.media_conn.read().await;
70            if !force
71                && let Some(conn) = &*guard
72                && !conn.is_expired()
73            {
74                return Ok(conn.clone());
75            }
76        }
77
78        let response = self.execute(MediaConnSpec::new()).await?;
79
80        let new_conn = MediaConn {
81            auth: response.auth,
82            ttl: response.ttl,
83            auth_ttl: response.auth_ttl,
84            hosts: response.hosts,
85            fetched_at: Instant::now(),
86        };
87
88        let mut write_guard = self.media_conn.write().await;
89        *write_guard = Some(new_conn.clone());
90
91        Ok(new_conn)
92    }
93}