use crate::node::NodeStr;
use compact_str::CompactString;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy)]
pub struct ParsedJidParts<'a> {
pub user: &'a str,
pub server: &'a str,
pub agent: u8,
pub device: u16,
pub integrator: u16,
}
#[inline]
pub fn parse_jid_fast(s: &str) -> Option<ParsedJidParts<'_>> {
if s.is_empty() {
return None;
}
let bytes = s.as_bytes();
let mut at_pos: Option<usize> = None;
let mut colon_pos: Option<usize> = None;
let mut last_dot_pos: Option<usize> = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'@' if at_pos.is_none() => at_pos = Some(i),
b':' if at_pos.is_none() => colon_pos = Some(i),
b'.' if at_pos.is_none() && colon_pos.is_none() => last_dot_pos = Some(i),
_ => {}
}
}
let at = match at_pos {
Some(pos) => pos,
None => {
return None;
}
};
let user_part = &s[..at];
let server = &s[at + 1..];
if user_part.is_empty() {
return None;
}
if server == HIDDEN_USER_SERVER {
let (user, device) = match colon_pos {
Some(pos) if pos < at => {
let device_slice = &s[pos + 1..at];
(&s[..pos], device_slice.parse::<u16>().unwrap_or(0))
}
_ => (user_part, 0),
};
return Some(ParsedJidParts {
user,
server,
agent: 0,
device,
integrator: 0,
});
}
if server == DEFAULT_USER_SERVER {
if let Some(pos) = colon_pos {
let user_end = pos;
let device_start = pos + 1;
let device_slice = &s[device_start..at];
let device = device_slice.parse::<u16>().unwrap_or(0);
return Some(ParsedJidParts {
user: &s[..user_end],
server,
agent: 0,
device,
integrator: 0,
});
}
if let Some(dot_pos) = last_dot_pos {
let suffix = &s[dot_pos + 1..at];
if let Ok(device_val) = suffix.parse::<u16>() {
return Some(ParsedJidParts {
user: &s[..dot_pos],
server,
agent: 0,
device: device_val,
integrator: 0,
});
}
}
return Some(ParsedJidParts {
user: user_part,
server,
agent: 0,
device: 0,
integrator: 0,
});
}
let (user_before_colon, device) = match colon_pos {
Some(pos) => {
let user_end = pos;
let device_start = pos + 1;
let device_slice = &s[device_start..at];
(&s[..user_end], device_slice.parse::<u16>().unwrap_or(0))
}
None => (user_part, 0),
};
let user_to_check = user_before_colon;
let (final_user, agent) = {
if let Some(dot_pos) = user_to_check.rfind('.') {
let suffix = &user_to_check[dot_pos + 1..];
if let Ok(agent_val) = suffix.parse::<u16>() {
if agent_val <= u8::MAX as u16 {
(&user_to_check[..dot_pos], agent_val as u8)
} else {
(user_to_check, 0)
}
} else {
(user_to_check, 0)
}
} else {
(user_to_check, 0)
}
};
Some(ParsedJidParts {
user: final_user,
server,
agent,
device,
integrator: 0,
})
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum Server {
#[default]
Pn = 0,
Lid = 1,
Group = 2,
Broadcast = 3,
Newsletter = 4,
Hosted = 5,
HostedLid = 6,
Messenger = 7,
Interop = 8,
Bot = 9,
Legacy = 10,
}
#[cfg(feature = "serde")]
impl serde::Serialize for Server {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Server {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = <&str>::deserialize(deserializer)?;
Server::try_from(s).map_err(serde::de::Error::custom)
}
}
impl Server {
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::Pn => "s.whatsapp.net",
Self::Lid => "lid",
Self::Group => "g.us",
Self::Broadcast => "broadcast",
Self::Newsletter => "newsletter",
Self::Hosted => "hosted",
Self::HostedLid => "hosted.lid",
Self::Messenger => "msgr",
Self::Interop => "interop",
Self::Bot => "bot",
Self::Legacy => "c.us",
}
}
#[inline]
pub fn is_pn_family(self) -> bool {
matches!(self, Self::Pn | Self::Hosted)
}
#[inline]
pub fn is_lid_family(self) -> bool {
matches!(self, Self::Lid | Self::HostedLid)
}
}
impl fmt::Display for Server {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl PartialEq<str> for Server {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for Server {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl TryFrom<&str> for Server {
type Error = JidError;
fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
match s {
"s.whatsapp.net" => Ok(Self::Pn),
"lid" => Ok(Self::Lid),
"g.us" => Ok(Self::Group),
"broadcast" => Ok(Self::Broadcast),
"newsletter" => Ok(Self::Newsletter),
"hosted" => Ok(Self::Hosted),
"hosted.lid" => Ok(Self::HostedLid),
"msgr" => Ok(Self::Messenger),
"interop" => Ok(Self::Interop),
"bot" => Ok(Self::Bot),
"c.us" => Ok(Self::Legacy),
other => Err(JidError::InvalidFormat(format!("unknown server: {other}"))),
}
}
}
pub const DEFAULT_USER_SERVER: &str = "s.whatsapp.net";
pub const SERVER_JID: &str = "s.whatsapp.net";
pub const GROUP_SERVER: &str = "g.us";
pub const LEGACY_USER_SERVER: &str = "c.us";
pub const BROADCAST_SERVER: &str = "broadcast";
pub const HIDDEN_USER_SERVER: &str = "lid";
pub const NEWSLETTER_SERVER: &str = "newsletter";
pub const HOSTED_SERVER: &str = "hosted";
pub const HOSTED_LID_SERVER: &str = "hosted.lid";
pub const MESSENGER_SERVER: &str = "msgr";
pub const INTEROP_SERVER: &str = "interop";
pub const BOT_SERVER: &str = "bot";
pub const STATUS_BROADCAST_USER: &str = "status";
pub type MessageId = String;
pub type MessageServerId = i32;
#[derive(Debug)]
pub enum JidError {
InvalidFormat(String),
Parse(std::num::ParseIntError),
}
impl fmt::Display for JidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JidError::InvalidFormat(s) => write!(f, "Invalid JID format: {s}"),
JidError::Parse(e) => write!(f, "Failed to parse component: {e}"),
}
}
}
impl std::error::Error for JidError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
JidError::Parse(e) => Some(e),
_ => None,
}
}
}
impl From<std::num::ParseIntError> for JidError {
fn from(err: std::num::ParseIntError) -> Self {
JidError::Parse(err)
}
}
pub trait JidExt {
fn user(&self) -> &str;
fn server(&self) -> Server;
fn device(&self) -> u16;
fn integrator(&self) -> u16;
fn is_ad(&self) -> bool {
self.device() > 0
&& matches!(
self.server(),
Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
)
}
fn is_interop(&self) -> bool {
self.server() == Server::Interop && self.integrator() > 0
}
fn is_messenger(&self) -> bool {
self.server() == Server::Messenger && self.device() > 0
}
fn is_group(&self) -> bool {
self.server() == Server::Group
}
fn is_broadcast_list(&self) -> bool {
self.server() == Server::Broadcast && self.user() != STATUS_BROADCAST_USER
}
fn is_status_broadcast(&self) -> bool {
self.server() == Server::Broadcast && self.user() == STATUS_BROADCAST_USER
}
fn is_bot(&self) -> bool {
(self.server() == Server::Pn
&& self.device() == 0
&& (self.user().starts_with("1313555") || self.user().starts_with("131655500")))
|| self.server() == Server::Bot
}
fn is_newsletter(&self) -> bool {
self.server() == Server::Newsletter
}
fn is_hosted(&self) -> bool {
self.device() == 99 || matches!(self.server(), Server::Hosted | Server::HostedLid)
}
fn is_empty(&self) -> bool {
self.user().is_empty()
}
fn is_same_user_as(&self, other: &impl JidExt) -> bool {
self.user() == other.user()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Jid {
pub user: CompactString,
pub server: Server,
pub agent: u8,
pub device: u16,
pub integrator: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, yoke::Yokeable)]
pub struct JidRef<'a> {
pub user: NodeStr<'a>,
pub server: Server,
pub agent: u8,
pub device: u16,
pub integrator: u16,
}
impl JidExt for Jid {
fn user(&self) -> &str {
&self.user
}
fn server(&self) -> Server {
self.server
}
fn device(&self) -> u16 {
self.device
}
fn integrator(&self) -> u16 {
self.integrator
}
}
impl Jid {
pub fn new(user: impl Into<CompactString>, server: Server) -> Self {
Self {
user: user.into(),
server,
..Default::default()
}
}
pub fn pn(user: impl Into<CompactString>) -> Self {
Self {
user: user.into(),
server: Server::Pn,
..Default::default()
}
}
pub fn lid(user: impl Into<CompactString>) -> Self {
Self {
user: user.into(),
server: Server::Lid,
..Default::default()
}
}
pub fn status_broadcast() -> Self {
Self {
user: CompactString::from(STATUS_BROADCAST_USER),
server: Server::Broadcast,
agent: 0,
device: 0,
integrator: 0,
}
}
pub fn group(id: impl Into<CompactString>) -> Self {
Self {
user: id.into(),
server: Server::Group,
..Default::default()
}
}
pub fn newsletter(id: impl Into<CompactString>) -> Self {
Self {
user: id.into(),
server: Server::Newsletter,
..Default::default()
}
}
pub fn pn_device(user: impl Into<CompactString>, device: u16) -> Self {
Self {
user: user.into(),
server: Server::Pn,
device,
..Default::default()
}
}
pub fn lid_device(user: impl Into<CompactString>, device: u16) -> Self {
Self {
user: user.into(),
server: Server::Lid,
device,
..Default::default()
}
}
#[inline]
pub fn is_pn(&self) -> bool {
self.server == Server::Pn
}
#[inline]
pub fn is_lid(&self) -> bool {
self.server == Server::Lid
}
#[inline]
pub fn user_base(&self) -> &str {
if let Some((base, _)) = self.user.split_once(':') {
base
} else {
&self.user
}
}
pub fn with_device(&self, device_id: u16) -> Self {
Self {
user: self.user.clone(),
server: self.server,
agent: self.agent,
device: device_id,
integrator: self.integrator,
}
}
pub fn to_non_ad(&self) -> Self {
Self {
user: self.user.clone(),
server: self.server,
integrator: self.integrator,
..Default::default()
}
}
#[inline]
pub fn matches_user_or_lid(&self, user: &Jid, lid: Option<&Jid>) -> bool {
self.is_same_user_as(user) || lid.is_some_and(|l| self.is_same_user_as(l))
}
pub fn normalize_for_prekey_bundle(&self) -> Self {
let mut jid = self.clone();
if matches!(jid.server, Server::Pn | Server::Lid) {
jid.agent = 0;
}
jid
}
pub fn to_ad_string(&self) -> String {
if self.user.is_empty() {
return self.server.as_str().to_string();
}
let mut s = String::with_capacity(self.user.len() + 20);
s.push_str(&self.user);
s.push('.');
s.push_str(itoa::Buffer::new().format(self.agent));
s.push(':');
s.push_str(itoa::Buffer::new().format(self.device));
s.push('@');
s.push_str(self.server.as_str());
s
}
#[inline]
pub fn push_to(&self, buf: &mut String) {
push_jid_to_string(&self.user, self.server, self.agent, self.device, buf);
}
#[inline]
pub fn device_eq(&self, other: &Jid) -> bool {
self.user == other.user && self.server == other.server && self.device == other.device
}
#[inline]
pub fn device_key(&self) -> DeviceKey<'_> {
DeviceKey {
user: &self.user,
server: self.server,
device: self.device,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DeviceKey<'a> {
pub user: &'a str,
pub server: Server,
pub device: u16,
}
impl<'a> JidExt for JidRef<'a> {
fn user(&self) -> &str {
&self.user
}
fn server(&self) -> Server {
self.server
}
fn device(&self) -> u16 {
self.device
}
fn integrator(&self) -> u16 {
self.integrator
}
}
impl<'a> JidRef<'a> {
pub fn to_owned(&self) -> Jid {
Jid {
user: self.user.to_compact_string(),
server: self.server,
agent: self.agent,
device: self.device,
integrator: self.integrator,
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for JidRef<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("Jid", 5)?;
s.serialize_field("user", &*self.user)?;
s.serialize_field("server", &self.server)?;
s.serialize_field("agent", &self.agent)?;
s.serialize_field("device", &self.device)?;
s.serialize_field("integrator", &self.integrator)?;
s.end()
}
}
impl FromStr for Jid {
type Err = JidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(parts) = parse_jid_fast(s) {
return Ok(Jid {
user: CompactString::from(parts.user),
server: Server::try_from(parts.server)?,
agent: parts.agent,
device: parts.device,
integrator: parts.integrator,
});
}
let (user_part, server) = match s.split_once('@') {
Some((u, s)) => (u, s),
None => ("", s),
};
if user_part.is_empty() && Server::try_from(server).is_err() {
return Err(JidError::InvalidFormat(format!(
"unknown server '{server}'"
)));
}
if server == HIDDEN_USER_SERVER {
let (user, device) = if let Some((u, d_str)) = user_part.rsplit_once(':') {
(u, d_str.parse()?)
} else {
(user_part, 0)
};
return Ok(Jid {
user: CompactString::from(user),
server: Server::try_from(server)?,
device,
agent: 0,
integrator: 0,
});
}
let mut user = user_part;
let mut device = 0;
let mut agent = 0;
if let Some((u, d_str)) = user_part.rsplit_once(':') {
user = u;
device = d_str.parse()?;
}
if server != DEFAULT_USER_SERVER
&& server != HIDDEN_USER_SERVER
&& let Some((u, last_part)) = user.rsplit_once('.')
&& let Ok(num_val) = last_part.parse::<u16>()
{
if num_val > u8::MAX as u16 {
return Err(JidError::InvalidFormat(format!(
"Agent component out of range: {num_val}"
)));
}
user = u;
agent = num_val as u8;
}
Ok(Jid {
user: CompactString::from(user),
server: Server::try_from(server)?,
agent,
device,
integrator: 0,
})
}
}
macro_rules! write_jid {
(infallible $buf:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
let (user, server, agent, device) = ($user, $server, $agent, $device);
if user.is_empty() {
$buf.push_str(server.as_str());
return;
}
$buf.push_str(user);
if agent > 0
&& !matches!(
server,
Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
)
{
$buf.push('.');
$buf.push_str(itoa::Buffer::new().format(agent));
}
if device > 0 {
$buf.push(':');
$buf.push_str(itoa::Buffer::new().format(device));
}
$buf.push('@');
$buf.push_str(server.as_str());
}};
(fallible $f:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
let (user, server, agent, device) = ($user, $server, $agent, $device);
if user.is_empty() {
return $f.write_str(server.as_str());
}
$f.write_str(user)?;
if agent > 0
&& !matches!(
server,
Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
)
{
$f.write_str(".")?;
$f.write_str(itoa::Buffer::new().format(agent))?;
}
if device > 0 {
$f.write_str(":")?;
$f.write_str(itoa::Buffer::new().format(device))?;
}
$f.write_str("@")?;
$f.write_str(server.as_str())
}};
}
#[inline]
pub fn push_jid_to_string(user: &str, server: Server, agent: u8, device: u16, buf: &mut String) {
write_jid!(infallible buf, user, server, agent, device);
}
#[inline]
pub fn push_jid_to_compact(
user: &str,
server: Server,
agent: u8,
device: u16,
buf: &mut CompactString,
) {
write_jid!(infallible buf, user, server, agent, device);
}
impl fmt::Display for Jid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_jid!(fallible f, &*self.user, self.server, self.agent, self.device)
}
}
impl<'a> fmt::Display for JidRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_jid!(fallible f, &*self.user, self.server, self.agent, self.device)
}
}
impl From<Jid> for String {
fn from(jid: Jid) -> Self {
jid.to_string()
}
}
impl<'a> From<JidRef<'a>> for String {
fn from(jid: JidRef<'a>) -> Self {
jid.to_string()
}
}
impl TryFrom<String> for Jid {
type Error = JidError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Jid::from_str(&value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn assert_jid_roundtrip(
input: &str,
expected_user: &str,
expected_server: &str,
expected_device: u16,
expected_agent: u8,
) {
assert_jid_parse_and_display(
input,
expected_user,
expected_server,
expected_device,
expected_agent,
input,
);
}
fn assert_jid_parse_and_display(
input: &str,
expected_user: &str,
expected_server: &str,
expected_device: u16,
expected_agent: u8,
expected_output: &str,
) {
let jid = Jid::from_str(input).unwrap_or_else(|_| panic!("Failed to parse JID: {}", input));
assert_eq!(
jid.user, expected_user,
"User part did not match for {}",
input
);
assert_eq!(
jid.server, expected_server,
"Server part did not match for {}",
input
);
assert_eq!(
jid.device, expected_device,
"Device part did not match for {}",
input
);
assert_eq!(
jid.agent, expected_agent,
"Agent part did not match for {}",
input
);
let formatted = jid.to_string();
assert_eq!(
formatted, expected_output,
"Formatted string did not match expected output for {}",
input
);
}
#[test]
fn test_jid_parsing_and_display_roundtrip() {
assert_jid_roundtrip(
"1234567890@s.whatsapp.net",
"1234567890",
"s.whatsapp.net",
0,
0,
);
assert_jid_roundtrip(
"1234567890:15@s.whatsapp.net",
"1234567890",
"s.whatsapp.net",
15,
0,
);
assert_jid_roundtrip("123-456@g.us", "123-456", "g.us", 0, 0);
assert_jid_roundtrip("s.whatsapp.net", "", "s.whatsapp.net", 0, 0);
assert_jid_roundtrip("12345.6789@lid", "12345.6789", "lid", 0, 0);
assert_jid_roundtrip("12345.6789:25@lid", "12345.6789", "lid", 25, 0);
}
#[test]
fn test_special_from_str_parsing() {
let jid = Jid::from_str("1234567890.2:15@hosted").expect("test hosted JID should be valid");
assert_eq!(jid.user, "1234567890");
assert_eq!(jid.server, "hosted");
assert_eq!(jid.device, 15);
assert_eq!(jid.agent, 2);
}
#[test]
fn test_manual_jid_formatting_edge_cases() {
let jid1 = Jid {
user: "1234567890".into(),
server: Server::Pn,
device: 15,
agent: 2,
integrator: 0,
};
assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net");
let jid2 = Jid {
user: "12345.6789".into(),
server: Server::Lid,
device: 25,
agent: 1,
integrator: 0,
};
assert_eq!(jid2.to_string(), "12345.6789:25@lid");
let jid3 = Jid {
user: "1234567890".into(),
server: Server::Hosted,
device: 15,
agent: 2,
integrator: 0,
};
assert_eq!(jid3.to_string(), "1234567890:15@hosted");
let jid4 = Jid {
user: "user".into(),
server: Server::Bot,
device: 10,
agent: 5,
integrator: 0,
};
assert_eq!(jid4.to_string(), "user.5:10@bot");
}
#[test]
fn test_invalid_jids_should_fail_to_parse() {
assert!(Jid::from_str("thisisnotajid").is_err());
assert!(Jid::from_str("").is_err());
assert!(Jid::from_str("@s.whatsapp.net").is_ok());
assert!(Jid::from_str("@unknown.server").is_err());
assert!(Jid::from_str("2").is_err());
}
#[test]
fn test_is_hosted_device_detection() {
let cloud_api_device: Jid = "5511999887766:99@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
cloud_api_device.is_hosted(),
"Device ID 99 on s.whatsapp.net should be detected as hosted (Cloud API)"
);
let cloud_api_lid: Jid = "100000012345678:99@lid"
.parse()
.expect("test JID should be valid");
assert!(
cloud_api_lid.is_hosted(),
"Device ID 99 on lid server should be detected as hosted"
);
let hosted_server: Jid = "5511999887766:99@hosted"
.parse()
.expect("test JID should be valid");
assert!(
hosted_server.is_hosted(),
"JID with @hosted server should be detected as hosted"
);
let hosted_lid_server: Jid = "100000012345678:99@hosted.lid"
.parse()
.expect("test JID should be valid");
assert!(
hosted_lid_server.is_hosted(),
"JID with @hosted.lid server should be detected as hosted"
);
let hosted_server_other_device: Jid = "5511999887766:0@hosted"
.parse()
.expect("test JID should be valid");
assert!(
hosted_server_other_device.is_hosted(),
"JID with @hosted server should be hosted regardless of device ID"
);
let regular_phone: Jid = "5511999887766:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!regular_phone.is_hosted(),
"Regular phone device (ID 0) should NOT be hosted"
);
let companion_device: Jid = "5511999887766:33@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!companion_device.is_hosted(),
"Companion device (ID 33) should NOT be hosted"
);
let regular_lid: Jid = "100000012345678:0@lid"
.parse()
.expect("test JID should be valid");
assert!(
!regular_lid.is_hosted(),
"Regular LID device should NOT be hosted"
);
let lid_companion: Jid = "100000012345678:33@lid"
.parse()
.expect("test JID should be valid");
assert!(
!lid_companion.is_hosted(),
"LID companion device (ID 33) should NOT be hosted"
);
let group_jid: Jid = "120363012345678@g.us"
.parse()
.expect("test JID should be valid");
assert!(
!group_jid.is_hosted(),
"Group JID should NOT be detected as hosted"
);
let user_jid: Jid = "5511999887766@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!user_jid.is_hosted(),
"User JID without device should NOT be hosted"
);
let bot_jid: Jid = "13136555001:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid");
assert!(
!bot_jid.is_hosted(),
"Bot JID should NOT be detected as hosted (different mechanism)"
);
}
#[test]
fn test_hosted_device_filtering_for_groups() {
let devices: Vec<Jid> = vec![
"5511999887766:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "5511999887766:33@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "5521988776655:0@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "100000012345678:0@lid"
.parse()
.expect("test JID should be valid"), "100000012345678:33@lid"
.parse()
.expect("test JID should be valid"), "5531977665544:99@s.whatsapp.net"
.parse()
.expect("test JID should be valid"), "100000087654321:99@lid"
.parse()
.expect("test JID should be valid"), "5541966554433:99@hosted"
.parse()
.expect("test JID should be valid"), ];
let filtered: Vec<&Jid> = devices.iter().filter(|jid| !jid.is_hosted()).collect();
assert_eq!(
filtered.len(),
5,
"Should have 5 non-hosted devices after filtering"
);
for jid in &filtered {
assert!(
!jid.is_hosted(),
"Filtered list should not contain hosted devices: {}",
jid
);
}
let hosted_count = devices.iter().filter(|jid| jid.is_hosted()).count();
assert_eq!(hosted_count, 3, "Should have filtered out 3 hosted devices");
}
#[test]
fn test_jid_pn_factory() {
let jid = Jid::pn("1234567890");
assert_eq!(jid.user, "1234567890");
assert_eq!(jid.server, DEFAULT_USER_SERVER);
assert_eq!(jid.device, 0);
assert!(jid.is_pn());
}
#[test]
fn test_jid_lid_factory() {
let jid = Jid::lid("100000012345678");
assert_eq!(jid.user, "100000012345678");
assert_eq!(jid.server, HIDDEN_USER_SERVER);
assert_eq!(jid.device, 0);
assert!(jid.is_lid());
}
#[test]
fn test_jid_group_factory() {
let jid = Jid::group("123456789-1234567890");
assert_eq!(jid.user, "123456789-1234567890");
assert_eq!(jid.server, GROUP_SERVER);
assert!(jid.is_group());
}
#[test]
fn test_jid_pn_device_factory() {
let jid = Jid::pn_device("1234567890", 5);
assert_eq!(jid.user, "1234567890");
assert_eq!(jid.server, DEFAULT_USER_SERVER);
assert_eq!(jid.device, 5);
assert!(jid.is_pn());
assert!(jid.is_ad());
}
#[test]
fn test_jid_lid_device_factory() {
let jid = Jid::lid_device("100000012345678", 33);
assert_eq!(jid.user, "100000012345678");
assert_eq!(jid.server, HIDDEN_USER_SERVER);
assert_eq!(jid.device, 33);
assert!(jid.is_lid());
assert!(jid.is_ad());
}
#[test]
fn test_status_broadcast_jid() {
let jid = Jid::status_broadcast();
assert_eq!(jid.user, STATUS_BROADCAST_USER);
assert_eq!(jid.server, BROADCAST_SERVER);
assert_eq!(jid.device, 0);
assert!(jid.is_status_broadcast());
assert!(!jid.is_group());
assert!(!jid.is_broadcast_list());
assert_eq!(jid.to_string(), "status@broadcast");
let parsed: Jid = "status@broadcast".parse().expect("should parse");
assert!(parsed.is_status_broadcast());
assert_eq!(parsed.user, "status");
assert_eq!(parsed.server, "broadcast");
let broadcast_list = Jid::new("12345", Server::Broadcast);
assert!(broadcast_list.is_broadcast_list());
assert!(!broadcast_list.is_status_broadcast());
}
#[test]
fn test_jid_to_non_ad_preserves_user_server() {
let device_jid = Jid::pn_device("1234567890", 33);
let non_ad = device_jid.to_non_ad();
assert_eq!(non_ad.user, "1234567890");
assert_eq!(non_ad.server, DEFAULT_USER_SERVER);
assert_eq!(non_ad.device, 0);
assert!(!non_ad.is_ad());
let lid_device = Jid::lid_device("100000012345678", 25);
let lid_non_ad = lid_device.to_non_ad();
assert_eq!(lid_non_ad.user, "100000012345678");
assert_eq!(lid_non_ad.server, HIDDEN_USER_SERVER);
assert_eq!(lid_non_ad.device, 0);
let status = Jid::status_broadcast();
let status_non_ad = status.to_non_ad();
assert_eq!(status_non_ad.to_string(), "status@broadcast");
}
#[test]
fn test_jid_factories_with_string_types() {
let jid1 = Jid::pn("123");
assert_eq!(jid1.user, "123");
let jid2 = Jid::lid(String::from("456"));
assert_eq!(jid2.user, "456");
let user = "789".to_string();
let jid3 = Jid::group(user);
assert_eq!(jid3.user, "789");
}
#[test]
fn test_jid_format_parity() {
struct Case {
user: &'static str,
server: Server,
agent: u8,
device: u16,
}
let cases = [
Case {
user: "",
server: Server::Pn,
agent: 0,
device: 0,
},
Case {
user: "5511999887766",
server: Server::Pn,
agent: 0,
device: 0,
},
Case {
user: "5511999887766",
server: Server::Pn,
agent: 0,
device: 2,
},
Case {
user: "5511999887766",
server: Server::Pn,
agent: 3,
device: 15,
},
Case {
user: "12345.6789",
server: Server::Lid,
agent: 1,
device: 25,
},
Case {
user: "100000012345678",
server: Server::Hosted,
agent: 2,
device: 99,
},
Case {
user: "100000012345678",
server: Server::HostedLid,
agent: 1,
device: 99,
},
Case {
user: "120363012345678901",
server: Server::Group,
agent: 0,
device: 0,
},
Case {
user: "user",
server: Server::Bot,
agent: 5,
device: 10,
},
Case {
user: "447911123456",
server: Server::Interop,
agent: 3,
device: 0,
},
Case {
user: "messenger_user",
server: Server::Messenger,
agent: 0,
device: 50,
},
Case {
user: "status",
server: Server::Broadcast,
agent: 0,
device: 0,
},
Case {
user: "newsletter_id",
server: Server::Newsletter,
agent: 0,
device: 0,
},
Case {
user: "447911123456789",
server: Server::Pn,
agent: 255,
device: 65535,
},
Case {
user: "1",
server: Server::Legacy,
agent: 0,
device: 1,
},
];
for (i, c) in cases.iter().enumerate() {
let jid = Jid {
user: c.user.into(),
server: c.server,
agent: c.agent,
device: c.device,
integrator: 0,
};
let display = jid.to_string();
let jid_ref = JidRef {
user: NodeStr::Borrowed(c.user),
server: c.server,
agent: c.agent,
device: c.device,
integrator: 0,
};
let ref_display = jid_ref.to_string();
let mut string_buf = String::new();
push_jid_to_string(c.user, c.server, c.agent, c.device, &mut string_buf);
let mut compact_buf = CompactString::default();
push_jid_to_compact(c.user, c.server, c.agent, c.device, &mut compact_buf);
let mut push_buf = String::new();
jid.push_to(&mut push_buf);
assert_eq!(display, ref_display, "case {i}: Display vs JidRef::Display");
assert_eq!(
display, string_buf,
"case {i}: Display vs push_jid_to_string"
);
assert_eq!(
display,
compact_buf.as_str(),
"case {i}: Display vs push_jid_to_compact"
);
assert_eq!(display, push_buf, "case {i}: Display vs Jid::push_to");
}
}
}