soblendwa 0.1.0

SoblendWA: a blazing fast WhatsApp Web multi-device client library written in Rust. Crafted by Soblend Development Studio.
Documentation
use std::collections::HashMap;

use super::Client;
use crate::binary::Node;
use crate::error::{Error, Result};
use crate::jid::Jid;
use crate::proto::{FutureProofMessage, Message, ProtocolMessage};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrivacySetting {
    LastSeen,
    Online,
    ProfilePicture,
    About,
    ReadReceipts,
    GroupsAdd,
    Calls,
    Messages,
}

impl PrivacySetting {
    pub fn as_str(self) -> &'static str {
        match self {
            PrivacySetting::LastSeen => "last",
            PrivacySetting::Online => "online",
            PrivacySetting::ProfilePicture => "profile",
            PrivacySetting::About => "status",
            PrivacySetting::ReadReceipts => "readreceipts",
            PrivacySetting::GroupsAdd => "groupadd",
            PrivacySetting::Calls => "calladd",
            PrivacySetting::Messages => "messages",
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrivacyValue {
    Everyone,
    Contacts,
    ContactsExcept,
    Nobody,
    MatchLastSeen,
    Known,
}

impl PrivacyValue {
    pub fn as_str(self) -> &'static str {
        match self {
            PrivacyValue::Everyone => "all",
            PrivacyValue::Contacts => "contacts",
            PrivacyValue::ContactsExcept => "contact_blacklist",
            PrivacyValue::Nobody => "none",
            PrivacyValue::MatchLastSeen => "match_last_seen",
            PrivacyValue::Known => "known",
        }
    }
}

#[derive(Clone, Debug)]
pub struct About {
    pub jid: Jid,
    pub text: Option<String>,
    pub set_at: i64,
}

#[derive(Clone, Debug, Default)]
pub struct BusinessProfile {
    pub jid: Option<Jid>,
    pub description: String,
    pub address: Option<String>,
    pub email: Option<String>,
    pub website: Option<String>,
    pub category: Option<String>,
}

impl Client {
    pub(super) fn server_iq(&self, xmlns: &'static str, kind: &'static str) -> Node {
        Node::new("iq").attr("to", Jid::server()).attr("type", kind).attr("xmlns", xmlns)
    }

    fn profile_target(&self, target: Option<&Jid>) -> Option<Jid> {
        target.map(Jid::to_non_device).filter(|jid| !self.is_me(jid))
    }

    pub async fn profile_picture_url(&self, jid: &Jid, full_size: bool) -> Result<Option<String>> {
        let mut picture =
            Node::new("picture").attr("type", if full_size { "image" } else { "preview" }).attr("query", "url");
        if let Some(token) = self.profile_tc_token_node(jid).await {
            picture.push_child(token);
        }
        let request =
            self.server_iq("w:profile:picture", "get").attr("target", jid.to_non_device()).child_node(picture);
        match self.query(request).await {
            Ok(response) => Ok(response.child("picture").and_then(|p| p.attr_string("url"))),
            Err(Error::Server { code: 401 | 404, .. }) => Ok(None),
            Err(err) => Err(err),
        }
    }

    pub async fn set_profile_picture(&self, target: Option<&Jid>, image: Vec<u8>) -> Result<()> {
        let jpeg = tokio::task::spawn_blocking(move || super::enrich::profile_picture(&image))
            .await
            .map_err(|err| Error::Media(err.to_string()))??;
        let request = self
            .server_iq("w:profile:picture", "set")
            .attr_opt("target", self.profile_target(target))
            .child_node(Node::new("picture").attr("type", "image").bytes(jpeg));
        self.query(request).await.map(drop)
    }

    pub async fn remove_profile_picture(&self, target: Option<&Jid>) -> Result<()> {
        let request = self.server_iq("w:profile:picture", "set").attr_opt("target", self.profile_target(target));
        self.query(request).await.map(drop)
    }

    pub async fn set_about(&self, text: &str) -> Result<()> {
        let request = self.server_iq("status", "set").child_node(Node::new("status").bytes(text.as_bytes().to_vec()));
        self.query(request).await.map(drop)
    }

    pub async fn fetch_about(&self, jids: &[Jid]) -> Result<Vec<About>> {
        let users = jids.iter().map(|jid| Node::new("user").attr("jid", jid.to_non_device())).collect();
        let response = self.query(self.usync("interactive", vec![Node::new("status")], users)).await?;
        Ok(response
            .path(&["usync", "list"])
            .map(Node::nodes)
            .unwrap_or_default()
            .iter()
            .filter_map(|user| {
                let jid = user.attr_jid("jid")?;
                let status = user.child("status")?;
                let text = status.content_string().filter(|t| !t.is_empty());
                Some(About { jid, text, set_at: status.attr_i64("t").unwrap_or_default() })
            })
            .collect())
    }

    async fn resolve_lid(&self, phone: &Jid) -> Result<Option<Jid>> {
        if let Some(lid) = self.lid_for_phone(phone).await? {
            return Ok(Some(lid));
        }
        let users = vec![Node::new("user").attr("jid", phone.to_non_device())];
        let response = self.query(self.usync("message", vec![Node::new("lid")], users)).await?;
        for user in response.path(&["usync", "list"]).map(Node::nodes).unwrap_or_default() {
            if let (Some(pn), Some(lid)) = (user.attr_jid("jid"), user.child("lid").and_then(|l| l.attr_jid("val"))) {
                self.remember_lid(&pn, &lid).await;
            }
        }
        self.lid_for_phone(phone).await
    }

    async fn update_block(&self, jid: &Jid, block: bool) -> Result<()> {
        let user = jid.to_non_device();
        let (lid, phone) = if user.is_lid() {
            (Some(user.clone()), self.phone_for_lid(&user).await?)
        } else {
            (self.resolve_lid(&user).await?, Some(user.clone()))
        };
        let action = if block { "block" } else { "unblock" };
        let item = match (lid, phone) {
            (Some(lid), phone) => {
                Node::new("item").attr("action", action).attr("jid", lid).attr_opt("pn_jid", phone.filter(|_| block))
            }
            (None, _) => Node::new("item").attr("action", action).attr("jid", user),
        };
        self.query(self.server_iq("blocklist", "set").child_node(item)).await.map(drop)
    }

    pub async fn block(&self, jid: &Jid) -> Result<()> {
        self.update_block(jid, true).await
    }

    pub async fn unblock(&self, jid: &Jid) -> Result<()> {
        self.update_block(jid, false).await
    }

    pub async fn blocklist(&self) -> Result<Vec<Jid>> {
        let response = self.query(self.server_iq("blocklist", "get")).await?;
        Ok(response
            .child("list")
            .map(|l| l.children_by("item").filter_map(|i| i.attr_jid("jid")).collect())
            .unwrap_or_default())
    }

    pub async fn privacy_settings(&self) -> Result<HashMap<String, String>> {
        let response = self.query(self.server_iq("privacy", "get").child_node(Node::new("privacy"))).await?;
        Ok(response
            .child("privacy")
            .map(|p| {
                p.children_by("category")
                    .filter_map(|c| Some((c.attr_string("name")?, c.attr_string("value")?)))
                    .collect()
            })
            .unwrap_or_default())
    }

    pub async fn set_privacy(&self, setting: PrivacySetting, value: PrivacyValue) -> Result<()> {
        let category = Node::new("category").attr("name", setting.as_str()).attr("value", value.as_str());
        let request = self.server_iq("privacy", "set").child_node(Node::new("privacy").child_node(category));
        self.query(request).await.map(drop)
    }

    pub async fn set_default_disappearing(&self, seconds: u32) -> Result<()> {
        let request = self
            .server_iq("disappearing_mode", "set")
            .child_node(Node::new("disappearing_mode").attr("duration", seconds));
        self.query(request).await.map(drop)
    }

    pub async fn set_disappearing(&self, chat: &Jid, seconds: u32) -> Result<()> {
        if chat.is_group() {
            return self.set_group_ephemeral(chat, seconds).await;
        }
        let setting = Message {
            protocol_message: Some(Box::new(ProtocolMessage {
                r#type: Some(ProtocolMessage::EPHEMERAL_SETTING),
                ephemeral_expiration: Some(seconds),
                ..Default::default()
            })),
            ..Default::default()
        };
        let wrapped = Message {
            ephemeral_message: Some(Box::new(FutureProofMessage { message: Some(Box::new(setting)) })),
            ..Default::default()
        };
        self.send_message(chat, wrapped).await.map(drop)
    }

    pub async fn business_profile(&self, jid: &Jid) -> Result<Option<BusinessProfile>> {
        let request = self.server_iq("w:biz", "get").child_node(
            Node::new("business_profile")
                .attr("v", "244")
                .child_node(Node::new("profile").attr("jid", jid.to_non_device())),
        );
        let response = self.query(request).await?;
        let Some(profile) = response.path(&["business_profile", "profile"]) else { return Ok(None) };
        let text = |tag: &str| profile.child(tag).and_then(Node::content_string);
        Ok(Some(BusinessProfile {
            jid: profile.attr_jid("jid"),
            description: text("description").unwrap_or_default(),
            address: text("address"),
            email: text("email"),
            website: text("website"),
            category: profile.path(&["categories", "category"]).and_then(Node::content_string),
        }))
    }
}