telegram_webapp_sdk/core/
context.rs1use once_cell::unsync::OnceCell;
5use percent_encoding::{percent_decode, percent_decode_str};
6use wasm_bindgen::JsValue;
7
8use super::types::{
9 init_data::TelegramInitData, launch_params::LaunchParams, theme_params::TelegramThemeParams
10};
11
12#[derive(Clone)]
14pub struct TelegramContext {
15 pub init_data: TelegramInitData,
16 pub theme_params: TelegramThemeParams,
17 pub raw_init_data: String
18}
19
20thread_local! {
21 static CONTEXT: OnceCell<TelegramContext> = const { OnceCell::new() };
23}
24
25impl TelegramContext {
26 pub fn init(
31 init_data: TelegramInitData,
32 theme_params: TelegramThemeParams,
33 raw_init_data: String
34 ) -> Result<(), &'static str> {
35 CONTEXT.with(|cell| {
36 cell.set(TelegramContext {
37 init_data,
38 theme_params,
39 raw_init_data
40 })
41 .map_err(|_| "TelegramContext already initialized")
42 })
43 }
44
45 pub fn get<F, R>(f: F) -> Option<R>
49 where
50 F: FnOnce(&TelegramContext) -> R
51 {
52 CONTEXT.with(|cell| cell.get().map(f))
53 }
54
55 pub fn get_raw_init_data() -> Result<String, &'static str> {
80 Self::get(|ctx| ctx.raw_init_data.clone()).ok_or("TelegramContext not initialized")
81 }
82}
83
84pub fn get_launch_params() -> Result<LaunchParams, JsValue> {
98 let _ = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
99
100 Ok(LaunchParams {
101 tg_web_app_platform: get_param("tgWebAppPlatform")
102 .or_else(|| Some(String::from("web"))),
103 tg_web_app_version: get_param("tgWebAppVersion"),
104 tg_web_app_start_param: get_param("tgWebAppStartParam"),
105 tg_web_app_show_settings: get_param("tgWebAppShowSettings").map(|s| s == "1"),
106 tg_web_app_bot_inline: get_param("tgWebAppBotInline").map(|s| s == "1")
107 })
108}
109
110fn get_param(key: &str) -> Option<String> {
111 let search = web_sys::window()?.document()?.location()?.search().ok()?;
112
113 let query = search.strip_prefix('?').unwrap_or(search.as_str());
114 extract_param(query, key)
115}
116
117fn extract_param(query: &str, key: &str) -> Option<String> {
118 query.split('&').find_map(|pair| {
119 if pair.is_empty() {
120 return None;
121 }
122
123 let mut parts = pair.splitn(2, '=');
124 let current_key = parts.next()?;
125 if current_key != key {
126 return None;
127 }
128
129 let raw_value = parts.next()?;
130 decode_query_value(raw_value)
131 })
132}
133
134fn decode_query_value(raw_value: &str) -> Option<String> {
135 if raw_value.contains('+') {
136 let mut buffer = Vec::with_capacity(raw_value.len());
137 for byte in raw_value.as_bytes() {
138 if *byte == b'+' {
139 buffer.push(b' ');
140 } else {
141 buffer.push(*byte);
142 }
143 }
144
145 return percent_decode(buffer.as_slice())
146 .decode_utf8()
147 .ok()
148 .map(|cow| cow.into_owned());
149 }
150
151 percent_decode_str(raw_value)
152 .decode_utf8()
153 .ok()
154 .map(|cow| cow.into_owned())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn extract_param_returns_first_entry() {
163 let query = "tgWebAppPlatform=android&tgWebAppVersion=9.2";
164 let platform = extract_param(query, "tgWebAppPlatform");
165 assert_eq!(platform.as_deref(), Some("android"));
166 }
167
168 #[test]
169 fn decode_query_value_handles_plus_and_percent_sequences() {
170 let query = "tgWebAppStartParam=hello%2Bworld+test";
171 let value = extract_param(query, "tgWebAppStartParam");
172 assert_eq!(value.as_deref(), Some("hello+world test"));
173 }
174
175 #[cfg(target_arch = "wasm32")]
176 mod wasm {
177 use wasm_bindgen::JsValue;
178 use wasm_bindgen_test::wasm_bindgen_test;
179
180 use super::super::get_launch_params;
181
182 #[allow(dead_code)]
183 #[wasm_bindgen_test]
184 fn get_launch_params_returns_error_without_window() {
185 let err = get_launch_params().unwrap_err();
186 assert_eq!(err, JsValue::from_str("no window"));
187 }
188
189 #[wasm_bindgen_test]
190 fn get_launch_params_reads_first_query_parameter() -> Result<(), JsValue> {
191 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
192 let location = window.location();
193 let original_search = location.search().unwrap_or_default();
194
195 location.set_search(
196 "?tgWebAppPlatform=android&tgWebAppVersion=9.2&tgWebAppStartParam=hello%2Bworld+test&tgWebAppShowSettings=1&tgWebAppBotInline=0"
197 )?;
198
199 let params = get_launch_params()?;
200 assert_eq!(params.tg_web_app_platform.as_deref(), Some("android"));
201 assert_eq!(params.tg_web_app_version.as_deref(), Some("9.2"));
202 assert_eq!(
203 params.tg_web_app_start_param.as_deref(),
204 Some("hello+world test")
205 );
206 assert_eq!(params.tg_web_app_show_settings, Some(true));
207 assert_eq!(params.tg_web_app_bot_inline, Some(false));
208
209 location.set_search(&original_search)?;
210 Ok(())
211 }
212 }
213}