use std::fmt;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use grammers_crypto::two_factor_auth::{calculate_2fa, check_p_and_g};
use sha2::{Digest, Sha256};
use crate::api::{self, ApiProfile, Client, LAYER, Versions};
use crate::fingerprint::{self, Issue};
pub const QR_LOGIN_URL_PREFIX: &str = "tg://login?token=";
#[must_use]
pub fn qr_login_url(token: &[u8]) -> String {
format!("{QR_LOGIN_URL_PREFIX}{}", URL_SAFE_NO_PAD.encode(token))
}
pub const SRP_NUMBER_SIZE: usize = 256;
const KNOWN_GOOD_PRIME: [u8; SRP_NUMBER_SIZE] = crate::tdata::hex_bytes(concat!(
"c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f",
"48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37",
"20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64",
"2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4",
"a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754",
"fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4",
"e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f",
"0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b",
));
const SRP_MAX_DRAWS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SrpParams<'a> {
pub srp_id: i64,
pub g: i32,
pub p: &'a [u8],
pub salt1: &'a [u8],
pub salt2: &'a [u8],
pub srp_b: &'a [u8],
}
#[derive(Clone, PartialEq, Eq)]
pub struct SrpCheck {
pub srp_id: i64,
pub a: [u8; SRP_NUMBER_SIZE],
pub m1: [u8; 32],
}
impl fmt::Debug for SrpCheck {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SrpCheck")
.field("srp_id", &self.srp_id)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SrpError {
#[error("bad p/g in password")]
BadPrimeOrGenerator,
#[error("bad b in check")]
BadB,
#[error("no usable random SRP exponent was drawn")]
NoGoodRandom,
#[error("the system random source failed")]
RandomUnavailable,
}
impl SrpError {
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::BadPrimeOrGenerator => "SRP_BAD_PRIME_OR_GENERATOR",
Self::BadB => "SRP_BAD_B",
Self::NoGoodRandom => "SRP_NO_GOOD_RANDOM",
Self::RandomUnavailable => "SRP_RANDOM_UNAVAILABLE",
}
}
#[must_use]
pub fn legacy_class(self) -> &'static str {
match self {
Self::BadPrimeOrGenerator
| Self::BadB
| Self::NoGoodRandom
| Self::RandomUnavailable => "ValueError",
}
}
}
#[must_use]
pub fn srp_inputs(srp_id: Option<i64>, srp_b: Option<&[u8]>) -> (i64, Vec<u8>) {
match (srp_id, srp_b) {
(Some(srp_id), Some(srp_b)) => (srp_id, srp_b.to_vec()),
_ => (0, Vec::new()),
}
}
pub const RPC_PASSWORD_HASH_INVALID: &str = "PASSWORD_HASH_INVALID";
pub const RPC_SESSION_PASSWORD_NEEDED: &str = "SESSION_PASSWORD_NEEDED";
pub const RPC_FRESH_RESET_AUTHORISATION_FORBIDDEN: &str = "FRESH_RESET_AUTHORISATION_FORBIDDEN";
pub const RPC_HASH_INVALID: &str = "HASH_INVALID";
pub const RPC_AUTH_TOKEN_ALREADY_ACCEPTED: &str = "AUTH_TOKEN_ALREADY_ACCEPTED";
pub const RPC_AUTH_TOKEN_EXPIRED: &str = "AUTH_TOKEN_EXPIRED";
pub const RPC_AUTH_TOKEN_INVALID: &str = "AUTH_TOKEN_INVALID";
pub const USER_ID_UNRESOLVED: &str = "Could not resolve the account's user id";
pub const SIGN_UP_REQUIRED: &str = "the server asked to sign up instead of logging in";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FlowFailure {
NoPasswordProvided,
PasswordIncorrect,
AuthTokenAlreadyAccepted,
AuthTokenExpired,
AuthTokenInvalid,
QrLoginTimeout,
FreshResetForbidden,
HashInvalid,
UnsupportedPasswordAlgorithm,
SignUpRequired,
UserIdUnresolved,
SessionsUnavailable,
}
impl FlowFailure {
pub const ALL: [Self; 12] = [
Self::NoPasswordProvided,
Self::PasswordIncorrect,
Self::AuthTokenAlreadyAccepted,
Self::AuthTokenExpired,
Self::AuthTokenInvalid,
Self::QrLoginTimeout,
Self::FreshResetForbidden,
Self::HashInvalid,
Self::UnsupportedPasswordAlgorithm,
Self::SignUpRequired,
Self::UserIdUnresolved,
Self::SessionsUnavailable,
];
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::NoPasswordProvided => "NO_PASSWORD_PROVIDED",
Self::PasswordIncorrect => "PASSWORD_INCORRECT",
Self::AuthTokenAlreadyAccepted => "AUTH_TOKEN_ALREADY_ACCEPTED",
Self::AuthTokenExpired => "AUTH_TOKEN_EXPIRED",
Self::AuthTokenInvalid => "AUTH_TOKEN_INVALID",
Self::QrLoginTimeout => "QR_LOGIN_TIMEOUT",
Self::FreshResetForbidden => "FRESH_RESET_FORBIDDEN",
Self::HashInvalid => "HASH_INVALID",
Self::UnsupportedPasswordAlgorithm => "UNSUPPORTED_PASSWORD_ALGORITHM",
Self::SignUpRequired => "SIGN_UP_REQUIRED",
Self::UserIdUnresolved => "USER_ID_UNRESOLVED",
Self::SessionsUnavailable => "SESSIONS_UNAVAILABLE",
}
}
#[must_use]
pub fn legacy_class(self) -> &'static str {
match self {
Self::NoPasswordProvided => "NoPasswordProvided",
Self::PasswordIncorrect => "PasswordIncorrect",
Self::AuthTokenAlreadyAccepted => "AuthTokenAlreadyAcceptedError",
Self::AuthTokenExpired => "AuthTokenExpiredError",
Self::AuthTokenInvalid => "AuthTokenInvalidError",
Self::QrLoginTimeout => "TimeoutError",
Self::FreshResetForbidden => "FreshResetAuthorisationForbiddenError",
Self::HashInvalid => "HashInvalidError",
Self::UnsupportedPasswordAlgorithm => "ValueError",
Self::SignUpRequired => "SignUpRequired",
Self::UserIdUnresolved => "TelethonUnauthorized",
Self::SessionsUnavailable => "OpenTeleException",
}
}
}
pub fn srp_check(
params: &SrpParams<'_>,
password: &str,
rng: &mut dyn FnMut(&mut [u8]),
) -> Result<SrpCheck, SrpError> {
srp_check_with(params, password, &mut |buf| {
rng(buf);
Ok(())
})
}
pub fn srp_check_random(params: &SrpParams<'_>, password: &str) -> Result<SrpCheck, SrpError> {
srp_check_with(params, password, &mut |buf| {
crate::crypto::random_fill(buf).map_err(|_| SrpError::RandomUnavailable)
})
}
fn srp_check_with(
params: &SrpParams<'_>,
password: &str,
rng: &mut dyn FnMut(&mut [u8]) -> Result<(), SrpError>,
) -> Result<SrpCheck, SrpError> {
let p: &[u8; SRP_NUMBER_SIZE] = params
.p
.try_into()
.map_err(|_| SrpError::BadPrimeOrGenerator)?;
if !good_prime_and_generator(p, params.g) {
return Err(SrpError::BadPrimeOrGenerator);
}
let b = pad_number(params.srp_b).ok_or(SrpError::BadB)?;
if b.iter().all(|&byte| byte == 0) || b >= *p {
return Err(SrpError::BadB);
}
let mut secret = [0u8; SRP_NUMBER_SIZE];
let mut outcome = Err(SrpError::NoGoodRandom);
for _ in 0..SRP_MAX_DRAWS {
if let Err(error) = rng(&mut secret) {
outcome = Err(error);
break;
}
let (m1, a) = calculate_2fa(
params.salt1,
params.salt2,
p,
¶ms.g,
b.to_vec(),
secret.to_vec(),
password.as_bytes(),
);
let u: [u8; 32] = Sha256::new()
.chain_update(a)
.chain_update(b)
.finalize()
.into();
if is_good_mod_exp_first(&a, p) && u.iter().any(|&byte| byte != 0) {
outcome = Ok(SrpCheck {
srp_id: params.srp_id,
a,
m1,
});
break;
}
}
secret.fill(0);
std::hint::black_box(&mut secret);
outcome
}
fn good_prime_and_generator(p: &[u8; SRP_NUMBER_SIZE], g: i32) -> bool {
if *p == KNOWN_GOOD_PRIME && matches!(g, 3 | 4 | 5 | 7) {
return true;
}
p[0] & 0x80 != 0 && (2..=7).contains(&g) && check_p_and_g(p, &g)
}
fn pad_number(number: &[u8]) -> Option<[u8; SRP_NUMBER_SIZE]> {
let mut out = [0u8; SRP_NUMBER_SIZE];
let start = SRP_NUMBER_SIZE.checked_sub(number.len())?;
out[start..].copy_from_slice(number);
Some(out)
}
fn bit_length(number: &[u8]) -> usize {
number
.iter()
.position(|&byte| byte != 0)
.map_or(0, |first| {
(number.len() - first) * 8 - number[first].leading_zeros() as usize
})
}
fn is_good_mod_exp_first(value: &[u8; SRP_NUMBER_SIZE], p: &[u8; SRP_NUMBER_SIZE]) -> bool {
const MIN_BITS: usize = 2048 - 64;
if value > p {
return false;
}
let mut diff = [0u8; SRP_NUMBER_SIZE];
let mut borrow = 0u8;
for i in (0..SRP_NUMBER_SIZE).rev() {
let (digit, under) = p[i].overflowing_sub(value[i]);
let (digit, under_borrow) = digit.overflowing_sub(borrow);
diff[i] = digit;
borrow = u8::from(under || under_borrow);
}
bit_length(&diff) >= MIN_BITS && bit_length(value) >= MIN_BITS
}
pub const NO_PASSWORD_PROVIDED: &str = "Two-step verification is enabled for this account.\n\
You need to provide the `password` to argument";
pub const FRESH_RESET_FORBIDDEN: &str = "You can't logout other sessions if less than 24 hours \
have passed since you logged on the current session.";
pub const HASH_INVALID: &str = "The provided hash is invalid.";
pub const SESSIONS_UNAVAILABLE: &str = "Could not fetch the account's sessions";
pub const SRP_ALGORITHM: &str = "passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow";
#[must_use]
pub fn unsupported_password_algorithm(algorithm: &str) -> String {
format!("unsupported password algorithm {algorithm}")
}
pub const QR_LOGIN_ATTEMPTS: u32 = 5;
pub const QR_EXPIRE_SAFETY_SECONDS: u64 = 5;
pub const QR_LOGIN_TIMEOUT: &str = "Something went wrong, i couldn't perform the QR login";
pub const QR_TIMEOUT_ERROR_TYPE: &str = "<class 'TimeoutError'>";
#[must_use]
pub fn qr_attempt_timeout_warning(attempt_number: u32, error_type: &str) -> String {
format!("\nQRLoginToNewClient attempt {attempt_number} failed because {error_type}")
}
#[must_use]
pub fn qr_attempt_retry_warning(attempt_number: u32) -> String {
format!("\nQRLoginToNewClient attempt {attempt_number} failed. Retrying..")
}
pub const EXISTING_SESSION_KEPT: &str = "\nCreateNewSession - a session file with the same name \
is already existed, returning the old session";
pub const EXISTING_SESSION_REPLACED: &str = "\nCreateNewSession - a session file with the same \
name is already existed, but its api_id is different from the current one, it will be \
overwritten";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PostLoginRequest {
GetConfig,
GetNearestDc,
UpdateStatus,
GetLanguages,
GetAppUpdate,
GetTermsOfServiceUpdate,
GetCountriesList,
}
impl PostLoginRequest {
#[must_use]
pub fn tl_name(self) -> &'static str {
match self {
Self::GetConfig => "help.getConfig",
Self::GetNearestDc => "help.getNearestDc",
Self::UpdateStatus => "account.updateStatus",
Self::GetLanguages => "langpack.getLanguages",
Self::GetAppUpdate => "help.getAppUpdate",
Self::GetTermsOfServiceUpdate => "help.getTermsOfServiceUpdate",
Self::GetCountriesList => "help.getCountriesList",
}
}
}
pub const POST_LOGIN_APP_UPDATE_SOURCE: &str = "";
pub const POST_LOGIN_COUNTRIES_LANG_CODE: &str = "en";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PostLoginStep {
pub request: PostLoginRequest,
pub delay_ms: Option<(u64, u64)>,
}
impl PostLoginStep {
#[must_use]
pub fn runs_for(&self, lang_pack: &str) -> bool {
self.request != PostLoginRequest::GetLanguages || !lang_pack.is_empty()
}
}
pub const POST_LOGIN_STEPS: [PostLoginStep; 7] = [
PostLoginStep {
request: PostLoginRequest::GetConfig,
delay_ms: None,
},
PostLoginStep {
request: PostLoginRequest::GetNearestDc,
delay_ms: Some((100, 300)),
},
PostLoginStep {
request: PostLoginRequest::UpdateStatus,
delay_ms: Some((200, 400)),
},
PostLoginStep {
request: PostLoginRequest::GetLanguages,
delay_ms: Some((100, 200)),
},
PostLoginStep {
request: PostLoginRequest::GetAppUpdate,
delay_ms: Some((300, 500)),
},
PostLoginStep {
request: PostLoginRequest::GetTermsOfServiceUpdate,
delay_ms: Some((100, 300)),
},
PostLoginStep {
request: PostLoginRequest::GetCountriesList,
delay_ms: Some((50, 150)),
},
];
#[must_use]
pub fn post_login_steps(lang_pack: &str) -> Vec<PostLoginStep> {
POST_LOGIN_STEPS
.into_iter()
.filter(|step| step.runs_for(lang_pack))
.collect()
}
pub fn random_delay_ms(min_ms: u64, max_ms: u64) -> crate::Result<u64> {
uniform_inclusive(min_ms, max_ms, &mut crate::crypto::random_fill)
}
pub fn random_delay_ms_with(
min_ms: u64,
max_ms: u64,
rng: &mut dyn FnMut(&mut [u8]),
) -> crate::Result<u64> {
uniform_inclusive(min_ms, max_ms, &mut |buf| {
rng(buf);
Ok(())
})
}
fn uniform_inclusive(
min: u64,
max: u64,
fill: &mut dyn FnMut(&mut [u8]) -> crate::Result<()>,
) -> crate::Result<u64> {
if min > max {
return Err(crate::Error::InvalidInput {
what: "delay range",
reason: format!("min {min} is above max {max}"),
});
}
let mut draw = || -> crate::Result<u64> {
let mut word = [0u8; 8];
fill(&mut word)?;
Ok(u64::from_le_bytes(word))
};
let Some(span) = (max - min).checked_add(1) else {
return draw();
};
let rejected = (u64::MAX % span + 1) % span;
loop {
let word = draw()?;
if word >= rejected {
return Ok(min + word % span);
}
}
}
pub const OFFICIAL_API_IDS: [i32; 6] = [2040, 6, 21724, 10840, 2834, 2496];
#[must_use]
pub fn is_official_api_id(api_id: i32) -> bool {
OFFICIAL_API_IDS.contains(&api_id)
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct SessionRow {
pub current: bool,
pub device_model: String,
pub platform: String,
pub system_version: String,
pub api_id: i32,
pub app_name: String,
pub app_version: String,
pub official_app: bool,
}
pub const SESSION_COLUMNS: [&str; 7] = [
" ",
"Device",
"Platform",
"System",
"API_ID",
"App name",
"Official App",
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct RawAuthorization<'a> {
pub current: Option<bool>,
pub device_model: &'a str,
pub platform: &'a str,
pub system_version: &'a str,
pub api_id: i32,
pub app_name: &'a str,
pub app_version: &'a str,
pub official_app: Option<bool>,
}
impl SessionRow {
#[must_use]
pub fn from_authorization(raw: &RawAuthorization<'_>) -> Self {
Self {
current: raw.current.unwrap_or(false),
device_model: raw.device_model.to_owned(),
platform: raw.platform.to_owned(),
system_version: raw.system_version.to_owned(),
api_id: raw.api_id,
app_name: raw.app_name.to_owned(),
app_version: raw.app_version.to_owned(),
official_app: raw.official_app.unwrap_or(false),
}
}
#[must_use]
pub fn cells(&self, index: usize) -> [String; 7] {
[
if self.current {
"Current".to_owned()
} else {
index.to_string()
},
self.device_model.clone(),
self.platform.clone(),
self.system_version.clone(),
self.api_id.to_string(),
format!("{} {}", self.app_name, self.app_version),
if self.official_app { "✔" } else { "✖" }.to_owned(),
]
}
}
#[must_use]
pub fn format_sessions(rows: &[SessionRow]) -> String {
let cells: Vec<[String; 7]> = rows
.iter()
.enumerate()
.map(|(index, row)| row.cells(index))
.collect();
format_table(&SESSION_COLUMNS, &cells, &[1])
}
#[must_use]
pub fn format_table<H, R, C>(headers: &[H], rows: &[R], add_split: &[usize]) -> String
where
H: AsRef<str>,
R: AsRef<[C]>,
C: AsRef<str>,
{
fn cell<'a, R: AsRef<[C]>, C: AsRef<str> + 'a>(row: &'a R, column: usize) -> &'a str {
row.as_ref().get(column).map_or("", AsRef::as_ref)
}
let widths: Vec<usize> = headers
.iter()
.enumerate()
.map(|(column, header)| {
rows.iter()
.map(|row| cell(row, column).chars().count())
.fold(header.as_ref().chars().count(), usize::max)
})
.collect();
let splitter = format!(
"|{}|\n",
widths
.iter()
.map(|width| "-".repeat(width + 2))
.collect::<Vec<_>>()
.join("+")
);
let mut out = splitter.clone();
out.push_str(&table_line(headers.iter().map(AsRef::as_ref), &widths));
out.push_str(&splitter);
for (index, row) in rows.iter().enumerate() {
if add_split.contains(&index) {
out.push_str(&splitter);
}
let texts = (0..widths.len()).map(|column| cell(row, column));
out.push_str(&table_line(texts, &widths));
}
out.push_str(splitter.trim_end_matches('\n'));
out
}
fn table_line<'a>(texts: impl Iterator<Item = &'a str>, widths: &[usize]) -> String {
let padded: Vec<String> = texts
.zip(widths)
.map(|(text, width)| {
let space = width + 2 - text.chars().count();
let left = space / 2;
format!("{}{text}{}", " ".repeat(left), " ".repeat(space - left))
})
.collect();
format!("|{}|\n", padded.join("|"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Check {
GetConfig,
NearestDc,
CurrentSession,
LayerMatch,
LangPack,
AppUpdate,
TermsOfService,
}
impl Check {
pub const ALL: [Self; 7] = [
Self::GetConfig,
Self::NearestDc,
Self::CurrentSession,
Self::LayerMatch,
Self::LangPack,
Self::AppUpdate,
Self::TermsOfService,
];
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::GetConfig => "get_config",
Self::NearestDc => "nearest_dc",
Self::CurrentSession => "current_session",
Self::LayerMatch => "layer_match",
Self::LangPack => "lang_pack",
Self::AppUpdate => "app_update",
Self::TermsOfService => "terms_of_service",
}
}
}
pub const LAYER_TOLERANCE: u32 = 15;
pub const CURRENT_SESSION_MISSING: &str = "Could not retrieve current session";
pub const LANG_PACK_EMPTY: &str = "lang_pack is empty (web client mode)";
pub const NO_APP_UPDATE: &str = "No update available (app_version accepted)";
pub const TERMS_OF_SERVICE_OK: &str = "ToS check OK";
pub const CONSISTENCY_WARNING_PREFIX: &str = "opentele2 consistency check issues:\n";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsistencyExpectations {
pub api_id: i32,
pub official_api_id: bool,
pub lang_pack: String,
pub advertised_layer: i32,
pub layer_tolerance: u32,
pub app_version: String,
pub version_issues: Vec<Issue>,
}
impl ConsistencyExpectations {
#[must_use]
pub fn new(profile: &ApiProfile, versions: &Versions) -> Self {
Self {
api_id: profile.api_id,
official_api_id: is_official_api_id(profile.api_id),
lang_pack: profile.lang_pack.clone(),
advertised_layer: LAYER,
layer_tolerance: LAYER_TOLERANCE,
app_version: profile.app_version.clone(),
version_issues: fingerprint::check_versions(profile, versions),
}
}
#[must_use]
pub fn for_client(client: Client, versions: &Versions) -> Self {
Self::new(&api::profile(client, versions), versions)
}
#[must_use]
pub fn lang_pack_skipped(&self) -> bool {
self.lang_pack.is_empty()
}
#[must_use]
pub fn layer_matches(&self, library_layer: i32) -> bool {
library_layer.abs_diff(self.advertised_layer) <= self.layer_tolerance
}
#[must_use]
pub fn app_version_current(&self) -> bool {
self.version_issues.is_empty()
}
#[must_use]
pub fn layer_result(&self, library_layer: i32) -> CheckResult {
CheckResult {
name: Check::LayerMatch.name().to_owned(),
passed: self.layer_matches(library_layer),
detail: layer_detail(library_layer, self.advertised_layer),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CheckResult {
pub name: String,
pub passed: bool,
pub detail: String,
}
#[must_use]
pub fn consistency_summary(results: &[CheckResult]) -> String {
let passed = results.iter().filter(|result| result.passed).count();
let mut lines = vec![format!(
"Consistency: {passed}/{} checks passed",
results.len()
)];
lines.extend(results.iter().map(|result| {
let status = if result.passed { "OK" } else { "FAIL" };
format!(" [{status}] {}: {}", result.name, result.detail)
}));
lines.join("\n")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CheckOutcome {
Finished {
passed: bool,
detail: String,
},
Raised(String),
}
impl CheckOutcome {
#[must_use]
pub fn into_result(self, check: Check) -> CheckResult {
let (passed, detail) = match self {
Self::Finished { passed, detail } => (passed, detail),
Self::Raised(error) => (false, check_exception(&error)),
};
CheckResult {
name: check.name().to_owned(),
passed,
detail,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConsistencyReport {
pub checks: Vec<CheckResult>,
}
impl ConsistencyReport {
#[must_use]
pub fn all_passed(&self) -> bool {
self.checks.iter().all(|result| result.passed)
}
#[must_use]
pub fn summary(&self) -> String {
consistency_summary(&self.checks)
}
#[must_use]
pub fn warning(&self) -> Option<String> {
(!self.all_passed()).then(|| format!("{CONSISTENCY_WARNING_PREFIX}{}", self.summary()))
}
}
#[must_use]
pub fn consistency_report(outcomes: [CheckOutcome; 7]) -> ConsistencyReport {
ConsistencyReport {
checks: Check::ALL
.into_iter()
.zip(outcomes)
.map(|(check, outcome)| outcome.into_result(check))
.collect(),
}
}
fn py_bool(value: bool) -> &'static str {
if value { "True" } else { "False" }
}
#[must_use]
pub fn get_config_detail(this_dc: Option<i32>) -> String {
match this_dc {
Some(dc) => format!("Config OK (DC {dc})"),
None => "Config OK (DC None)".to_owned(),
}
}
#[must_use]
pub fn get_config_error(error: &str) -> String {
format!("Server rejected getConfig: {error}")
}
#[must_use]
pub fn nearest_dc_detail(country: &str, this_dc: i32, nearest_dc: i32) -> String {
format!("country='{country}', this_dc={this_dc}, nearest_dc={nearest_dc}")
}
#[must_use]
pub fn nearest_dc_error(error: &str) -> String {
format!("Error calling getNearestDc: {error}")
}
#[must_use]
pub fn current_session_detail(row: &SessionRow) -> String {
format!(
"api_id={}, official={}, device='{}', app='{} {}'",
row.api_id,
py_bool(row.official_app),
row.device_model,
row.app_name,
row.app_version
)
}
#[must_use]
pub fn layer_detail(library_layer: i32, advertised_layer: i32) -> String {
format!(
"Telethon layer={library_layer}, advertised schema layer={advertised_layer}, diff={}",
library_layer.abs_diff(advertised_layer)
)
}
#[must_use]
pub fn lang_pack_detail(lang_pack: &str, languages: usize) -> String {
format!("lang_pack '{lang_pack}' valid ({languages} languages)")
}
#[must_use]
pub fn lang_pack_error(lang_pack: &str, error: &str) -> String {
format!("lang_pack '{lang_pack}' rejected: {error}")
}
#[must_use]
pub fn app_update_detail(version: Option<&str>) -> String {
format!(
"Update available: v{} (but request succeeded)",
version.unwrap_or("?")
)
}
#[must_use]
pub fn app_update_error(error: &str) -> String {
format!("Error calling getAppUpdate: {error}")
}
#[must_use]
pub fn check_error(error: &str) -> String {
format!("Error: {error}")
}
#[must_use]
pub fn check_exception(error: &str) -> String {
format!("Exception: {error}")
}