tg_webapp_init_data/
lib.rs

1//! Parse and validate initData for Telegram Mini Apps
2
3use std::collections::BTreeMap;
4use std::time::{Duration, SystemTime};
5
6use serde::Deserialize;
7
8pub enum Error {
9    InvalidHash,
10    MissingField(&'static str),
11    InvalidJson(&'static str, serde_json::Error),
12    InvalidNumericField(&'static str),
13}
14
15#[derive(Debug)]
16pub struct WebAppInitData {
17    // query_id: Option<String>,
18    user: Option<WebAppUser>,
19    receiver: Option<WebAppUser>,
20    // chat: Option<String>,
21    // chat_type: Option<String>,
22    // chat_instance: Option<String>,
23    // start_param: Option<String>,
24    // can_send_after: Option<i64>,
25    auth_date: u64,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct WebAppUser {
30    id: i64,
31    is_bot: Option<bool>,
32    first_name: String,
33    last_name: Option<String>,
34    username: Option<String>,
35    language_code: Option<String>,
36    #[serde(default)]
37    is_premium: bool,
38    #[serde(default)]
39    added_to_attachment_menu: bool,
40    #[serde(default)]
41    allows_write_to_pm: bool,
42    photo_url: Option<String>,
43}
44
45impl WebAppInitData {
46    pub fn new(token: &str, raw: &[u8]) -> Result<Self, Error> {
47        let mut decoded: BTreeMap<_, _> = form_urlencoded::parse(raw).collect();
48        let hash = decoded.remove("hash").ok_or(Error::MissingField("hash"))?;
49
50        let mut data_check_string = String::new();
51        for (k, v) in &decoded {
52            if !data_check_string.is_empty() {
53                data_check_string.push('\n');
54            }
55            data_check_string.push_str(k);
56            data_check_string.push('=');
57            data_check_string.push_str(v);
58        }
59
60        let secret_key = hmac_sha256::HMAC::mac(token, "WebAppData");
61        let actual_hash = hmac_sha256::HMAC::mac(&data_check_string, secret_key);
62        if hex(&actual_hash) != *hash {
63            return Err(Error::InvalidHash);
64        }
65
66        Ok(WebAppInitData {
67            user: decoded
68                .remove("user")
69                .map(|x| serde_json::from_str(&x))
70                .transpose()
71                .map_err(|e| Error::InvalidJson("user", e))?,
72            receiver: decoded
73                .remove("receiver")
74                .map(|x| serde_json::from_str(&x))
75                .transpose()
76                .map_err(|e| Error::InvalidJson("receiver", e))?,
77            auth_date: decoded
78                .remove("auth_date")
79                .ok_or(Error::MissingField("auth_date"))?
80                .parse()
81                .map_err(|_e| Error::InvalidNumericField("auth_date"))?,
82        })
83    }
84
85    pub fn user(&self) -> Option<&WebAppUser> {
86        self.user.as_ref()
87    }
88
89    pub fn receiver(&self) -> Option<&WebAppUser> {
90        self.receiver.as_ref()
91    }
92
93    pub fn elapsed_since_auth(&self) -> Option<Duration> {
94        let now = SystemTime::now()
95            .duration_since(SystemTime::UNIX_EPOCH)
96            .ok()?
97            .as_secs();
98        let secs = now.checked_sub(self.auth_date)?;
99        Some(Duration::from_secs(secs))
100    }
101}
102
103impl WebAppUser {
104    pub fn id(&self) -> i64 {
105        self.id
106    }
107
108    pub fn is_bot(&self) -> Option<bool> {
109        self.is_bot
110    }
111
112    pub fn first_name(&self) -> &str {
113        &self.first_name
114    }
115
116    pub fn last_name(&self) -> Option<&str> {
117        self.last_name.as_deref()
118    }
119
120    pub fn username(&self) -> Option<&str> {
121        self.username.as_deref()
122    }
123
124    pub fn language_code(&self) -> Option<&str> {
125        self.language_code.as_deref()
126    }
127
128    pub fn is_premium(&self) -> bool {
129        self.is_premium
130    }
131
132    pub fn added_to_attachment_menu(&self) -> bool {
133        self.added_to_attachment_menu
134    }
135
136    pub fn allows_write_to_pm(&self) -> bool {
137        self.allows_write_to_pm
138    }
139
140    pub fn photo_url(&self) -> Option<&str> {
141        self.photo_url.as_deref()
142    }
143}
144
145fn hex(bytes: &[u8]) -> String {
146    let mut result = String::with_capacity(bytes.len() * 2);
147    for byte in bytes {
148        use std::fmt::Write;
149        let _ = write!(result, "{byte:02x}");
150    }
151    result
152}