use anyhow::{Result, anyhow};
use prost::Message;
use std::str::FromStr;
use wacore_binary::node::{Node, NodeRef};
use waproto::whatsapp as wa;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WAPatchName {
CriticalBlock,
CriticalUnblockLow,
RegularLow,
RegularHigh,
Regular,
Unknown,
}
impl WAPatchName {
pub fn as_str(&self) -> &'static str {
match self {
Self::CriticalBlock => "critical_block",
Self::CriticalUnblockLow => "critical_unblock_low",
Self::RegularLow => "regular_low",
Self::RegularHigh => "regular_high",
Self::Regular => "regular",
Self::Unknown => "unknown",
}
}
}
impl FromStr for WAPatchName {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"critical_block" => Self::CriticalBlock,
"critical_unblock_low" => Self::CriticalUnblockLow,
"regular_low" => Self::RegularLow,
"regular_high" => Self::RegularHigh,
"regular" => Self::Regular,
_ => Self::Unknown,
})
}
}
#[derive(Debug, Clone)]
pub struct PatchList {
pub name: WAPatchName,
pub has_more_patches: bool,
pub patches: Vec<wa::SyncdPatch>,
pub snapshot: Option<wa::SyncdSnapshot>, pub snapshot_ref: Option<wa::ExternalBlobReference>, pub error: Option<CollectionSyncError>,
}
#[derive(Debug, Clone)]
pub enum CollectionSyncError {
Conflict { has_more: bool },
Fatal { code: u16, text: String },
Retry { code: u16, text: String },
}
impl std::fmt::Display for CollectionSyncError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Conflict { has_more } => write!(f, "conflict (has_more={has_more})"),
Self::Fatal { code, text } => write!(f, "fatal error {code}: {text}"),
Self::Retry { code, text } => write!(f, "retryable error {code}: {text}"),
}
}
}
pub fn parse_patch_list(node: &Node) -> Result<PatchList> {
let collection = node
.get_optional_child_by_tag(&["sync", "collection"]) .ok_or_else(|| anyhow!("missing sync/collection"))?;
parse_single_collection(collection)
}
pub fn parse_patch_list_ref(node: &NodeRef<'_>) -> Result<PatchList> {
parse_patch_list(&node.to_owned())
}
pub fn parse_patch_lists_ref(node: &NodeRef<'_>) -> Result<Vec<PatchList>> {
parse_patch_lists(&node.to_owned())
}
pub fn parse_patch_lists(node: &Node) -> Result<Vec<PatchList>> {
let sync_node = if node.tag == "sync" {
node
} else {
node.get_optional_child("sync")
.ok_or_else(|| anyhow!("missing sync node in response"))?
};
let Some(children) = sync_node.children() else {
return Ok(Vec::new());
};
children
.iter()
.filter(|c| c.tag == "collection")
.map(parse_single_collection)
.collect()
}
fn parse_single_collection(collection: &Node) -> Result<PatchList> {
let mut ag = collection.attrs();
let name_str = ag
.optional_string("name")
.ok_or_else(|| anyhow!("collection missing 'name' attribute"))?
.to_string();
let has_more = ag.optional_bool("has_more_patches");
let col_type = ag.optional_string("type");
let error = parse_collection_error(collection, col_type.as_deref());
ag.finish()?;
let mut snapshot_ref = None;
if let Some(snapshot_node) = collection.get_optional_child("snapshot")
&& let Some(wacore_binary::node::NodeContent::Bytes(raw)) = &snapshot_node.content
&& let Ok(ext_ref) = wa::ExternalBlobReference::decode(raw.as_slice())
{
snapshot_ref = Some(ext_ref);
}
let snapshot = None;
let children_ref = collection
.get_optional_child("patches")
.and_then(|n| n.children());
let mut patches: Vec<wa::SyncdPatch> =
Vec::with_capacity(children_ref.as_ref().map_or(0, |c| c.len()));
if let Some(children) = children_ref {
for child in children {
if child.tag == "patch"
&& let Some(wacore_binary::node::NodeContent::Bytes(raw)) = &child.content
{
match wa::SyncdPatch::decode(raw.as_slice()) {
Ok(p) => patches.push(p),
Err(e) => return Err(anyhow!("failed to unmarshal patch: {e}")),
}
}
}
}
Ok(PatchList {
name: WAPatchName::from_str(&name_str).unwrap_or(WAPatchName::Unknown),
has_more_patches: has_more,
patches,
snapshot,
snapshot_ref,
error,
})
}
fn parse_collection_error(
collection: &Node,
col_type: Option<&str>,
) -> Option<CollectionSyncError> {
if col_type? != "error" {
return None;
}
let (code, text) = if let Some(error_node) = collection.get_optional_child("error") {
let mut error_attrs = error_node.attrs();
let code_str = error_attrs.optional_string("code");
let text = error_attrs
.optional_string("text")
.as_deref()
.unwrap_or("")
.to_string();
let code: u16 = code_str.as_deref().unwrap_or("0").parse().unwrap_or(0);
(code, text)
} else {
(0u16, "missing <error> child".to_string())
};
Some(match code {
409 => CollectionSyncError::Conflict {
has_more: collection.attrs().optional_bool("has_more_patches"),
},
400 | 404 => CollectionSyncError::Fatal { code, text },
_ => CollectionSyncError::Retry { code, text },
})
}