1use std::borrow::Cow;
2use std::fmt;
3use std::str::FromStr;
4
5pub const DEFAULT_USER_SERVER: &str = "s.whatsapp.net";
6pub const SERVER_JID: &str = "s.whatsapp.net";
7pub const GROUP_SERVER: &str = "g.us";
8pub const LEGACY_USER_SERVER: &str = "c.us";
9pub const BROADCAST_SERVER: &str = "broadcast";
10pub const HIDDEN_USER_SERVER: &str = "lid";
11pub const NEWSLETTER_SERVER: &str = "newsletter";
12pub const HOSTED_SERVER: &str = "hosted";
13pub const MESSENGER_SERVER: &str = "msgr";
14pub const INTEROP_SERVER: &str = "interop";
15pub const BOT_SERVER: &str = "bot";
16pub const STATUS_BROADCAST_USER: &str = "status";
17
18pub type MessageId = String;
19pub type MessageServerId = i32;
20#[derive(Debug)]
21pub enum JidError {
22 InvalidFormat(String),
24 Parse(std::num::ParseIntError),
26}
27
28impl fmt::Display for JidError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 JidError::InvalidFormat(s) => write!(f, "Invalid JID format: {s}"),
32 JidError::Parse(e) => write!(f, "Failed to parse component: {e}"),
33 }
34 }
35}
36
37impl std::error::Error for JidError {
38 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
39 match self {
40 JidError::Parse(e) => Some(e),
41 _ => None,
42 }
43 }
44}
45
46impl From<std::num::ParseIntError> for JidError {
48 fn from(err: std::num::ParseIntError) -> Self {
49 JidError::Parse(err)
50 }
51}
52
53pub trait JidExt {
54 fn user(&self) -> &str;
55 fn server(&self) -> &str;
56 fn device(&self) -> u16;
57 fn integrator(&self) -> u16;
58
59 fn is_ad(&self) -> bool {
60 self.device() > 0
61 && (self.server() == DEFAULT_USER_SERVER
62 || self.server() == HIDDEN_USER_SERVER
63 || self.server() == HOSTED_SERVER)
64 }
65
66 fn is_interop(&self) -> bool {
67 self.server() == INTEROP_SERVER && self.integrator() > 0
68 }
69
70 fn is_messenger(&self) -> bool {
71 self.server() == MESSENGER_SERVER && self.device() > 0
72 }
73
74 fn is_group(&self) -> bool {
75 self.server() == GROUP_SERVER
76 }
77
78 fn is_broadcast_list(&self) -> bool {
79 self.server() == BROADCAST_SERVER && self.user() != STATUS_BROADCAST_USER
80 }
81
82 fn is_bot(&self) -> bool {
83 (self.server() == DEFAULT_USER_SERVER
84 && self.device() == 0
85 && (self.user().starts_with("1313555") || self.user().starts_with("131655500")))
86 || self.server() == BOT_SERVER
87 }
88
89 fn is_empty(&self) -> bool {
90 self.server().is_empty()
91 }
92
93 fn is_same_user_as(&self, other: &impl JidExt) -> bool {
94 self.user() == other.user()
95 }
96}
97
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
100pub struct Jid {
101 pub user: String,
102 pub server: String,
103 pub agent: u8,
104 pub device: u16,
105 pub integrator: u16,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash)]
109pub struct JidRef<'a> {
110 pub user: Cow<'a, str>,
111 pub server: Cow<'a, str>,
112 pub agent: u8,
113 pub device: u16,
114 pub integrator: u16,
115}
116
117impl JidExt for Jid {
118 fn user(&self) -> &str {
119 &self.user
120 }
121 fn server(&self) -> &str {
122 &self.server
123 }
124 fn device(&self) -> u16 {
125 self.device
126 }
127 fn integrator(&self) -> u16 {
128 self.integrator
129 }
130}
131
132impl Jid {
133 pub fn new(user: &str, server: &str) -> Self {
134 Self {
135 user: user.to_string(),
136 server: server.to_string(),
137 ..Default::default()
138 }
139 }
140
141 pub fn actual_agent(&self) -> u8 {
142 match self.server.as_str() {
143 DEFAULT_USER_SERVER => 0,
144 HIDDEN_USER_SERVER => self.agent,
149 _ => self.agent,
150 }
151 }
152
153 pub fn to_non_ad(&self) -> Self {
154 Self {
155 user: self.user.clone(),
156 server: self.server.clone(),
157 integrator: self.integrator,
158 ..Default::default()
159 }
160 }
161
162 pub fn to_ad_string(&self) -> String {
163 if self.user.is_empty() {
164 self.server.clone()
165 } else {
166 format!(
167 "{}.{}:{}@{}",
168 self.user, self.agent, self.device, self.server
169 )
170 }
171 }
172}
173
174impl<'a> JidExt for JidRef<'a> {
175 fn user(&self) -> &str {
176 &self.user
177 }
178 fn server(&self) -> &str {
179 &self.server
180 }
181 fn device(&self) -> u16 {
182 self.device
183 }
184 fn integrator(&self) -> u16 {
185 self.integrator
186 }
187}
188
189impl<'a> JidRef<'a> {
190 pub fn new(user: Cow<'a, str>, server: Cow<'a, str>) -> Self {
191 Self {
192 user,
193 server,
194 agent: 0,
195 device: 0,
196 integrator: 0,
197 }
198 }
199
200 pub fn to_owned(&self) -> Jid {
201 Jid {
202 user: self.user.to_string(),
203 server: self.server.to_string(),
204 agent: self.agent,
205 device: self.device,
206 integrator: self.integrator,
207 }
208 }
209}
210
211impl FromStr for Jid {
212 type Err = JidError;
213 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 let (user_part, server) = match s.split_once('@') {
215 Some((u, s)) => (u, s.to_string()),
216 None => ("", s.to_string()),
217 };
218
219 if user_part.is_empty() {
220 if s.contains('@') {
221 return Err(JidError::InvalidFormat(
222 "Invalid JID format: empty user part".to_string(),
223 ));
224 } else {
225 let known_servers = [
226 DEFAULT_USER_SERVER,
227 GROUP_SERVER,
228 LEGACY_USER_SERVER,
229 BROADCAST_SERVER,
230 HIDDEN_USER_SERVER,
231 NEWSLETTER_SERVER,
232 HOSTED_SERVER,
233 MESSENGER_SERVER,
234 INTEROP_SERVER,
235 BOT_SERVER,
236 STATUS_BROADCAST_USER,
237 ];
238 if !known_servers.contains(&server.as_str()) {
239 return Err(JidError::InvalidFormat(format!(
240 "Invalid JID format: unknown server '{}'",
241 server
242 )));
243 }
244 }
245 }
246
247 if server == HIDDEN_USER_SERVER {
250 let (user, device) = if let Some((u, d_str)) = user_part.rsplit_once(':') {
251 (u, d_str.parse()?)
252 } else {
253 (user_part, 0)
254 };
255 return Ok(Jid {
256 user: user.to_string(),
257 server,
258 device,
259 agent: 0,
260 integrator: 0,
261 });
262 }
263
264 let mut user = user_part;
266 let mut device = 0;
267 let mut agent = 0;
268
269 if let Some((u, d_str)) = user_part.rsplit_once(':') {
270 user = u;
271 device = d_str.parse()?;
272 }
273
274 if server != DEFAULT_USER_SERVER
275 && server != HIDDEN_USER_SERVER
276 && let Some((u, last_part)) = user.rsplit_once('.')
277 && let Ok(num_val) = last_part.parse::<u16>()
278 {
279 user = u;
280 agent = num_val as u8;
281 }
282
283 if let Some((u, last_part)) = user_part.rsplit_once('.')
284 && let Ok(num_val) = last_part.parse::<u16>()
285 {
286 if server == DEFAULT_USER_SERVER {
287 user = u;
288 device = num_val;
289 } else {
290 user = u;
291 if num_val > u8::MAX as u16 {
292 return Err(JidError::InvalidFormat(format!(
293 "Agent component out of range: {num_val}"
294 )));
295 }
296 agent = num_val as u8;
297 }
298 }
299
300 Ok(Jid {
301 user: user.to_string(),
302 server,
303 agent,
304 device,
305 integrator: 0,
306 })
307 }
308}
309
310impl fmt::Display for Jid {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 if self.user.is_empty() {
313 write!(f, "{}", self.server)
314 } else {
315 write!(f, "{}", self.user)?;
316
317 if self.agent > 0 {
322 let server_str = self.server(); if server_str != DEFAULT_USER_SERVER
327 && server_str != HIDDEN_USER_SERVER
328 && server_str != HOSTED_SERVER
329 {
330 write!(f, ".{}", self.agent)?;
331 }
332 }
333
334 if self.device > 0 {
335 write!(f, ":{}", self.device)?;
336 }
337
338 write!(f, "@{}", self.server)
339 }
340 }
341}
342
343impl<'a> fmt::Display for JidRef<'a> {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 if self.user.is_empty() {
346 write!(f, "{}", self.server)
347 } else {
348 write!(f, "{}", self.user)?;
349
350 if self.agent > 0 {
355 let server_str = self.server(); if server_str != DEFAULT_USER_SERVER
360 && server_str != HIDDEN_USER_SERVER
361 && server_str != HOSTED_SERVER
362 {
363 write!(f, ".{}", self.agent)?;
364 }
365 }
366
367 if self.device > 0 {
368 write!(f, ":{}", self.device)?;
369 }
370
371 write!(f, "@{}", self.server)
372 }
373 }
374}
375
376impl From<Jid> for String {
377 fn from(jid: Jid) -> Self {
378 jid.to_string()
379 }
380}
381
382impl<'a> From<JidRef<'a>> for String {
383 fn from(jid: JidRef<'a>) -> Self {
384 jid.to_string()
385 }
386}
387
388impl TryFrom<String> for Jid {
389 type Error = JidError;
390 fn try_from(value: String) -> Result<Self, Self::Error> {
391 Jid::from_str(&value)
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use std::str::FromStr;
399
400 fn assert_jid_roundtrip(
402 input: &str,
403 expected_user: &str,
404 expected_server: &str,
405 expected_device: u16,
406 expected_agent: u8,
407 ) {
408 let jid = Jid::from_str(input).unwrap_or_else(|_| panic!("Failed to parse JID: {}", input));
410
411 assert_eq!(
412 jid.user, expected_user,
413 "User part did not match for {}",
414 input
415 );
416 assert_eq!(
417 jid.server, expected_server,
418 "Server part did not match for {}",
419 input
420 );
421 assert_eq!(
422 jid.device, expected_device,
423 "Device part did not match for {}",
424 input
425 );
426 assert_eq!(
427 jid.agent, expected_agent,
428 "Agent part did not match for {}",
429 input
430 );
431
432 let formatted = jid.to_string();
434 assert_eq!(
435 formatted, input,
436 "Formatted string did not match original input"
437 );
438 }
439
440 #[test]
441 fn test_jid_parsing_and_display_roundtrip() {
442 assert_jid_roundtrip(
444 "1234567890@s.whatsapp.net",
445 "1234567890",
446 "s.whatsapp.net",
447 0,
448 0,
449 );
450 assert_jid_roundtrip(
451 "1234567890:15@s.whatsapp.net",
452 "1234567890",
453 "s.whatsapp.net",
454 15,
455 0,
456 );
457 assert_jid_roundtrip("123-456@g.us", "123-456", "g.us", 0, 0);
458 assert_jid_roundtrip("s.whatsapp.net", "", "s.whatsapp.net", 0, 0);
459
460 assert_jid_roundtrip("12345.6789@lid", "12345.6789", "lid", 0, 0);
462 assert_jid_roundtrip("12345.6789:25@lid", "12345.6789", "lid", 25, 0);
463 }
464
465 #[test]
466 fn test_special_from_str_parsing() {
467 let jid = Jid::from_str("1234567890.2:15@hosted").unwrap();
469 assert_eq!(jid.user, "1234567890");
470 assert_eq!(jid.server, "hosted");
471 assert_eq!(jid.device, 15);
472 assert_eq!(jid.agent, 2);
473 }
474
475 #[test]
476 fn test_manual_jid_formatting_edge_cases() {
477 let jid1 = Jid {
484 user: "1234567890".to_string(),
485 server: "s.whatsapp.net".to_string(),
486 device: 15,
487 agent: 2, integrator: 0,
489 };
490 assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net");
493
494 let jid2 = Jid {
497 user: "12345.6789".to_string(),
498 server: "lid".to_string(),
499 device: 25,
500 agent: 1, integrator: 0,
502 };
503 assert_eq!(jid2.to_string(), "12345.6789:25@lid");
506
507 let jid3 = Jid {
510 user: "1234567890".to_string(),
511 server: "hosted".to_string(),
512 device: 15,
513 agent: 2,
514 integrator: 0,
515 };
516 assert_eq!(jid3.to_string(), "1234567890:15@hosted");
519
520 let jid4 = Jid {
522 user: "user".to_string(),
523 server: "custom.net".to_string(),
524 device: 10,
525 agent: 5,
526 integrator: 0,
527 };
528 assert_eq!(jid4.to_string(), "user.5:10@custom.net");
530 }
531
532 #[test]
533 fn test_invalid_jids_should_fail_to_parse() {
534 assert!(Jid::from_str("thisisnotajid").is_err());
535 assert!(Jid::from_str("").is_err());
536 assert!(Jid::from_str("@s.whatsapp.net").is_err());
537 assert!(Jid::from_str("2").is_err());
540 }
541}