wacore_binary/
jid.rs

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    // REMOVE: #[error("...")]
23    InvalidFormat(String),
24    // REMOVE: #[error("...")]
25    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
46// Add From impl
47impl 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            // For LID (HIDDEN_USER_SERVER), use the parsed agent value.
145            // LID user identifiers can contain dots (e.g., "236395184570386.1"),
146            // which are part of the identity, not agent separators.
147            // Only non-device LID JIDs (without ':') may have an agent suffix.
148            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        // Special handling for LID JIDs, as their user part can contain dots
248        // that should not be interpreted as agent separators.
249        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        // Fallback to existing logic for other JID types (s.whatsapp.net, etc.)
265        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            // The agent is encoded in the server type for AD JIDs.
318            // We should NOT append it to the user string for standard servers.
319            // Only non-standard servers might use an agent suffix.
320            // The old JS logic appears to never append the agent for s.whatsapp.net or lid.
321            if self.agent > 0 {
322                // This is a guess based on the failure. The old JS logic is complex.
323                // We will only append the agent if the server is NOT s.whatsapp.net or lid.
324                // AND the server is not one that is derived *from* the agent (like 'hosted').
325                let server_str = self.server(); // Use trait method
326                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            // The agent is encoded in the server type for AD JIDs.
351            // We should NOT append it to the user string for standard servers.
352            // Only non-standard servers might use an agent suffix.
353            // The old JS logic appears to never append the agent for s.whatsapp.net or lid.
354            if self.agent > 0 {
355                // This is a guess based on the failure. The old JS logic is complex.
356                // We will only append the agent if the server is NOT s.whatsapp.net or lid.
357                // AND the server is not one that is derived *from* the agent (like 'hosted').
358                let server_str = self.server(); // Use trait method
359                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    /// Helper function to test a full parsing and display round-trip.
401    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        // 1. Test parsing from string (FromStr trait)
409        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        // 2. Test formatting back to string (Display trait)
433        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        // Standard cases
443        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        // LID JID cases (critical for the bug)
461        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        // Test parsing of JIDs with an agent, which should be stored in the struct
468        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        // This test directly validates the fixes for the parity failures.
478        // We manually construct the Jid struct as the binary decoder would,
479        // then we assert that its string representation is correct.
480
481        // Failure Case 1: An AD-JID for s.whatsapp.net decoded with an agent.
482        // The Display trait MUST NOT show the agent number.
483        let jid1 = Jid {
484            user: "1234567890".to_string(),
485            server: "s.whatsapp.net".to_string(),
486            device: 15,
487            agent: 2, // This agent would be decoded from binary but should be ignored in display
488            integrator: 0,
489        };
490        // Expected: "1234567890:15@s.whatsapp.net" (agent is omitted)
491        // Buggy: "1234567890.2:15@s.whatsapp.net"
492        assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net");
493
494        // Failure Case 2: A LID JID with a device, decoded with an agent.
495        // The Display trait MUST NOT show the agent number.
496        let jid2 = Jid {
497            user: "12345.6789".to_string(),
498            server: "lid".to_string(),
499            device: 25,
500            agent: 1, // This agent would be decoded from binary but should be ignored in display
501            integrator: 0,
502        };
503        // Expected: "12345.6789:25@lid"
504        // Buggy: "12345.6789.1:25@lid"
505        assert_eq!(jid2.to_string(), "12345.6789:25@lid");
506
507        // Failure Case 3: A JID that was decoded as "hosted" because of its agent.
508        // The Display trait MUST NOT show the agent number.
509        let jid3 = Jid {
510            user: "1234567890".to_string(),
511            server: "hosted".to_string(),
512            device: 15,
513            agent: 2,
514            integrator: 0,
515        };
516        // Expected: "1234567890:15@hosted"
517        // Buggy: "1234567890.2:15@hosted"
518        assert_eq!(jid3.to_string(), "1234567890:15@hosted");
519
520        // Verification Case: A generic JID where the agent SHOULD be displayed.
521        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        // The agent should be displayed because the server is not a special AD-JID type
529        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        // Jid::from_str("2") should not be possible due to type constraints,
538        // but if it were, it should fail. The string must contain '@'.
539        assert!(Jid::from_str("2").is_err());
540    }
541}