use anyhow::Result;
use async_trait::async_trait;
use bytes::Bytes;
use std::collections::HashMap;
use std::sync::Arc;
pub const WHATSAPP_WEB_WS_URL: &str = "wss://web.whatsapp.com/ws/chat";
pub const WHATSAPP_WEB_ORIGIN: &str = "https://web.whatsapp.com";
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DisconnectReason {
ServerClose { code: Option<u16>, reason: String },
StreamEnded,
ReadError(String),
Unknown,
}
impl std::fmt::Display for DisconnectReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ServerClose { code, reason } => match (code, reason.is_empty()) {
(Some(c), false) => write!(f, "server close frame (code {c}: {reason})"),
(Some(c), true) => write!(f, "server close frame (code {c})"),
(None, false) => write!(f, "server close frame ({reason})"),
(None, true) => write!(f, "server close frame (no code)"),
},
Self::StreamEnded => write!(f, "stream ended (EOF)"),
Self::ReadError(e) => write!(f, "read error: {e}"),
Self::Unknown => write!(f, "unknown"),
}
}
}
impl DisconnectReason {
pub fn is_clean_shutdown(&self) -> bool {
match self {
Self::StreamEnded => true,
Self::ServerClose { code, .. } => matches!(code, None | Some(1000) | Some(1001)),
Self::ReadError(_) => false,
Self::Unknown => false,
}
}
}
#[derive(Debug, Clone)]
pub enum TransportEvent {
Connected,
DataReceived(Bytes),
Disconnected(DisconnectReason),
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Transport: crate::sync_marker::MaybeSendSync {
async fn send(&self, data: Bytes) -> Result<(), anyhow::Error>;
async fn disconnect(&self);
fn resource_report(&self) -> Option<crate::stats::TransportResourceReport> {
None
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait TransportFactory: crate::sync_marker::MaybeSendSync {
async fn create_transport(
&self,
) -> Result<(Arc<dyn Transport>, async_channel::Receiver<TransportEvent>), anyhow::Error>;
}
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub url: String,
pub method: String, pub headers: HashMap<String, String>,
pub body: Option<Bytes>,
}
impl HttpRequest {
pub fn get(url: impl Into<String>) -> Self {
Self {
url: url.into(),
method: "GET".to_string(),
headers: HashMap::new(),
body: None,
}
}
pub fn post(url: impl Into<String>) -> Self {
Self {
url: url.into(),
method: "POST".to_string(),
headers: HashMap::new(),
body: None,
}
}
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(body.into());
self
}
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status_code: u16,
pub body: Vec<u8>,
}
impl HttpResponse {
pub fn body_string(&self) -> Result<String> {
Ok(String::from_utf8(self.body.clone())?)
}
}
pub struct StreamingHttpResponse {
pub status_code: u16,
pub body: Box<dyn std::io::Read + Send>,
}
pub type UploadBody = Box<dyn std::io::Read + Send>;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HttpClient: crate::sync_marker::MaybeSendSync {
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse>;
fn supports_streaming(&self) -> bool {
false
}
fn execute_streaming(&self, _request: HttpRequest) -> Result<StreamingHttpResponse> {
Err(anyhow::anyhow!(
"Streaming not supported by this HTTP client"
))
}
fn supports_upload_streaming(&self) -> bool {
false
}
fn execute_upload(
&self,
_request: HttpRequest,
_body: UploadBody,
_content_length: u64,
) -> Result<HttpResponse> {
Err(anyhow::anyhow!(
"Upload streaming not supported by this HTTP client"
))
}
fn resource_report(&self) -> Option<crate::stats::HttpResourceReport> {
None
}
}
#[cfg(test)]
mod tests {
use super::DisconnectReason;
#[test]
fn clean_shutdowns_are_classified_clean() {
assert!(DisconnectReason::StreamEnded.is_clean_shutdown());
assert!(
DisconnectReason::ServerClose {
code: Some(1000),
reason: String::new()
}
.is_clean_shutdown()
);
assert!(
DisconnectReason::ServerClose {
code: Some(1001),
reason: "going away".to_string()
}
.is_clean_shutdown()
);
assert!(
DisconnectReason::ServerClose {
code: None,
reason: String::new()
}
.is_clean_shutdown()
);
}
#[test]
fn real_errors_are_never_classified_clean() {
assert!(!DisconnectReason::ReadError("connection reset".to_string()).is_clean_shutdown());
assert!(!DisconnectReason::Unknown.is_clean_shutdown());
for code in [1002u16, 1006, 1011, 1012, 1013, 3000, 4000] {
assert!(
!DisconnectReason::ServerClose {
code: Some(code),
reason: String::new()
}
.is_clean_shutdown(),
"close code {code} must not be treated as a clean shutdown"
);
}
}
}