1pub mod parse;
16
17use std::thread;
18use std::time::Duration;
19
20use opentele2_core::api::Versions;
21use serde_json::{Map, Value};
22use ureq::Agent;
23
24pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
26
27pub const API_USER_AGENT: &str = "opentele2";
29
30pub const TG_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
32 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36";
33
34const API_ACCEPT: &str = "application/json, text/plain, */*";
35const TG_ACCEPT: &str = "text/html,application/xhtml+xml,*/*;q=0.8";
36
37const TDESKTOP_URL: &str = "https://api.github.com/repos/telegramdesktop/tdesktop/releases/latest";
38const TELEGRAM_X_URL: &str = "https://api.github.com/repos/TGX-Android/Telegram-X/releases/latest";
39const TG_ANDROID_URL: &str = "https://t.me/s/tgstable?q=android%20versions%3A";
41const TG_IOS_URL: &str = "https://t.me/s/tgstable?q=ios%20versions%3A";
42const PLAY_ANDROID_URL: &str = "https://play.rajkumaar.co.in/json?id=org.telegram.messenger";
43const ITUNES_IOS_URL: &str = "https://itunes.apple.com/lookup?id=686449807";
44const BREW_MACOS_URL: &str = "https://formulae.brew.sh/api/cask/telegram.json";
45const ITUNES_MACOS_URL: &str = "https://itunes.apple.com/lookup?id=747648890";
46const WEB_K_URL: &str =
47 "https://raw.githubusercontent.com/morethanwords/tweb/master/public/version";
48const WEB_A_URL: &str =
49 "https://raw.githubusercontent.com/Ajaxy/telegram-tt/refs/heads/master/public/version.txt";
50const CHROME_URL: &str =
51 "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json";
52const FIREFOX_URL: &str = "https://product-details.mozilla.org/1.0/firefox_versions.json";
53
54trait PatchField: Sized {
57 const EXPECTED: &'static str;
59
60 fn to_value(&self) -> Value;
61
62 fn from_value(value: &Value) -> Option<Self>;
63}
64
65impl PatchField for String {
66 const EXPECTED: &'static str = "a string";
67
68 fn to_value(&self) -> Value {
69 Value::String(self.clone())
70 }
71
72 fn from_value(value: &Value) -> Option<Self> {
73 value.as_str().map(str::to_owned)
74 }
75}
76
77impl PatchField for u32 {
78 const EXPECTED: &'static str = "an unsigned 32-bit integer";
79
80 fn to_value(&self) -> Value {
81 Value::from(*self)
82 }
83
84 fn from_value(value: &Value) -> Option<Self> {
85 value.as_u64().and_then(|n| Self::try_from(n).ok())
86 }
87}
88
89macro_rules! versions_patch {
93 ($($(#[$doc:meta])* $field:ident: $ty:ty,)*) => {
94 #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
97 pub struct VersionsPatch {
98 $($(#[$doc])* pub $field: Option<$ty>,)*
99 }
100
101 impl VersionsPatch {
102 pub const FIELDS: &[&str] = &[$(stringify!($field)),*];
105
106 pub fn merge(&mut self, other: Self) {
108 $(
109 if other.$field.is_some() {
110 self.$field = other.$field;
111 }
112 )*
113 }
114
115 #[must_use]
118 pub fn to_map(&self) -> Map<String, Value> {
119 let mut map = Map::new();
120 $(
121 if let Some(value) = &self.$field {
122 map.insert(stringify!($field).to_owned(), value.to_value());
123 }
124 )*
125 map
126 }
127
128 pub fn from_map(map: &Map<String, Value>) -> Result<Self, String> {
136 let mut patch = Self::default();
137 for (key, value) in map {
138 match key.as_str() {
139 $(
140 stringify!($field) => {
141 patch.$field = from_json(key, value)?;
142 }
143 )*
144 _ => return Err(format!("unknown VersionsPatch field `{key}`")),
145 }
146 }
147 Ok(patch)
148 }
149 }
150 };
151}
152
153fn from_json<T: PatchField>(key: &str, value: &Value) -> Result<Option<T>, String> {
154 if value.is_null() {
155 return Ok(None);
156 }
157 T::from_value(value)
158 .map(Some)
159 .ok_or_else(|| format!("VersionsPatch field `{key}` must be {}", T::EXPECTED))
160}
161
162versions_patch! {
163 desktop_app_version: String,
166 android_app_version: String,
168 android_app_version_code: u32,
170 android_x_app_version: String,
172 ios_app_version: String,
174 ios_build_number: u32,
176 macos_app_version: String,
178 macos_build_number: u32,
180 web_k_version: String,
182 web_a_version: String,
184 chrome_version: String,
186 firefox_version: String,
188}
189
190impl VersionsPatch {
191 #[must_use]
193 pub fn is_empty(&self) -> bool {
194 *self == Self::default()
195 }
196
197 pub fn apply(&self, versions: &mut Versions) {
202 fn set<T: Clone>(into: &mut T, from: Option<&T>) {
203 if let Some(value) = from {
204 into.clone_from(value);
205 }
206 }
207 set(
208 &mut versions.android_app_version,
209 self.android_app_version.as_ref(),
210 );
211 set(
212 &mut versions.android_app_version_code,
213 self.android_app_version_code.as_ref(),
214 );
215 set(
216 &mut versions.android_x_app_version,
217 self.android_x_app_version.as_ref(),
218 );
219 set(&mut versions.ios_app_version, self.ios_app_version.as_ref());
220 set(
221 &mut versions.ios_build_number,
222 self.ios_build_number.as_ref(),
223 );
224 set(
225 &mut versions.macos_app_version,
226 self.macos_app_version.as_ref(),
227 );
228 set(
229 &mut versions.macos_build_number,
230 self.macos_build_number.as_ref(),
231 );
232 set(&mut versions.web_k_version, self.web_k_version.as_ref());
233 set(&mut versions.web_a_version, self.web_a_version.as_ref());
234 if let Some(chrome_version) = &self.chrome_version {
235 versions.set_chrome_version(chrome_version.as_str());
236 }
237 set(&mut versions.firefox_version, self.firefox_version.as_ref());
238 }
239}
240
241type Source = fn(&Agent) -> Option<VersionsPatch>;
242
243const SOURCES: [Source; 9] = [
244 fetch_tdesktop,
245 fetch_android,
246 fetch_telegram_x,
247 fetch_ios,
248 fetch_macos,
249 fetch_web_k,
250 fetch_web_a,
251 fetch_chrome,
252 fetch_firefox,
253];
254
255#[must_use]
262pub fn fetch_all(timeout: Duration) -> VersionsPatch {
263 let agent: Agent = Agent::config_builder()
264 .timeout_global(Some(timeout))
265 .build()
266 .into();
267 let agent = &agent;
268 thread::scope(|scope| {
269 let handles: Vec<_> = SOURCES
270 .into_iter()
271 .map(|source| scope.spawn(move || source(agent)))
272 .collect();
273 let mut patch = VersionsPatch::default();
274 for handle in handles {
277 if let Ok(Some(answer)) = handle.join() {
278 patch.merge(answer);
279 }
280 }
281 patch
282 })
283}
284
285fn get(agent: &Agent, url: &str, user_agent: &str, accept: &str) -> Option<String> {
286 agent
287 .get(url)
288 .header("User-Agent", user_agent)
289 .header("Accept", accept)
290 .call()
291 .ok()?
292 .body_mut()
293 .read_to_string()
294 .ok()
295}
296
297fn get_api(agent: &Agent, url: &str) -> Option<String> {
298 get(agent, url, API_USER_AGENT, API_ACCEPT)
299}
300
301fn get_tg(agent: &Agent, url: &str) -> Option<String> {
302 get(agent, url, TG_USER_AGENT, TG_ACCEPT)
303}
304
305fn fetch_tdesktop(agent: &Agent) -> Option<VersionsPatch> {
306 parse::parse_tdesktop(&get_api(agent, TDESKTOP_URL)?)
307}
308
309fn fetch_android(agent: &Agent) -> Option<VersionsPatch> {
310 get_tg(agent, TG_ANDROID_URL)
311 .as_deref()
312 .and_then(parse::parse_android_tgstable)
313 .or_else(|| parse::parse_android_play(&get_api(agent, PLAY_ANDROID_URL)?))
314}
315
316fn fetch_telegram_x(agent: &Agent) -> Option<VersionsPatch> {
317 parse::parse_telegram_x(&get_api(agent, TELEGRAM_X_URL)?)
318}
319
320fn fetch_ios(agent: &Agent) -> Option<VersionsPatch> {
321 get_tg(agent, TG_IOS_URL)
322 .as_deref()
323 .and_then(parse::parse_ios_tgstable)
324 .or_else(|| parse::parse_ios_itunes(&get_api(agent, ITUNES_IOS_URL)?))
325}
326
327fn fetch_macos(agent: &Agent) -> Option<VersionsPatch> {
328 get_api(agent, BREW_MACOS_URL)
329 .as_deref()
330 .and_then(parse::parse_macos_brew)
331 .or_else(|| parse::parse_macos_itunes(&get_api(agent, ITUNES_MACOS_URL)?))
332}
333
334fn fetch_web_k(agent: &Agent) -> Option<VersionsPatch> {
335 parse::parse_web_k(&get_api(agent, WEB_K_URL)?)
336}
337
338fn fetch_web_a(agent: &Agent) -> Option<VersionsPatch> {
339 parse::parse_web_a(&get_api(agent, WEB_A_URL)?)
340}
341
342fn fetch_chrome(agent: &Agent) -> Option<VersionsPatch> {
343 parse::parse_chrome(&get_api(agent, CHROME_URL)?)
344}
345
346fn fetch_firefox(agent: &Agent) -> Option<VersionsPatch> {
347 parse::parse_firefox(&get_api(agent, FIREFOX_URL)?)
348}