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::fmt;
use std::str::FromStr;

use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::error::Error;

pub const USER_SERVER: &str = "s.whatsapp.net";
pub const LEGACY_USER_SERVER: &str = "c.us";
pub const GROUP_SERVER: &str = "g.us";
pub const LID_SERVER: &str = "lid";
pub const BROADCAST_SERVER: &str = "broadcast";
pub const NEWSLETTER_SERVER: &str = "newsletter";
pub const HOSTED_SERVER: &str = "hosted";
pub const HOSTED_LID_SERVER: &str = "hosted.lid";
pub const CALL_SERVER: &str = "call";

#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Jid {
    pub user: String,
    pub device: u16,
    pub server: String,
}

impl Jid {
    pub fn new(user: impl Into<String>, server: impl Into<String>) -> Self {
        Jid { user: user.into(), device: 0, server: server.into() }
    }

    pub fn phone(number: &str) -> Self {
        let digits: String = number.chars().filter(char::is_ascii_digit).collect();
        Jid::new(digits, USER_SERVER)
    }

    pub fn group(id: impl Into<String>) -> Self {
        Jid::new(id, GROUP_SERVER)
    }

    pub fn lid(id: impl Into<String>) -> Self {
        Jid::new(id, LID_SERVER)
    }

    pub fn server() -> Self {
        Jid::new("", USER_SERVER)
    }

    pub fn group_server() -> Self {
        Jid::new("", GROUP_SERVER)
    }

    pub fn status_broadcast() -> Self {
        Jid::new("status", BROADCAST_SERVER)
    }

    pub(crate) fn from_agent(user: String, agent: u8, device: u16) -> Self {
        let server = match agent {
            1 => LID_SERVER,
            128 => HOSTED_SERVER,
            129 => HOSTED_LID_SERVER,
            _ => USER_SERVER,
        };
        Jid { user, device, server: server.to_owned() }
    }

    pub fn with_device(&self, device: u16) -> Self {
        Jid { user: self.user.clone(), device, server: self.server.clone() }
    }

    pub fn to_non_device(&self) -> Self {
        self.with_device(0)
    }

    pub fn agent(&self) -> u8 {
        match self.server.as_str() {
            LID_SERVER => 1,
            HOSTED_SERVER => 128,
            HOSTED_LID_SERVER => 129,
            _ => 0,
        }
    }

    pub fn is_user(&self) -> bool {
        matches!(
            self.server.as_str(),
            USER_SERVER | LID_SERVER | HOSTED_SERVER | HOSTED_LID_SERVER | LEGACY_USER_SERVER
        ) && !self.user.is_empty()
    }

    pub fn is_pn(&self) -> bool {
        matches!(self.server.as_str(), USER_SERVER | HOSTED_SERVER | LEGACY_USER_SERVER)
    }

    pub fn is_lid(&self) -> bool {
        matches!(self.server.as_str(), LID_SERVER | HOSTED_LID_SERVER)
    }

    pub fn is_group(&self) -> bool {
        self.server == GROUP_SERVER
    }

    pub fn is_broadcast(&self) -> bool {
        self.server == BROADCAST_SERVER
    }

    pub fn is_status_broadcast(&self) -> bool {
        self.is_broadcast() && self.user == "status"
    }

    pub fn is_newsletter(&self) -> bool {
        self.server == NEWSLETTER_SERVER
    }

    pub fn is_server(&self) -> bool {
        self.user.is_empty()
    }

    pub fn same_user(&self, other: &Jid) -> bool {
        self.user == other.user && self.normalized_server() == other.normalized_server()
    }

    fn normalized_server(&self) -> &str {
        if self.server == LEGACY_USER_SERVER {
            USER_SERVER
        } else {
            &self.server
        }
    }

    pub fn signal_address(&self) -> String {
        match self.agent() {
            0 => format!("{}.{}", self.user, self.device),
            agent => format!("{}_{}.{}", self.user, agent, self.device),
        }
    }

    pub fn parse(input: &str) -> Result<Self, Error> {
        input.parse()
    }
}

impl fmt::Display for Jid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.user.is_empty() {
            f.write_str(&self.server)
        } else if self.device > 0 {
            write!(f, "{}:{}@{}", self.user, self.device, self.server)
        } else {
            write!(f, "{}@{}", self.user, self.server)
        }
    }
}

impl FromStr for Jid {
    type Err = Error;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        let Some((left, server)) = input.split_once('@') else {
            return match input {
                USER_SERVER | GROUP_SERVER | BROADCAST_SERVER | LID_SERVER | NEWSLETTER_SERVER | CALL_SERVER => {
                    Ok(Jid::new("", input))
                }
                _ => Err(Error::InvalidJid(input.to_owned())),
            };
        };
        if server.is_empty() {
            return Err(Error::InvalidJid(input.to_owned()));
        }
        let (user_part, device) = match left.split_once(':') {
            Some((user, device)) => {
                let device = device.parse().map_err(|_| Error::InvalidJid(input.to_owned()))?;
                (user, device)
            }
            None => (left, 0),
        };
        let user = user_part.split_once('_').map_or(user_part, |(user, _)| user);
        let server = if server == LEGACY_USER_SERVER { USER_SERVER } else { server };
        Ok(Jid { user: user.to_owned(), device, server: server.to_owned() })
    }
}

impl TryFrom<&str> for Jid {
    type Error = Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        value.parse()
    }
}

impl TryFrom<String> for Jid {
    type Error = Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        value.parse()
    }
}

impl Serialize for Jid {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.collect_str(self)
    }
}

impl<'de> Deserialize<'de> for Jid {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let raw = String::deserialize(deserializer)?;
        raw.parse().map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_and_formats() {
        let jid: Jid = "5215512345678:12@s.whatsapp.net".parse().unwrap();
        assert_eq!(jid.user, "5215512345678");
        assert_eq!(jid.device, 12);
        assert_eq!(jid.to_string(), "5215512345678:12@s.whatsapp.net");
        assert_eq!(jid.signal_address(), "5215512345678.12");

        let lid: Jid = "123456789:3@lid".parse().unwrap();
        assert_eq!(lid.signal_address(), "123456789_1.3");
        assert!(lid.is_lid());

        let group = Jid::group("120363000000000000");
        assert_eq!(group.to_string(), "120363000000000000@g.us");
        assert_eq!(Jid::server().to_string(), "s.whatsapp.net");
        assert_eq!(Jid::phone("+52 1 55 1234-5678").user, "5215512345678");
    }
}