1use crate::node::NodeStr;
2use compact_str::CompactString;
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy)]
9pub struct ParsedJidParts<'a> {
10 pub user: &'a str,
11 pub server: &'a str,
12 pub agent: u8,
13 pub device: u16,
14 pub integrator: u16,
15}
16
17#[inline]
23pub fn parse_jid_fast(s: &str) -> Option<ParsedJidParts<'_>> {
24 if s.is_empty() {
25 return None;
26 }
27
28 let bytes = s.as_bytes();
29
30 let mut at_pos: Option<usize> = None;
32 let mut colon_pos: Option<usize> = None;
33 let mut last_dot_pos: Option<usize> = None;
34
35 for (i, &b) in bytes.iter().enumerate() {
36 match b {
37 b'@' if at_pos.is_none() => at_pos = Some(i),
38 b':' if at_pos.is_none() => colon_pos = Some(i),
40 b'.' if at_pos.is_none() && colon_pos.is_none() => last_dot_pos = Some(i),
42 _ => {}
43 }
44 }
45
46 let at = match at_pos {
48 Some(pos) => pos,
49 None => {
50 return None;
52 }
53 };
54
55 let user_part = &s[..at];
56 let server = &s[at + 1..];
57
58 if user_part.is_empty() {
60 return None;
61 }
62
63 if server == HIDDEN_USER_SERVER {
65 let (user, device) = match colon_pos {
66 Some(pos) if pos < at => {
67 let device_slice = &s[pos + 1..at];
68 (&s[..pos], device_slice.parse::<u16>().unwrap_or(0))
69 }
70 _ => (user_part, 0),
71 };
72 return Some(ParsedJidParts {
73 user,
74 server,
75 agent: 0,
76 device,
77 integrator: 0,
78 });
79 }
80
81 if server == DEFAULT_USER_SERVER {
83 if let Some(pos) = colon_pos {
85 let user_end = pos;
86 let device_start = pos + 1;
87 let device_slice = &s[device_start..at];
88 let device = device_slice.parse::<u16>().unwrap_or(0);
89 return Some(ParsedJidParts {
90 user: &s[..user_end],
91 server,
92 agent: 0,
93 device,
94 integrator: 0,
95 });
96 }
97 if let Some(dot_pos) = last_dot_pos {
99 let suffix = &s[dot_pos + 1..at];
101 if let Ok(device_val) = suffix.parse::<u16>() {
102 return Some(ParsedJidParts {
103 user: &s[..dot_pos],
104 server,
105 agent: 0,
106 device: device_val,
107 integrator: 0,
108 });
109 }
110 }
111 return Some(ParsedJidParts {
113 user: user_part,
114 server,
115 agent: 0,
116 device: 0,
117 integrator: 0,
118 });
119 }
120
121 let (user_before_colon, device) = match colon_pos {
123 Some(pos) => {
124 let user_end = pos;
126 let device_start = pos + 1;
127 let device_slice = &s[device_start..at];
128 (&s[..user_end], device_slice.parse::<u16>().unwrap_or(0))
129 }
130 None => (user_part, 0),
131 };
132
133 let user_to_check = user_before_colon;
135 let (final_user, agent) = {
136 if let Some(dot_pos) = user_to_check.rfind('.') {
137 let suffix = &user_to_check[dot_pos + 1..];
138 if let Ok(agent_val) = suffix.parse::<u16>() {
139 if agent_val <= u8::MAX as u16 {
140 (&user_to_check[..dot_pos], agent_val as u8)
141 } else {
142 (user_to_check, 0)
143 }
144 } else {
145 (user_to_check, 0)
146 }
147 } else {
148 (user_to_check, 0)
149 }
150 };
151
152 Some(ParsedJidParts {
153 user: final_user,
154 server,
155 agent,
156 device,
157 integrator: 0,
158 })
159}
160
161#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
166#[repr(u8)]
167pub enum Server {
168 #[default]
169 Pn = 0,
170 Lid = 1,
171 Group = 2,
172 Broadcast = 3,
173 Newsletter = 4,
174 Hosted = 5,
175 HostedLid = 6,
176 Messenger = 7,
177 Interop = 8,
178 Bot = 9,
179 Legacy = 10,
180}
181
182#[cfg(feature = "serde")]
183impl serde::Serialize for Server {
184 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
185 serializer.serialize_str(self.as_str())
186 }
187}
188
189#[cfg(feature = "serde")]
190impl<'de> serde::Deserialize<'de> for Server {
191 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
192 let s = <&str>::deserialize(deserializer)?;
193 Server::try_from(s).map_err(serde::de::Error::custom)
194 }
195}
196
197impl Server {
198 #[inline]
199 pub fn as_str(self) -> &'static str {
200 match self {
201 Self::Pn => "s.whatsapp.net",
202 Self::Lid => "lid",
203 Self::Group => "g.us",
204 Self::Broadcast => "broadcast",
205 Self::Newsletter => "newsletter",
206 Self::Hosted => "hosted",
207 Self::HostedLid => "hosted.lid",
208 Self::Messenger => "msgr",
209 Self::Interop => "interop",
210 Self::Bot => "bot",
211 Self::Legacy => "c.us",
212 }
213 }
214
215 #[inline]
218 pub fn is_pn_family(self) -> bool {
219 matches!(self, Self::Pn | Self::Hosted)
220 }
221
222 #[inline]
224 pub fn is_lid_family(self) -> bool {
225 matches!(self, Self::Lid | Self::HostedLid)
226 }
227}
228
229impl fmt::Display for Server {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 f.write_str(self.as_str())
232 }
233}
234
235impl PartialEq<str> for Server {
236 fn eq(&self, other: &str) -> bool {
237 self.as_str() == other
238 }
239}
240
241impl PartialEq<&str> for Server {
242 fn eq(&self, other: &&str) -> bool {
243 self.as_str() == *other
244 }
245}
246
247impl TryFrom<&str> for Server {
248 type Error = JidError;
249 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
250 match s {
251 "s.whatsapp.net" => Ok(Self::Pn),
252 "lid" => Ok(Self::Lid),
253 "g.us" => Ok(Self::Group),
254 "broadcast" => Ok(Self::Broadcast),
255 "newsletter" => Ok(Self::Newsletter),
256 "hosted" => Ok(Self::Hosted),
257 "hosted.lid" => Ok(Self::HostedLid),
258 "msgr" => Ok(Self::Messenger),
259 "interop" => Ok(Self::Interop),
260 "bot" => Ok(Self::Bot),
261 "c.us" => Ok(Self::Legacy),
262 other => Err(JidError::InvalidFormat(format!("unknown server: {other}"))),
263 }
264 }
265}
266
267pub const DEFAULT_USER_SERVER: &str = "s.whatsapp.net";
269pub const SERVER_JID: &str = "s.whatsapp.net";
270pub const GROUP_SERVER: &str = "g.us";
271pub const LEGACY_USER_SERVER: &str = "c.us";
272pub const BROADCAST_SERVER: &str = "broadcast";
273pub const HIDDEN_USER_SERVER: &str = "lid";
274pub const NEWSLETTER_SERVER: &str = "newsletter";
275pub const HOSTED_SERVER: &str = "hosted";
276pub const HOSTED_LID_SERVER: &str = "hosted.lid";
277pub const MESSENGER_SERVER: &str = "msgr";
278pub const INTEROP_SERVER: &str = "interop";
279pub const BOT_SERVER: &str = "bot";
280pub const STATUS_BROADCAST_USER: &str = "status";
281
282pub type MessageId = String;
283pub type MessageServerId = i32;
284#[derive(Debug)]
285pub enum JidError {
286 InvalidFormat(String),
288 Parse(std::num::ParseIntError),
290}
291
292impl fmt::Display for JidError {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 match self {
295 JidError::InvalidFormat(s) => write!(f, "Invalid JID format: {s}"),
296 JidError::Parse(e) => write!(f, "Failed to parse component: {e}"),
297 }
298 }
299}
300
301impl std::error::Error for JidError {
302 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
303 match self {
304 JidError::Parse(e) => Some(e),
305 _ => None,
306 }
307 }
308}
309
310impl From<std::num::ParseIntError> for JidError {
312 fn from(err: std::num::ParseIntError) -> Self {
313 JidError::Parse(err)
314 }
315}
316
317pub trait JidExt {
318 fn user(&self) -> &str;
319 fn server(&self) -> Server;
320 fn device(&self) -> u16;
321 fn integrator(&self) -> u16;
322
323 fn is_ad(&self) -> bool {
324 self.device() > 0
325 && matches!(
326 self.server(),
327 Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
328 )
329 }
330
331 fn is_interop(&self) -> bool {
332 self.server() == Server::Interop && self.integrator() > 0
333 }
334
335 fn is_messenger(&self) -> bool {
336 self.server() == Server::Messenger && self.device() > 0
337 }
338
339 fn is_group(&self) -> bool {
340 self.server() == Server::Group
341 }
342
343 fn is_broadcast_list(&self) -> bool {
344 self.server() == Server::Broadcast && self.user() != STATUS_BROADCAST_USER
345 }
346
347 fn is_status_broadcast(&self) -> bool {
348 self.server() == Server::Broadcast && self.user() == STATUS_BROADCAST_USER
349 }
350
351 fn is_bot(&self) -> bool {
352 (self.server() == Server::Pn
353 && self.device() == 0
354 && (self.user().starts_with("1313555") || self.user().starts_with("131655500")))
355 || self.server() == Server::Bot
356 }
357
358 fn is_newsletter(&self) -> bool {
359 self.server() == Server::Newsletter
360 }
361
362 fn is_hosted(&self) -> bool {
366 self.device() == 99 || matches!(self.server(), Server::Hosted | Server::HostedLid)
367 }
368
369 fn is_empty(&self) -> bool {
370 self.user().is_empty()
371 }
372
373 fn is_same_user_as(&self, other: &impl JidExt) -> bool {
374 self.user() == other.user()
375 }
376}
377
378#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
379#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
380pub struct Jid {
381 pub user: CompactString,
382 pub server: Server,
383 pub agent: u8,
384 pub device: u16,
385 pub integrator: u16,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Hash, yoke::Yokeable)]
389pub struct JidRef<'a> {
390 pub user: NodeStr<'a>,
391 pub server: Server,
392 pub agent: u8,
393 pub device: u16,
394 pub integrator: u16,
395}
396
397impl JidExt for Jid {
398 fn user(&self) -> &str {
399 &self.user
400 }
401 fn server(&self) -> Server {
402 self.server
403 }
404 fn device(&self) -> u16 {
405 self.device
406 }
407 fn integrator(&self) -> u16 {
408 self.integrator
409 }
410}
411
412impl Jid {
413 pub fn new(user: impl Into<CompactString>, server: Server) -> Self {
414 Self {
415 user: user.into(),
416 server,
417 ..Default::default()
418 }
419 }
420
421 pub fn pn(user: impl Into<CompactString>) -> Self {
423 Self {
424 user: user.into(),
425 server: Server::Pn,
426 ..Default::default()
427 }
428 }
429
430 pub fn lid(user: impl Into<CompactString>) -> Self {
432 Self {
433 user: user.into(),
434 server: Server::Lid,
435 ..Default::default()
436 }
437 }
438
439 pub fn status_broadcast() -> Self {
441 Self {
442 user: CompactString::from(STATUS_BROADCAST_USER),
443 server: Server::Broadcast,
444 agent: 0,
445 device: 0,
446 integrator: 0,
447 }
448 }
449
450 pub fn group(id: impl Into<CompactString>) -> Self {
452 Self {
453 user: id.into(),
454 server: Server::Group,
455 ..Default::default()
456 }
457 }
458
459 pub fn newsletter(id: impl Into<CompactString>) -> Self {
461 Self {
462 user: id.into(),
463 server: Server::Newsletter,
464 ..Default::default()
465 }
466 }
467
468 pub fn pn_device(user: impl Into<CompactString>, device: u16) -> Self {
470 Self {
471 user: user.into(),
472 server: Server::Pn,
473 device,
474 ..Default::default()
475 }
476 }
477
478 pub fn lid_device(user: impl Into<CompactString>, device: u16) -> Self {
480 Self {
481 user: user.into(),
482 server: Server::Lid,
483 device,
484 ..Default::default()
485 }
486 }
487
488 #[inline]
490 pub fn is_pn(&self) -> bool {
491 self.server == Server::Pn
492 }
493
494 #[inline]
496 pub fn is_lid(&self) -> bool {
497 self.server == Server::Lid
498 }
499
500 #[inline]
502 pub fn user_base(&self) -> &str {
503 if let Some((base, _)) = self.user.split_once(':') {
504 base
505 } else {
506 &self.user
507 }
508 }
509
510 pub fn with_device(&self, device_id: u16) -> Self {
512 Self {
513 user: self.user.clone(),
514 server: self.server,
515 agent: self.agent,
516 device: device_id,
517 integrator: self.integrator,
518 }
519 }
520
521 pub fn to_non_ad(&self) -> Self {
522 Self {
523 user: self.user.clone(),
524 server: self.server,
525 integrator: self.integrator,
526 ..Default::default()
527 }
528 }
529
530 #[inline]
533 pub fn matches_user_or_lid(&self, user: &Jid, lid: Option<&Jid>) -> bool {
534 self.is_same_user_as(user) || lid.is_some_and(|l| self.is_same_user_as(l))
535 }
536
537 pub fn normalize_for_prekey_bundle(&self) -> Self {
543 let mut jid = self.clone();
544 if matches!(jid.server, Server::Pn | Server::Lid) {
545 jid.agent = 0;
546 }
547 jid
548 }
549
550 pub fn to_ad_string(&self) -> String {
551 if self.user.is_empty() {
552 return self.server.as_str().to_string();
553 }
554 let mut s = String::with_capacity(self.user.len() + 20);
555 s.push_str(&self.user);
556 s.push('.');
557 s.push_str(itoa::Buffer::new().format(self.agent));
558 s.push(':');
559 s.push_str(itoa::Buffer::new().format(self.device));
560 s.push('@');
561 s.push_str(self.server.as_str());
562 s
563 }
564
565 #[inline]
568 pub fn push_to(&self, buf: &mut String) {
569 push_jid_to_string(&self.user, self.server, self.agent, self.device, buf);
570 }
571
572 #[inline]
574 pub fn device_eq(&self, other: &Jid) -> bool {
575 self.user == other.user && self.server == other.server && self.device == other.device
576 }
577
578 #[inline]
580 pub fn device_key(&self) -> DeviceKey<'_> {
581 DeviceKey {
582 user: &self.user,
583 server: self.server,
584 device: self.device,
585 }
586 }
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
591pub struct DeviceKey<'a> {
592 pub user: &'a str,
593 pub server: Server,
594 pub device: u16,
595}
596
597impl<'a> JidExt for JidRef<'a> {
598 fn user(&self) -> &str {
599 &self.user
600 }
601 fn server(&self) -> Server {
602 self.server
603 }
604 fn device(&self) -> u16 {
605 self.device
606 }
607 fn integrator(&self) -> u16 {
608 self.integrator
609 }
610}
611
612impl<'a> JidRef<'a> {
613 pub fn to_owned(&self) -> Jid {
614 Jid {
615 user: self.user.to_compact_string(),
616 server: self.server,
617 agent: self.agent,
618 device: self.device,
619 integrator: self.integrator,
620 }
621 }
622}
623
624#[cfg(feature = "serde")]
625impl serde::Serialize for JidRef<'_> {
626 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
627 use serde::ser::SerializeStruct;
628 let mut s = serializer.serialize_struct("Jid", 5)?;
629 s.serialize_field("user", &*self.user)?;
630 s.serialize_field("server", &self.server)?;
631 s.serialize_field("agent", &self.agent)?;
632 s.serialize_field("device", &self.device)?;
633 s.serialize_field("integrator", &self.integrator)?;
634 s.end()
635 }
636}
637
638impl FromStr for Jid {
639 type Err = JidError;
640 fn from_str(s: &str) -> Result<Self, Self::Err> {
641 if let Some(parts) = parse_jid_fast(s) {
643 return Ok(Jid {
644 user: CompactString::from(parts.user),
645 server: Server::try_from(parts.server)?,
646 agent: parts.agent,
647 device: parts.device,
648 integrator: parts.integrator,
649 });
650 }
651
652 let (user_part, server) = match s.split_once('@') {
655 Some((u, s)) => (u, s),
656 None => ("", s),
657 };
658
659 if user_part.is_empty() && Server::try_from(server).is_err() {
660 return Err(JidError::InvalidFormat(format!(
661 "unknown server '{server}'"
662 )));
663 }
664
665 if server == HIDDEN_USER_SERVER {
668 let (user, device) = if let Some((u, d_str)) = user_part.rsplit_once(':') {
669 (u, d_str.parse()?)
670 } else {
671 (user_part, 0)
672 };
673 return Ok(Jid {
674 user: CompactString::from(user),
675 server: Server::try_from(server)?,
676 device,
677 agent: 0,
678 integrator: 0,
679 });
680 }
681
682 let mut user = user_part;
684 let mut device = 0;
685 let mut agent = 0;
686
687 if let Some((u, d_str)) = user_part.rsplit_once(':') {
688 user = u;
689 device = d_str.parse()?;
690 }
691
692 if server != DEFAULT_USER_SERVER
693 && server != HIDDEN_USER_SERVER
694 && let Some((u, last_part)) = user.rsplit_once('.')
695 && let Ok(num_val) = last_part.parse::<u16>()
696 {
697 if num_val > u8::MAX as u16 {
698 return Err(JidError::InvalidFormat(format!(
699 "Agent component out of range: {num_val}"
700 )));
701 }
702 user = u;
703 agent = num_val as u8;
704 }
705
706 Ok(Jid {
707 user: CompactString::from(user),
708 server: Server::try_from(server)?,
709 agent,
710 device,
711 integrator: 0,
712 })
713 }
714}
715
716macro_rules! write_jid {
723 (infallible $buf:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
725 let (user, server, agent, device) = ($user, $server, $agent, $device);
726 if user.is_empty() {
727 $buf.push_str(server.as_str());
728 return;
729 }
730 $buf.push_str(user);
731 if agent > 0
732 && !matches!(
733 server,
734 Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
735 )
736 {
737 $buf.push('.');
738 $buf.push_str(itoa::Buffer::new().format(agent));
739 }
740 if device > 0 {
741 $buf.push(':');
742 $buf.push_str(itoa::Buffer::new().format(device));
743 }
744 $buf.push('@');
745 $buf.push_str(server.as_str());
746 }};
747 (fallible $f:expr, $user:expr, $server:expr, $agent:expr, $device:expr) => {{
749 let (user, server, agent, device) = ($user, $server, $agent, $device);
750 if user.is_empty() {
751 return $f.write_str(server.as_str());
752 }
753 $f.write_str(user)?;
754 if agent > 0
755 && !matches!(
756 server,
757 Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid
758 )
759 {
760 $f.write_str(".")?;
761 $f.write_str(itoa::Buffer::new().format(agent))?;
762 }
763 if device > 0 {
764 $f.write_str(":")?;
765 $f.write_str(itoa::Buffer::new().format(device))?;
766 }
767 $f.write_str("@")?;
768 $f.write_str(server.as_str())
769 }};
770}
771
772#[inline]
775pub fn push_jid_to_string(user: &str, server: Server, agent: u8, device: u16, buf: &mut String) {
776 write_jid!(infallible buf, user, server, agent, device);
777}
778
779#[inline]
782pub fn push_jid_to_compact(
783 user: &str,
784 server: Server,
785 agent: u8,
786 device: u16,
787 buf: &mut CompactString,
788) {
789 write_jid!(infallible buf, user, server, agent, device);
790}
791
792impl fmt::Display for Jid {
793 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
794 write_jid!(fallible f, &*self.user, self.server, self.agent, self.device)
795 }
796}
797
798impl<'a> fmt::Display for JidRef<'a> {
799 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
800 write_jid!(fallible f, &*self.user, self.server, self.agent, self.device)
801 }
802}
803
804impl From<Jid> for String {
805 fn from(jid: Jid) -> Self {
806 jid.to_string()
807 }
808}
809
810impl<'a> From<JidRef<'a>> for String {
811 fn from(jid: JidRef<'a>) -> Self {
812 jid.to_string()
813 }
814}
815
816impl TryFrom<String> for Jid {
817 type Error = JidError;
818 fn try_from(value: String) -> Result<Self, Self::Error> {
819 Jid::from_str(&value)
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use std::str::FromStr;
827
828 fn assert_jid_roundtrip(
830 input: &str,
831 expected_user: &str,
832 expected_server: &str,
833 expected_device: u16,
834 expected_agent: u8,
835 ) {
836 assert_jid_parse_and_display(
837 input,
838 expected_user,
839 expected_server,
840 expected_device,
841 expected_agent,
842 input,
843 );
844 }
845
846 fn assert_jid_parse_and_display(
848 input: &str,
849 expected_user: &str,
850 expected_server: &str,
851 expected_device: u16,
852 expected_agent: u8,
853 expected_output: &str,
854 ) {
855 let jid = Jid::from_str(input).unwrap_or_else(|_| panic!("Failed to parse JID: {}", input));
857
858 assert_eq!(
859 jid.user, expected_user,
860 "User part did not match for {}",
861 input
862 );
863 assert_eq!(
864 jid.server, expected_server,
865 "Server part did not match for {}",
866 input
867 );
868 assert_eq!(
869 jid.device, expected_device,
870 "Device part did not match for {}",
871 input
872 );
873 assert_eq!(
874 jid.agent, expected_agent,
875 "Agent part did not match for {}",
876 input
877 );
878
879 let formatted = jid.to_string();
881 assert_eq!(
882 formatted, expected_output,
883 "Formatted string did not match expected output for {}",
884 input
885 );
886 }
887
888 #[test]
889 fn test_jid_parsing_and_display_roundtrip() {
890 assert_jid_roundtrip(
892 "1234567890@s.whatsapp.net",
893 "1234567890",
894 "s.whatsapp.net",
895 0,
896 0,
897 );
898 assert_jid_roundtrip(
899 "1234567890:15@s.whatsapp.net",
900 "1234567890",
901 "s.whatsapp.net",
902 15,
903 0,
904 );
905 assert_jid_roundtrip("123-456@g.us", "123-456", "g.us", 0, 0);
906
907 assert_jid_roundtrip("s.whatsapp.net", "", "s.whatsapp.net", 0, 0);
910
911 assert_jid_roundtrip("12345.6789@lid", "12345.6789", "lid", 0, 0);
913 assert_jid_roundtrip("12345.6789:25@lid", "12345.6789", "lid", 25, 0);
914 }
915
916 #[test]
917 fn test_special_from_str_parsing() {
918 let jid = Jid::from_str("1234567890.2:15@hosted").expect("test hosted JID should be valid");
920 assert_eq!(jid.user, "1234567890");
921 assert_eq!(jid.server, "hosted");
922 assert_eq!(jid.device, 15);
923 assert_eq!(jid.agent, 2);
924 }
925
926 #[test]
927 fn test_manual_jid_formatting_edge_cases() {
928 let jid1 = Jid {
935 user: "1234567890".into(),
936 server: Server::Pn,
937 device: 15,
938 agent: 2,
939 integrator: 0,
940 };
941 assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net");
942
943 let jid2 = Jid {
944 user: "12345.6789".into(),
945 server: Server::Lid,
946 device: 25,
947 agent: 1,
948 integrator: 0,
949 };
950 assert_eq!(jid2.to_string(), "12345.6789:25@lid");
951
952 let jid3 = Jid {
953 user: "1234567890".into(),
954 server: Server::Hosted,
955 device: 15,
956 agent: 2,
957 integrator: 0,
958 };
959 assert_eq!(jid3.to_string(), "1234567890:15@hosted");
960
961 let jid4 = Jid {
963 user: "user".into(),
964 server: Server::Bot,
965 device: 10,
966 agent: 5,
967 integrator: 0,
968 };
969 assert_eq!(jid4.to_string(), "user.5:10@bot");
970 }
971
972 #[test]
973 fn test_invalid_jids_should_fail_to_parse() {
974 assert!(Jid::from_str("thisisnotajid").is_err());
975 assert!(Jid::from_str("").is_err());
976 assert!(Jid::from_str("@s.whatsapp.net").is_ok());
978 assert!(Jid::from_str("@unknown.server").is_err());
980 assert!(Jid::from_str("2").is_err());
983 }
984
985 #[test]
1013 fn test_is_hosted_device_detection() {
1014 let cloud_api_device: Jid = "5511999887766:99@s.whatsapp.net"
1019 .parse()
1020 .expect("test JID should be valid");
1021 assert!(
1022 cloud_api_device.is_hosted(),
1023 "Device ID 99 on s.whatsapp.net should be detected as hosted (Cloud API)"
1024 );
1025
1026 let cloud_api_lid: Jid = "100000012345678:99@lid"
1028 .parse()
1029 .expect("test JID should be valid");
1030 assert!(
1031 cloud_api_lid.is_hosted(),
1032 "Device ID 99 on lid server should be detected as hosted"
1033 );
1034
1035 let hosted_server: Jid = "5511999887766:99@hosted"
1037 .parse()
1038 .expect("test JID should be valid");
1039 assert!(
1040 hosted_server.is_hosted(),
1041 "JID with @hosted server should be detected as hosted"
1042 );
1043
1044 let hosted_lid_server: Jid = "100000012345678:99@hosted.lid"
1046 .parse()
1047 .expect("test JID should be valid");
1048 assert!(
1049 hosted_lid_server.is_hosted(),
1050 "JID with @hosted.lid server should be detected as hosted"
1051 );
1052
1053 let hosted_server_other_device: Jid = "5511999887766:0@hosted"
1056 .parse()
1057 .expect("test JID should be valid");
1058 assert!(
1059 hosted_server_other_device.is_hosted(),
1060 "JID with @hosted server should be hosted regardless of device ID"
1061 );
1062
1063 let regular_phone: Jid = "5511999887766:0@s.whatsapp.net"
1067 .parse()
1068 .expect("test JID should be valid");
1069 assert!(
1070 !regular_phone.is_hosted(),
1071 "Regular phone device (ID 0) should NOT be hosted"
1072 );
1073
1074 let companion_device: Jid = "5511999887766:33@s.whatsapp.net"
1076 .parse()
1077 .expect("test JID should be valid");
1078 assert!(
1079 !companion_device.is_hosted(),
1080 "Companion device (ID 33) should NOT be hosted"
1081 );
1082
1083 let regular_lid: Jid = "100000012345678:0@lid"
1085 .parse()
1086 .expect("test JID should be valid");
1087 assert!(
1088 !regular_lid.is_hosted(),
1089 "Regular LID device should NOT be hosted"
1090 );
1091
1092 let lid_companion: Jid = "100000012345678:33@lid"
1094 .parse()
1095 .expect("test JID should be valid");
1096 assert!(
1097 !lid_companion.is_hosted(),
1098 "LID companion device (ID 33) should NOT be hosted"
1099 );
1100
1101 let group_jid: Jid = "120363012345678@g.us"
1103 .parse()
1104 .expect("test JID should be valid");
1105 assert!(
1106 !group_jid.is_hosted(),
1107 "Group JID should NOT be detected as hosted"
1108 );
1109
1110 let user_jid: Jid = "5511999887766@s.whatsapp.net"
1112 .parse()
1113 .expect("test JID should be valid");
1114 assert!(
1115 !user_jid.is_hosted(),
1116 "User JID without device should NOT be hosted"
1117 );
1118
1119 let bot_jid: Jid = "13136555001:0@s.whatsapp.net"
1121 .parse()
1122 .expect("test JID should be valid");
1123 assert!(
1124 !bot_jid.is_hosted(),
1125 "Bot JID should NOT be detected as hosted (different mechanism)"
1126 );
1127 }
1128
1129 #[test]
1140 fn test_hosted_device_filtering_for_groups() {
1141 let devices: Vec<Jid> = vec![
1143 "5511999887766:0@s.whatsapp.net"
1145 .parse()
1146 .expect("test JID should be valid"), "5511999887766:33@s.whatsapp.net"
1148 .parse()
1149 .expect("test JID should be valid"), "5521988776655:0@s.whatsapp.net"
1151 .parse()
1152 .expect("test JID should be valid"), "100000012345678:0@lid"
1154 .parse()
1155 .expect("test JID should be valid"), "100000012345678:33@lid"
1157 .parse()
1158 .expect("test JID should be valid"), "5531977665544:99@s.whatsapp.net"
1161 .parse()
1162 .expect("test JID should be valid"), "100000087654321:99@lid"
1164 .parse()
1165 .expect("test JID should be valid"), "5541966554433:99@hosted"
1167 .parse()
1168 .expect("test JID should be valid"), ];
1170
1171 let filtered: Vec<&Jid> = devices.iter().filter(|jid| !jid.is_hosted()).collect();
1173
1174 assert_eq!(
1176 filtered.len(),
1177 5,
1178 "Should have 5 non-hosted devices after filtering"
1179 );
1180
1181 for jid in &filtered {
1183 assert!(
1184 !jid.is_hosted(),
1185 "Filtered list should not contain hosted devices: {}",
1186 jid
1187 );
1188 }
1189
1190 let hosted_count = devices.iter().filter(|jid| jid.is_hosted()).count();
1192 assert_eq!(hosted_count, 3, "Should have filtered out 3 hosted devices");
1193 }
1194
1195 #[test]
1196 fn test_jid_pn_factory() {
1197 let jid = Jid::pn("1234567890");
1198 assert_eq!(jid.user, "1234567890");
1199 assert_eq!(jid.server, DEFAULT_USER_SERVER);
1200 assert_eq!(jid.device, 0);
1201 assert!(jid.is_pn());
1202 }
1203
1204 #[test]
1205 fn test_jid_lid_factory() {
1206 let jid = Jid::lid("100000012345678");
1207 assert_eq!(jid.user, "100000012345678");
1208 assert_eq!(jid.server, HIDDEN_USER_SERVER);
1209 assert_eq!(jid.device, 0);
1210 assert!(jid.is_lid());
1211 }
1212
1213 #[test]
1214 fn test_jid_group_factory() {
1215 let jid = Jid::group("123456789-1234567890");
1216 assert_eq!(jid.user, "123456789-1234567890");
1217 assert_eq!(jid.server, GROUP_SERVER);
1218 assert!(jid.is_group());
1219 }
1220
1221 #[test]
1222 fn test_jid_pn_device_factory() {
1223 let jid = Jid::pn_device("1234567890", 5);
1224 assert_eq!(jid.user, "1234567890");
1225 assert_eq!(jid.server, DEFAULT_USER_SERVER);
1226 assert_eq!(jid.device, 5);
1227 assert!(jid.is_pn());
1228 assert!(jid.is_ad());
1229 }
1230
1231 #[test]
1232 fn test_jid_lid_device_factory() {
1233 let jid = Jid::lid_device("100000012345678", 33);
1234 assert_eq!(jid.user, "100000012345678");
1235 assert_eq!(jid.server, HIDDEN_USER_SERVER);
1236 assert_eq!(jid.device, 33);
1237 assert!(jid.is_lid());
1238 assert!(jid.is_ad());
1239 }
1240
1241 #[test]
1242 fn test_status_broadcast_jid() {
1243 let jid = Jid::status_broadcast();
1244 assert_eq!(jid.user, STATUS_BROADCAST_USER);
1245 assert_eq!(jid.server, BROADCAST_SERVER);
1246 assert_eq!(jid.device, 0);
1247 assert!(jid.is_status_broadcast());
1248 assert!(!jid.is_group());
1249 assert!(!jid.is_broadcast_list());
1250 assert_eq!(jid.to_string(), "status@broadcast");
1251
1252 let parsed: Jid = "status@broadcast".parse().expect("should parse");
1254 assert!(parsed.is_status_broadcast());
1255 assert_eq!(parsed.user, "status");
1256 assert_eq!(parsed.server, "broadcast");
1257
1258 let broadcast_list = Jid::new("12345", Server::Broadcast);
1260 assert!(broadcast_list.is_broadcast_list());
1261 assert!(!broadcast_list.is_status_broadcast());
1262 }
1263
1264 #[test]
1265 fn test_jid_to_non_ad_preserves_user_server() {
1266 let device_jid = Jid::pn_device("1234567890", 33);
1268 let non_ad = device_jid.to_non_ad();
1269 assert_eq!(non_ad.user, "1234567890");
1270 assert_eq!(non_ad.server, DEFAULT_USER_SERVER);
1271 assert_eq!(non_ad.device, 0);
1272 assert!(!non_ad.is_ad());
1273
1274 let lid_device = Jid::lid_device("100000012345678", 25);
1276 let lid_non_ad = lid_device.to_non_ad();
1277 assert_eq!(lid_non_ad.user, "100000012345678");
1278 assert_eq!(lid_non_ad.server, HIDDEN_USER_SERVER);
1279 assert_eq!(lid_non_ad.device, 0);
1280
1281 let status = Jid::status_broadcast();
1283 let status_non_ad = status.to_non_ad();
1284 assert_eq!(status_non_ad.to_string(), "status@broadcast");
1285 }
1286
1287 #[test]
1288 fn test_jid_factories_with_string_types() {
1289 let jid1 = Jid::pn("123");
1291 assert_eq!(jid1.user, "123");
1292
1293 let jid2 = Jid::lid(String::from("456"));
1295 assert_eq!(jid2.user, "456");
1296
1297 let user = "789".to_string();
1299 let jid3 = Jid::group(user);
1300 assert_eq!(jid3.user, "789");
1301 }
1302
1303 #[test]
1307 fn test_jid_format_parity() {
1308 struct Case {
1309 user: &'static str,
1310 server: Server,
1311 agent: u8,
1312 device: u16,
1313 }
1314
1315 let cases = [
1316 Case {
1318 user: "",
1319 server: Server::Pn,
1320 agent: 0,
1321 device: 0,
1322 },
1323 Case {
1325 user: "5511999887766",
1326 server: Server::Pn,
1327 agent: 0,
1328 device: 0,
1329 },
1330 Case {
1332 user: "5511999887766",
1333 server: Server::Pn,
1334 agent: 0,
1335 device: 2,
1336 },
1337 Case {
1339 user: "5511999887766",
1340 server: Server::Pn,
1341 agent: 3,
1342 device: 15,
1343 },
1344 Case {
1346 user: "12345.6789",
1347 server: Server::Lid,
1348 agent: 1,
1349 device: 25,
1350 },
1351 Case {
1353 user: "100000012345678",
1354 server: Server::Hosted,
1355 agent: 2,
1356 device: 99,
1357 },
1358 Case {
1360 user: "100000012345678",
1361 server: Server::HostedLid,
1362 agent: 1,
1363 device: 99,
1364 },
1365 Case {
1367 user: "120363012345678901",
1368 server: Server::Group,
1369 agent: 0,
1370 device: 0,
1371 },
1372 Case {
1374 user: "user",
1375 server: Server::Bot,
1376 agent: 5,
1377 device: 10,
1378 },
1379 Case {
1381 user: "447911123456",
1382 server: Server::Interop,
1383 agent: 3,
1384 device: 0,
1385 },
1386 Case {
1388 user: "messenger_user",
1389 server: Server::Messenger,
1390 agent: 0,
1391 device: 50,
1392 },
1393 Case {
1395 user: "status",
1396 server: Server::Broadcast,
1397 agent: 0,
1398 device: 0,
1399 },
1400 Case {
1402 user: "newsletter_id",
1403 server: Server::Newsletter,
1404 agent: 0,
1405 device: 0,
1406 },
1407 Case {
1409 user: "447911123456789",
1410 server: Server::Pn,
1411 agent: 255,
1412 device: 65535,
1413 },
1414 Case {
1416 user: "1",
1417 server: Server::Legacy,
1418 agent: 0,
1419 device: 1,
1420 },
1421 ];
1422
1423 for (i, c) in cases.iter().enumerate() {
1424 let jid = Jid {
1425 user: c.user.into(),
1426 server: c.server,
1427 agent: c.agent,
1428 device: c.device,
1429 integrator: 0,
1430 };
1431
1432 let display = jid.to_string();
1434
1435 let jid_ref = JidRef {
1437 user: NodeStr::Borrowed(c.user),
1438 server: c.server,
1439 agent: c.agent,
1440 device: c.device,
1441 integrator: 0,
1442 };
1443 let ref_display = jid_ref.to_string();
1444
1445 let mut string_buf = String::new();
1447 push_jid_to_string(c.user, c.server, c.agent, c.device, &mut string_buf);
1448
1449 let mut compact_buf = CompactString::default();
1451 push_jid_to_compact(c.user, c.server, c.agent, c.device, &mut compact_buf);
1452
1453 let mut push_buf = String::new();
1455 jid.push_to(&mut push_buf);
1456
1457 assert_eq!(display, ref_display, "case {i}: Display vs JidRef::Display");
1458 assert_eq!(
1459 display, string_buf,
1460 "case {i}: Display vs push_jid_to_string"
1461 );
1462 assert_eq!(
1463 display,
1464 compact_buf.as_str(),
1465 "case {i}: Display vs push_jid_to_compact"
1466 );
1467 assert_eq!(display, push_buf, "case {i}: Display vs Jid::push_to");
1468 }
1469 }
1470}