pub mod parse;
use std::thread;
use std::time::Duration;
use opentele2_core::api::Versions;
use serde_json::{Map, Value};
use ureq::Agent;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
pub const API_USER_AGENT: &str = "opentele2";
pub const TG_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36";
const API_ACCEPT: &str = "application/json, text/plain, */*";
const TG_ACCEPT: &str = "text/html,application/xhtml+xml,*/*;q=0.8";
const TDESKTOP_URL: &str = "https://api.github.com/repos/telegramdesktop/tdesktop/releases/latest";
const TELEGRAM_X_URL: &str = "https://api.github.com/repos/TGX-Android/Telegram-X/releases/latest";
const TG_ANDROID_URL: &str = "https://t.me/s/tgstable?q=android%20versions%3A";
const TG_IOS_URL: &str = "https://t.me/s/tgstable?q=ios%20versions%3A";
const PLAY_ANDROID_URL: &str = "https://play.rajkumaar.co.in/json?id=org.telegram.messenger";
const ITUNES_IOS_URL: &str = "https://itunes.apple.com/lookup?id=686449807";
const BREW_MACOS_URL: &str = "https://formulae.brew.sh/api/cask/telegram.json";
const ITUNES_MACOS_URL: &str = "https://itunes.apple.com/lookup?id=747648890";
const WEB_K_URL: &str =
"https://raw.githubusercontent.com/morethanwords/tweb/master/public/version";
const WEB_A_URL: &str =
"https://raw.githubusercontent.com/Ajaxy/telegram-tt/refs/heads/master/public/version.txt";
const CHROME_URL: &str =
"https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json";
const FIREFOX_URL: &str = "https://product-details.mozilla.org/1.0/firefox_versions.json";
trait PatchField: Sized {
const EXPECTED: &'static str;
fn to_value(&self) -> Value;
fn from_value(value: &Value) -> Option<Self>;
}
impl PatchField for String {
const EXPECTED: &'static str = "a string";
fn to_value(&self) -> Value {
Value::String(self.clone())
}
fn from_value(value: &Value) -> Option<Self> {
value.as_str().map(str::to_owned)
}
}
impl PatchField for u32 {
const EXPECTED: &'static str = "an unsigned 32-bit integer";
fn to_value(&self) -> Value {
Value::from(*self)
}
fn from_value(value: &Value) -> Option<Self> {
value.as_u64().and_then(|n| Self::try_from(n).ok())
}
}
macro_rules! versions_patch {
($($(#[$doc:meta])* $field:ident: $ty:ty,)*) => {
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct VersionsPatch {
$($(#[$doc])* pub $field: Option<$ty>,)*
}
impl VersionsPatch {
pub const FIELDS: &[&str] = &[$(stringify!($field)),*];
pub fn merge(&mut self, other: Self) {
$(
if other.$field.is_some() {
self.$field = other.$field;
}
)*
}
#[must_use]
pub fn to_map(&self) -> Map<String, Value> {
let mut map = Map::new();
$(
if let Some(value) = &self.$field {
map.insert(stringify!($field).to_owned(), value.to_value());
}
)*
map
}
pub fn from_map(map: &Map<String, Value>) -> Result<Self, String> {
let mut patch = Self::default();
for (key, value) in map {
match key.as_str() {
$(
stringify!($field) => {
patch.$field = from_json(key, value)?;
}
)*
_ => return Err(format!("unknown VersionsPatch field `{key}`")),
}
}
Ok(patch)
}
}
};
}
fn from_json<T: PatchField>(key: &str, value: &Value) -> Result<Option<T>, String> {
if value.is_null() {
return Ok(None);
}
T::from_value(value)
.map(Some)
.ok_or_else(|| format!("VersionsPatch field `{key}` must be {}", T::EXPECTED))
}
versions_patch! {
desktop_app_version: String,
android_app_version: String,
android_app_version_code: u32,
android_x_app_version: String,
ios_app_version: String,
ios_build_number: u32,
macos_app_version: String,
macos_build_number: u32,
web_k_version: String,
web_a_version: String,
chrome_version: String,
firefox_version: String,
}
impl VersionsPatch {
#[must_use]
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
pub fn apply(&self, versions: &mut Versions) {
fn set<T: Clone>(into: &mut T, from: Option<&T>) {
if let Some(value) = from {
into.clone_from(value);
}
}
set(
&mut versions.android_app_version,
self.android_app_version.as_ref(),
);
set(
&mut versions.android_app_version_code,
self.android_app_version_code.as_ref(),
);
set(
&mut versions.android_x_app_version,
self.android_x_app_version.as_ref(),
);
set(&mut versions.ios_app_version, self.ios_app_version.as_ref());
set(
&mut versions.ios_build_number,
self.ios_build_number.as_ref(),
);
set(
&mut versions.macos_app_version,
self.macos_app_version.as_ref(),
);
set(
&mut versions.macos_build_number,
self.macos_build_number.as_ref(),
);
set(&mut versions.web_k_version, self.web_k_version.as_ref());
set(&mut versions.web_a_version, self.web_a_version.as_ref());
if let Some(chrome_version) = &self.chrome_version {
versions.set_chrome_version(chrome_version.as_str());
}
set(&mut versions.firefox_version, self.firefox_version.as_ref());
}
}
type Source = fn(&Agent) -> Option<VersionsPatch>;
const SOURCES: [Source; 9] = [
fetch_tdesktop,
fetch_android,
fetch_telegram_x,
fetch_ios,
fetch_macos,
fetch_web_k,
fetch_web_a,
fetch_chrome,
fetch_firefox,
];
#[must_use]
pub fn fetch_all(timeout: Duration) -> VersionsPatch {
let agent: Agent = Agent::config_builder()
.timeout_global(Some(timeout))
.build()
.into();
let agent = &agent;
thread::scope(|scope| {
let handles: Vec<_> = SOURCES
.into_iter()
.map(|source| scope.spawn(move || source(agent)))
.collect();
let mut patch = VersionsPatch::default();
for handle in handles {
if let Ok(Some(answer)) = handle.join() {
patch.merge(answer);
}
}
patch
})
}
fn get(agent: &Agent, url: &str, user_agent: &str, accept: &str) -> Option<String> {
agent
.get(url)
.header("User-Agent", user_agent)
.header("Accept", accept)
.call()
.ok()?
.body_mut()
.read_to_string()
.ok()
}
fn get_api(agent: &Agent, url: &str) -> Option<String> {
get(agent, url, API_USER_AGENT, API_ACCEPT)
}
fn get_tg(agent: &Agent, url: &str) -> Option<String> {
get(agent, url, TG_USER_AGENT, TG_ACCEPT)
}
fn fetch_tdesktop(agent: &Agent) -> Option<VersionsPatch> {
parse::parse_tdesktop(&get_api(agent, TDESKTOP_URL)?)
}
fn fetch_android(agent: &Agent) -> Option<VersionsPatch> {
get_tg(agent, TG_ANDROID_URL)
.as_deref()
.and_then(parse::parse_android_tgstable)
.or_else(|| parse::parse_android_play(&get_api(agent, PLAY_ANDROID_URL)?))
}
fn fetch_telegram_x(agent: &Agent) -> Option<VersionsPatch> {
parse::parse_telegram_x(&get_api(agent, TELEGRAM_X_URL)?)
}
fn fetch_ios(agent: &Agent) -> Option<VersionsPatch> {
get_tg(agent, TG_IOS_URL)
.as_deref()
.and_then(parse::parse_ios_tgstable)
.or_else(|| parse::parse_ios_itunes(&get_api(agent, ITUNES_IOS_URL)?))
}
fn fetch_macos(agent: &Agent) -> Option<VersionsPatch> {
get_api(agent, BREW_MACOS_URL)
.as_deref()
.and_then(parse::parse_macos_brew)
.or_else(|| parse::parse_macos_itunes(&get_api(agent, ITUNES_MACOS_URL)?))
}
fn fetch_web_k(agent: &Agent) -> Option<VersionsPatch> {
parse::parse_web_k(&get_api(agent, WEB_K_URL)?)
}
fn fetch_web_a(agent: &Agent) -> Option<VersionsPatch> {
parse::parse_web_a(&get_api(agent, WEB_A_URL)?)
}
fn fetch_chrome(agent: &Agent) -> Option<VersionsPatch> {
parse::parse_chrome(&get_api(agent, CHROME_URL)?)
}
fn fetch_firefox(agent: &Agent) -> Option<VersionsPatch> {
parse::parse_firefox(&get_api(agent, FIREFOX_URL)?)
}