use std::hash::{Hash, Hasher};
use serde_json::{Map, Value};
use crate::devices::{self, Platform, WebClient};
use crate::{Error, Result};
pub const LAYER: i32 = 230;
fn windows_chrome_user_agent(chrome_version: &str) -> String {
format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/{chrome_version} Safari/537.36"
)
}
const WEB_API_ID: i32 = 2496;
const WEB_API_HASH: &str = "8da85b0d5bfe62527e5b244c209159c3";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Client {
Desktop,
Android,
AndroidX,
Ios,
MacOS,
WebA,
WebK,
Webogram,
}
impl Client {
pub const ALL: [Self; 8] = [
Self::Desktop,
Self::Android,
Self::AndroidX,
Self::Ios,
Self::MacOS,
Self::WebA,
Self::WebK,
Self::Webogram,
];
#[must_use]
pub fn is_web(self) -> bool {
matches!(self, Self::WebA | Self::WebK | Self::Webogram)
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Desktop => "TelegramDesktop",
Self::Android => "TelegramAndroid",
Self::AndroidX => "TelegramAndroidX",
Self::Ios => "TelegramIOS",
Self::MacOS => "TelegramMacOS",
Self::WebA => "TelegramWeb_A",
Self::WebK => "TelegramWeb_K",
Self::Webogram => "Webogram",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|client| client.name() == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Versions {
pub android_app_version: String,
pub android_app_version_code: u32,
pub android_sdk_range: (u32, u32),
pub android_latest_sdk: u32,
pub ios_app_version: String,
pub ios_build_number: u32,
pub ios_version_range: (String, String),
pub ios_system_version: String,
pub macos_system_version: String,
pub desktop_app_version: String,
pub desktop_app_version_suffix: String,
pub macos_app_version: String,
pub macos_build_number: u32,
pub android_x_app_version: String,
pub web_a_version: String,
pub web_k_version: String,
pub chrome_version: String,
pub user_agent: String,
pub firefox_version: String,
}
impl Versions {
#[must_use]
pub fn offline() -> Self {
let chrome_version = "155.0.0.0".to_owned();
Self {
android_app_version: "13.0.3".to_owned(),
android_app_version_code: 7183,
android_sdk_range: (29, 37),
android_latest_sdk: 37,
ios_app_version: "13.0".to_owned(),
ios_build_number: 35303,
ios_version_range: ("18.0".to_owned(), "27.0.1".to_owned()),
ios_system_version: "27.0.1".to_owned(),
macos_system_version: "27.0.1".to_owned(),
desktop_app_version: format_app_version(crate::tdata::APP_VERSION),
desktop_app_version_suffix: "x64".to_owned(),
macos_app_version: "13.0".to_owned(),
macos_build_number: 283_305,
android_x_app_version: "0.29.1.1817".to_owned(),
web_a_version: "12.0.47 A".to_owned(),
web_k_version: "2.2 K".to_owned(),
user_agent: windows_chrome_user_agent(&chrome_version),
chrome_version,
firefox_version: "150.0".to_owned(),
}
}
pub fn set_chrome_version(&mut self, version: impl Into<String>) {
self.chrome_version = version.into();
self.user_agent = windows_chrome_user_agent(&self.chrome_version);
}
}
#[must_use]
pub fn format_app_version(version: u32) -> String {
let major = version / 1_000_000;
let minor = version / 1000 % 1000;
let patch = version % 1000;
if patch == 0 {
format!("{major}.{minor}")
} else {
format!("{major}.{minor}.{patch}")
}
}
#[derive(Debug, Clone)]
pub struct ApiProfile {
pub api_id: i32,
pub api_hash: String,
pub device_model: String,
pub system_version: String,
pub app_version: String,
pub lang_code: String,
pub system_lang_code: String,
pub lang_pack: String,
}
impl ApiProfile {
fn identity(&self) -> (i32, &str, &str, &str, &str) {
(
self.api_id,
&self.api_hash,
&self.device_model,
&self.system_version,
&self.app_version,
)
}
pub fn from_session_json(data: &Value) -> Result<Self> {
let Value::Object(data) = data else {
return Err(invalid_session_json("the session JSON is not an object"));
};
let api_id = match data.get("app_id") {
Some(Value::Number(number)) => number
.as_i64()
.and_then(|id| i32::try_from(id).ok())
.ok_or_else(|| invalid_session_json("app_id is not an i32 integer"))?,
_ => return Err(invalid_session_json("app_id is missing or not an integer")),
};
let required = |key: &str| -> Result<String> {
optional_text(data, key)?
.ok_or_else(|| invalid_session_json(&format!("{key} is missing")))
};
let system_lang_code = match optional_text(data, "system_lang_code")? {
Some(code) if !code.is_empty() => code,
_ => optional_text(data, "system_lang_pack")?.unwrap_or_else(|| "en".to_owned()),
};
Ok(Self {
api_id,
api_hash: required("app_hash")?,
device_model: required("device")?,
system_version: required("sdk")?,
app_version: required("app_version")?,
lang_code: optional_text(data, "lang_code")?.unwrap_or_else(|| "en".to_owned()),
system_lang_code,
lang_pack: optional_text(data, "lang_pack")?.unwrap_or_default(),
})
}
#[must_use]
pub fn to_session_json(&self, extra: Option<&Map<String, Value>>) -> Value {
let mut result = Map::new();
result.insert("app_id".to_owned(), self.api_id.into());
result.insert("app_hash".to_owned(), self.api_hash.clone().into());
result.insert("device".to_owned(), self.device_model.clone().into());
result.insert("sdk".to_owned(), self.system_version.clone().into());
result.insert("app_version".to_owned(), self.app_version.clone().into());
result.insert(
"system_lang_pack".to_owned(),
self.system_lang_code.clone().into(),
);
result.insert(
"system_lang_code".to_owned(),
self.system_lang_code.clone().into(),
);
result.insert("lang_pack".to_owned(), self.lang_pack.clone().into());
result.insert("lang_code".to_owned(), self.lang_code.clone().into());
let mut extra_fields = default_extra_fields();
if let Some(extra) = extra {
extra_fields.extend(extra.clone());
}
result.extend(extra_fields);
Value::Object(result)
}
}
impl PartialEq for ApiProfile {
fn eq(&self, other: &Self) -> bool {
self.identity() == other.identity()
}
}
impl Eq for ApiProfile {}
impl Hash for ApiProfile {
fn hash<H: Hasher>(&self, state: &mut H) {
self.identity().hash(state);
}
}
fn default_extra_fields() -> Map<String, Value> {
session_json::default_extra_fields()
}
pub mod session_json {
use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JsonDefault {
Null,
Bool(bool),
Str(&'static str),
}
impl JsonDefault {
#[must_use]
pub fn to_value(self) -> Value {
match self {
Self::Null => Value::Null,
Self::Bool(flag) => Value::Bool(flag),
Self::Str(text) => Value::from(text),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Field {
pub key: &'static str,
pub attribute: &'static str,
pub default: JsonDefault,
pub fallback: Option<&'static str>,
}
pub const FIELDS: [Field; 9] = [
field("app_id", "api_id", JsonDefault::Null),
field("app_hash", "api_hash", JsonDefault::Null),
field("device", "device_model", JsonDefault::Null),
field("sdk", "system_version", JsonDefault::Null),
field("app_version", "app_version", JsonDefault::Null),
field(
"system_lang_pack",
"system_lang_code",
JsonDefault::Str("en"),
),
Field {
fallback: Some("system_lang_pack"),
..field("system_lang_code", "system_lang_code", JsonDefault::Null)
},
field("lang_pack", "lang_pack", JsonDefault::Str("")),
field("lang_code", "lang_code", JsonDefault::Str("en")),
];
pub const ATTRIBUTES: [&str; 8] = [
"api_id",
"api_hash",
"device_model",
"system_version",
"app_version",
"lang_code",
"system_lang_code",
"lang_pack",
];
pub const DEFAULT_EXTRA_FIELDS: [(&str, JsonDefault); 19] = [
("twoFA", JsonDefault::Null),
("role", JsonDefault::Str("")),
("id", JsonDefault::Null),
("phone", JsonDefault::Null),
("username", JsonDefault::Null),
("date_of_birth", JsonDefault::Null),
("date_of_birth_integrity", JsonDefault::Null),
("is_premium", JsonDefault::Bool(false)),
("has_profile_pic", JsonDefault::Bool(false)),
("spamblock", JsonDefault::Null),
("register_time", JsonDefault::Null),
("last_check_time", JsonDefault::Null),
("avatar", JsonDefault::Null),
("first_name", JsonDefault::Str("")),
("last_name", JsonDefault::Str("")),
("sex", JsonDefault::Null),
("proxy", JsonDefault::Null),
("ipv6", JsonDefault::Bool(false)),
("session_file", JsonDefault::Str("")),
];
const fn field(key: &'static str, attribute: &'static str, default: JsonDefault) -> Field {
Field {
key,
attribute,
default,
fallback: None,
}
}
#[must_use]
pub fn default_extra_fields() -> Map<String, Value> {
DEFAULT_EXTRA_FIELDS
.iter()
.map(|(key, value)| ((*key).to_owned(), value.to_value()))
.collect()
}
fn is_truthy(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(flag) => *flag,
Value::Number(number) => number.as_f64().is_some_and(|n| n.abs() > 0.0),
Value::String(text) => !text.is_empty(),
Value::Array(items) => !items.is_empty(),
Value::Object(entries) => !entries.is_empty(),
}
}
fn get(data: &Map<String, Value>, field: &Field) -> Value {
data.get(field.key)
.cloned()
.unwrap_or_else(|| field.default.to_value())
}
#[must_use]
pub fn read(data: &Map<String, Value>) -> Map<String, Value> {
let is_fallback = |key: &str| FIELDS.iter().any(|f| f.fallback == Some(key));
ATTRIBUTES
.iter()
.filter_map(|&attribute| {
let field = FIELDS
.iter()
.find(|f| f.attribute == attribute && !is_fallback(f.key))?;
let mut value = get(data, field);
if let Some(fallback) = field.fallback
&& !is_truthy(&value)
&& let Some(other) = FIELDS.iter().find(|f| f.key == fallback)
{
value = get(data, other);
}
Some((attribute.to_owned(), value))
})
.collect()
}
#[must_use]
pub fn write(
attributes: &Map<String, Value>,
extra: Option<&Map<String, Value>>,
) -> Map<String, Value> {
let mut result: Map<String, Value> = FIELDS
.iter()
.map(|field| {
let value = attributes.get(field.attribute).cloned();
(field.key.to_owned(), value.unwrap_or(Value::Null))
})
.collect();
let mut defaults = default_extra_fields();
if let Some(extra) = extra {
defaults.extend(extra.clone());
}
result.extend(defaults);
result
}
}
fn optional_text(data: &Map<String, Value>, key: &str) -> Result<Option<String>> {
match data.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(text)) => Ok(Some(text.clone())),
Some(_) => Err(invalid_session_json(&format!("{key} is not a string"))),
}
}
fn invalid_session_json(reason: &str) -> Error {
Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid session JSON: {reason}"),
))
}
#[must_use]
pub fn profile(client: Client, versions: &Versions) -> ApiProfile {
let (api_id, api_hash, device_model, system_version, app_version, lang_pack) = match client {
Client::Desktop => (
2040,
"b18441a1ff607e10a989891a5462e627",
"Desktop",
"Windows 11 x64".to_owned(),
if versions.desktop_app_version_suffix.is_empty() {
versions.desktop_app_version.clone()
} else {
format!(
"{} {}",
versions.desktop_app_version, versions.desktop_app_version_suffix
)
},
"tdesktop",
),
Client::Android => (
6,
"eb06d4abfb49dc3eeb1aeb98ae0f581e",
"samsungSM-S938B",
"SDK 36".to_owned(),
format!(
"{} ({})",
versions.android_app_version, versions.android_app_version_code
),
"android",
),
Client::AndroidX => (
21724,
"3e0cb5efcd52300aec5994fdfc5bdc16",
"samsungSM-S938B",
"SDK 36".to_owned(),
versions.android_x_app_version.clone(),
"android",
),
Client::Ios => (
10840,
"33c45224029d59cb3ad0c16134215aeb",
"iPhone 17 Pro",
versions.ios_system_version.clone(),
format!(
"{} ({}) ",
versions.ios_app_version, versions.ios_build_number
),
"ios",
),
Client::MacOS => (
2834,
"68875f756c9b437a8b916ca3de215815",
"MacBook Air M4",
versions.macos_system_version.clone(),
format!(
"{} ({}) ",
versions.macos_app_version, versions.macos_build_number
),
"macos",
),
Client::WebA => (
WEB_API_ID,
WEB_API_HASH,
versions.user_agent.as_str(),
"Windows".to_owned(),
versions.web_a_version.clone(),
"",
),
Client::WebK => (
WEB_API_ID,
WEB_API_HASH,
versions.user_agent.as_str(),
"Win32".to_owned(),
versions.web_k_version.clone(),
"macos",
),
Client::Webogram => (
WEB_API_ID,
WEB_API_HASH,
versions.user_agent.as_str(),
"Win32".to_owned(),
"0.7.0".to_owned(),
"",
),
};
ApiProfile {
api_id,
api_hash: api_hash.to_owned(),
device_model: device_model.to_owned(),
system_version,
app_version,
lang_code: "en".to_owned(),
system_lang_code: "en-US".to_owned(),
lang_pack: lang_pack.to_owned(),
}
}
pub fn generate(client: Client, seed: Option<&str>, versions: &Versions) -> Result<ApiProfile> {
let platform = match client {
Client::Desktop => return generate_desktop(None, seed, versions),
Client::Android | Client::AndroidX => Platform::Android,
Client::Ios => Platform::Ios,
Client::MacOS => Platform::MacOS { swift: true },
Client::WebA => Platform::Web {
client: WebClient::A,
},
Client::WebK | Client::Webogram => Platform::Web {
client: WebClient::K,
},
};
let device = match platform {
Platform::Web { client: web } => {
devices::pick_from(&devices::web_catalog_for(web, versions)?, platform, seed)?
}
_ => devices::pick(platform, seed)?,
};
Ok(ApiProfile {
device_model: device.model,
system_version: device.system_version,
..profile(client, versions)
})
}
fn desktop_platform(system: &str) -> Option<Platform> {
match system {
"windows" => Some(Platform::Windows),
"macos" => Some(Platform::MacOS { swift: false }),
"linux" => Some(Platform::Linux),
_ => None,
}
}
pub fn generate_desktop(
system: Option<&str>,
seed: Option<&str>,
versions: &Versions,
) -> Result<ApiProfile> {
let (system, seed) = match (system, seed) {
(Some(name), None) if desktop_platform(name).is_none() => (None, Some(name)),
other => other,
};
let platform = match system.and_then(desktop_platform) {
Some(platform) => platform,
None => devices::desktop_system(seed)?,
};
let device = devices::pick(platform, seed)?;
let version = &versions.desktop_app_version;
let app_version = match platform {
Platform::Windows => format!("{version} x64"),
Platform::MacOS { .. } => format!("{version} arm64"),
_ => version.clone(),
};
Ok(ApiProfile {
device_model: device.model,
system_version: device.system_version,
app_version,
..profile(Client::Desktop, versions)
})
}