Skip to main content

hermes_tdata/
account.rs

1//! Account representation
2
3use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4
5use crate::{AUTH_KEY_SIZE, Result};
6
7/// Telegram datacenter addresses (production)
8const DC_ADDRESSES: [(i32, Ipv4Addr, u16); 5] = [
9    (1, Ipv4Addr::new(149, 154, 175, 53), 443),
10    (2, Ipv4Addr::new(149, 154, 167, 51), 443),
11    (3, Ipv4Addr::new(149, 154, 175, 100), 443),
12    (4, Ipv4Addr::new(149, 154, 167, 91), 443),
13    (5, Ipv4Addr::new(91, 108, 56, 130), 443),
14];
15
16/// Get socket address for a datacenter
17fn get_dc_addr(dc_id: i32) -> SocketAddr {
18    DC_ADDRESSES.iter().find(|(id, _, _)| *id == dc_id).map_or_else(
19        || SocketAddr::new(IpAddr::V4(Ipv4Addr::new(149, 154, 167, 51)), 443),
20        |(_, ip, port)| SocketAddr::new(IpAddr::V4(*ip), *port),
21    )
22}
23
24/// A Telegram account extracted from tdata
25#[derive(Debug)]
26pub struct Account {
27    /// Account index (0-2)
28    index: i32,
29    /// Datacenter ID (1-5)
30    dc_id: i32,
31    /// User ID
32    user_id: i64,
33    /// Authorization key (256 bytes)
34    auth_key: [u8; AUTH_KEY_SIZE],
35}
36
37impl Account {
38    /// Create a new account
39    pub(crate) const fn new(
40        index: i32,
41        dc_id: i32,
42        user_id: i64,
43        auth_key: [u8; AUTH_KEY_SIZE],
44    ) -> Self {
45        Self { index, dc_id, user_id, auth_key }
46    }
47
48    /// Get the account index (0-2)
49    #[must_use]
50    pub const fn index(&self) -> i32 {
51        self.index
52    }
53
54    /// Get the datacenter ID (1-5)
55    #[must_use]
56    pub const fn dc_id(&self) -> i32 {
57        self.dc_id
58    }
59
60    /// Get the user ID
61    #[must_use]
62    pub const fn user_id(&self) -> i64 {
63        self.user_id
64    }
65
66    /// Get the raw auth key bytes
67    #[must_use]
68    pub const fn auth_key_bytes(&self) -> &[u8; AUTH_KEY_SIZE] {
69        &self.auth_key
70    }
71
72    /// Convert to grammers session data
73    ///
74    /// Returns the session data that can be used with grammers-client
75    pub fn to_grammers_session(&self) -> Result<grammers_session::Session> {
76        use grammers_session::Session;
77
78        let session = Session::new();
79
80        // Insert datacenter with the auth key
81        let addr = get_dc_addr(self.dc_id);
82        session.insert_dc(self.dc_id, addr, self.auth_key);
83
84        // Set the user as the "self" chat
85        if self.user_id != 0 {
86            session.set_user(self.user_id, self.dc_id, false);
87        }
88
89        Ok(session)
90    }
91
92    /// Export session as a base64 string (portable format)
93    pub fn to_session_string(&self) -> Result<String> {
94        let session = self.to_grammers_session()?;
95
96        // Serialize session to bytes and base64 encode
97        let data = session.save();
98        Ok(base64_encode(&data))
99    }
100}
101
102/// Base64 encode without external dependency
103fn base64_encode(data: &[u8]) -> String {
104    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
105
106    let mut result = String::new();
107    let mut i = 0;
108
109    while i < data.len() {
110        let b0 = data[i] as usize;
111        let b1 = if i + 1 < data.len() { data[i + 1] as usize } else { 0 };
112        let b2 = if i + 2 < data.len() { data[i + 2] as usize } else { 0 };
113
114        result.push(ALPHABET[b0 >> 2] as char);
115        result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);
116
117        if i + 1 < data.len() {
118            result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
119        } else {
120            result.push('=');
121        }
122
123        if i + 2 < data.len() {
124            result.push(ALPHABET[b2 & 0x3f] as char);
125        } else {
126            result.push('=');
127        }
128
129        i += 3;
130    }
131
132    result
133}
134
135#[cfg(test)]
136#[allow(
137    clippy::unwrap_used,
138    clippy::expect_used,
139    clippy::indexing_slicing,
140    clippy::unreadable_literal,
141    reason = "test assertions with controlled inputs"
142)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_account_creation() {
148        let auth_key = [0xAB; AUTH_KEY_SIZE];
149        let account = Account::new(0, 2, 12345678, auth_key);
150
151        assert_eq!(account.index(), 0);
152        assert_eq!(account.dc_id(), 2);
153        assert_eq!(account.user_id(), 12345678);
154        assert_eq!(account.auth_key_bytes(), &auth_key);
155    }
156
157    #[test]
158    fn test_base64_encode() {
159        assert_eq!(base64_encode(b""), "");
160        assert_eq!(base64_encode(b"f"), "Zg==");
161        assert_eq!(base64_encode(b"fo"), "Zm8=");
162        assert_eq!(base64_encode(b"foo"), "Zm9v");
163        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
164        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
165        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
166    }
167}