use std::ffi::{OsStr, OsString};
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use serde_json::{Map, Value};
use crate::api::ApiProfile;
use crate::crypto::AuthKey;
use crate::session::codecs::account_target;
use crate::tdata::account::Account;
use crate::{Error, Result};
pub const VERSION: i64 = 8;
pub const SESSION_SUFFIX: &str = ".session";
pub const JSON_SUFFIX: &str = ".json";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Table {
pub name: &'static str,
pub ddl: &'static str,
pub columns: &'static [&'static str],
}
pub const SCHEMA: [Table; 5] = [
Table {
name: "version",
ddl: "create table version (version integer primary key)",
columns: &["version"],
},
Table {
name: "sessions",
ddl: "create table sessions (dc_id integer primary key, server_address text, \
port integer, auth_key blob, takeout_id integer, tmp_auth_key blob)",
columns: &[
"dc_id",
"server_address",
"port",
"auth_key",
"takeout_id",
"tmp_auth_key",
],
},
Table {
name: "entities",
ddl: "create table entities (id integer primary key, hash integer not null, \
username text, phone integer, name text, date integer)",
columns: &["id", "hash", "username", "phone", "name", "date"],
},
Table {
name: "sent_files",
ddl: "create table sent_files (md5_digest blob, file_size integer, type integer, \
id integer, hash integer, primary key(md5_digest, file_size, type))",
columns: &["md5_digest", "file_size", "type", "id", "hash"],
},
Table {
name: "update_state",
ddl: "create table update_state (id integer primary key, pts integer, qts integer, \
date integer, seq integer)",
columns: &["id", "pts", "qts", "date", "seq"],
},
];
pub const DELETE_VERSION: &str = "delete from version";
pub const INSERT_VERSION: &str = "insert into version values (?)";
pub const DELETE_SESSIONS: &str = "delete from sessions";
pub const INSERT_SESSION: &str = "insert into sessions values (?, ?, ?, ?, ?, ?)";
#[derive(Debug, Clone)]
pub struct SessionRows {
pub dc_id: i32,
pub server_address: String,
pub port: u16,
pub auth_key: AuthKey,
pub takeout_id: Option<i64>,
}
impl SessionRows {
pub fn from_account(account: &Account, ipv6: bool) -> Result<Self> {
let (addr, server_address, _, key) = account_target(account, ipv6)?;
Ok(Self {
dc_id: account.main_dc,
server_address,
port: addr.port(),
auth_key: key.clone(),
takeout_id: None,
})
}
}
fn strip_suffix(path: &OsStr, suffix: &str) -> Option<OsString> {
#[cfg(unix)]
{
use std::os::unix::ffi::{OsStrExt, OsStringExt};
path.as_bytes()
.strip_suffix(suffix.as_bytes())
.map(|head| OsString::from_vec(head.to_vec()))
}
#[cfg(windows)]
{
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let wide: Vec<u16> = path.encode_wide().collect();
let suffix: Vec<u16> = suffix.encode_utf16().collect();
wide.strip_suffix(suffix.as_slice())
.map(OsString::from_wide)
}
#[cfg(not(any(unix, windows)))]
{
path.to_str()?.strip_suffix(suffix).map(OsString::from)
}
}
fn with_suffix(base: &OsStr, suffix: &str) -> PathBuf {
let mut path = base.to_os_string();
path.push(suffix);
PathBuf::from(path)
}
#[must_use]
pub fn session_path(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref().as_os_str();
if path.as_encoded_bytes().ends_with(SESSION_SUFFIX.as_bytes()) {
PathBuf::from(path)
} else {
with_suffix(path, SESSION_SUFFIX)
}
}
#[must_use]
pub fn pair_paths(base: impl AsRef<Path>) -> (PathBuf, PathBuf) {
let mut stem = base.as_ref().as_os_str().to_os_string();
for suffix in [SESSION_SUFFIX, JSON_SUFFIX] {
if let Some(head) = strip_suffix(&stem, suffix) {
stem = head;
}
}
(
with_suffix(&stem, SESSION_SUFFIX),
with_suffix(&stem, JSON_SUFFIX),
)
}
#[must_use]
pub fn companion_json(profile: &ApiProfile, extra: Option<&Map<String, Value>>) -> Value {
profile.to_session_json(extra)
}
#[must_use]
pub fn companion_text(profile: &ApiProfile, extra: Option<&Map<String, Value>>) -> String {
py_json_text(&companion_json(profile, extra))
}
#[must_use]
pub fn py_json_text(value: &Value) -> String {
let mut text = String::new();
push_py_json(&mut text, value, 0);
text
}
pub fn read_companion(text: &str) -> Result<(ApiProfile, Map<String, Value>)> {
let data: Value = serde_json::from_str(text).map_err(|error| Error::InvalidInput {
what: "JSON file",
reason: error.to_string(),
})?;
for field in ["app_id", "app_hash"] {
let present = data
.as_object()
.and_then(|object| object.get(field))
.is_some_and(|value| !value.is_null());
if !present {
return Err(Error::InvalidInput {
what: "JSON file",
reason: format!("Missing required field '{field}' in JSON"),
});
}
}
let profile = ApiProfile::from_session_json(&data)?;
let Value::Object(object) = data else {
unreachable!("the required fields were found in an object");
};
Ok((profile, object))
}
fn push_py_indent(out: &mut String, depth: usize) {
out.push('\n');
for _ in 0..depth {
out.push_str(" ");
}
}
fn push_py_json(out: &mut String, value: &Value, depth: usize) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),
Value::Number(number) => push_py_number(out, number),
Value::String(text) => push_py_string(out, text),
Value::Array(items) => {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
push_py_indent(out, depth + 1);
push_py_json(out, item, depth + 1);
}
push_py_indent(out, depth);
out.push(']');
}
Value::Object(entries) => {
if entries.is_empty() {
out.push_str("{}");
return;
}
out.push('{');
for (index, (key, item)) in entries.iter().enumerate() {
if index > 0 {
out.push(',');
}
push_py_indent(out, depth + 1);
push_py_string(out, key);
out.push_str(": ");
push_py_json(out, item, depth + 1);
}
push_py_indent(out, depth);
out.push('}');
}
}
}
fn push_py_string(out: &mut String, text: &str) {
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
'\0'..='\u{1f}' => {
let _ = write!(out, "\\u{:04x}", u32::from(ch));
}
_ => out.push(ch),
}
}
out.push('"');
}
fn push_py_number(out: &mut String, number: &serde_json::Number) {
if let Some(int) = number.as_i64() {
out.push_str(&int.to_string());
} else if let Some(int) = number.as_u64() {
out.push_str(&int.to_string());
} else if let Some(float) = number.as_f64() {
push_py_float(out, float);
} else {
out.push_str(&number.to_string());
}
}
fn push_py_float(out: &mut String, value: f64) {
if value.is_nan() {
out.push_str("NaN");
return;
}
if value.is_infinite() {
out.push_str(if value > 0.0 { "Infinity" } else { "-Infinity" });
return;
}
if value.is_sign_negative() {
out.push('-');
}
let scientific = format!("{:e}", value.abs());
let (mantissa, exponent) = scientific
.split_once('e')
.unwrap_or((scientific.as_str(), "0"));
let exponent: i64 = exponent.parse().unwrap_or(0);
let digits: String = mantissa.chars().filter(|&c| c != '.').collect();
let point = exponent + 1;
if !(-4 < point && point <= 16) {
let (first, rest) = digits.split_at(1);
out.push_str(first);
if !rest.is_empty() {
out.push('.');
out.push_str(rest);
}
let sign = if exponent < 0 { '-' } else { '+' };
let _ = write!(out, "e{sign}{:02}", exponent.unsigned_abs());
} else if point <= 0 {
out.push_str("0.");
for _ in point..0 {
out.push('0');
}
out.push_str(&digits);
} else {
let point = usize::try_from(point).unwrap_or(0);
if point >= digits.len() {
out.push_str(&digits);
for _ in digits.len()..point {
out.push('0');
}
out.push_str(".0");
} else {
let (whole, fraction) = digits.split_at(point);
out.push_str(whole);
out.push('.');
out.push_str(fraction);
}
}
}
#[cfg(feature = "telethon-sqlite")]
pub use file::{read_session_file, write_session_file};
#[cfg(feature = "telethon-sqlite")]
mod file {
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use rusqlite::types::Value;
use rusqlite::{Connection, OpenFlags, OptionalExtension, params};
use super::{
DELETE_SESSIONS, DELETE_VERSION, INSERT_SESSION, INSERT_VERSION, SCHEMA, SessionRows,
VERSION, session_path,
};
use crate::crypto::AuthKey;
use crate::{ConfigFailure, Error, Result};
fn sqlite_error(error: rusqlite::Error) -> Error {
Error::Io(std::io::Error::other(error))
}
fn schema_is_current(conn: &Connection) -> Result<bool> {
let mut statement = conn
.prepare("select name from sqlite_master where type='table'")
.map_err(sqlite_error)?;
let present = statement
.query_map([], |row| row.get::<_, String>(0))
.map_err(sqlite_error)?
.collect::<rusqlite::Result<HashSet<String>>>()
.map_err(sqlite_error)?;
if !SCHEMA.iter().all(|table| present.contains(table.name)) {
return Ok(false);
}
for table in &SCHEMA {
let mut statement = conn
.prepare(&format!("pragma table_info({})", table.name))
.map_err(sqlite_error)?;
let columns = statement
.query_map([], |row| row.get::<_, String>(1))
.map_err(sqlite_error)?
.collect::<rusqlite::Result<Vec<String>>>()
.map_err(sqlite_error)?;
if !columns
.iter()
.map(String::as_str)
.eq(table.columns.iter().copied())
{
return Ok(false);
}
}
Ok(true)
}
pub fn write_session_file(path: impl AsRef<Path>, rows: &SessionRows) -> Result<PathBuf> {
let full_path = session_path(path);
let mut conn = Connection::open(&full_path).map_err(sqlite_error)?;
let tx = conn.transaction().map_err(sqlite_error)?;
if !schema_is_current(&tx)? {
for table in &SCHEMA {
tx.execute(&format!("drop table if exists {}", table.name), [])
.map_err(sqlite_error)?;
}
for table in &SCHEMA {
tx.execute(table.ddl, []).map_err(sqlite_error)?;
}
}
tx.execute(DELETE_VERSION, []).map_err(sqlite_error)?;
tx.execute(INSERT_VERSION, [VERSION])
.map_err(sqlite_error)?;
tx.execute(DELETE_SESSIONS, []).map_err(sqlite_error)?;
tx.execute(
INSERT_SESSION,
params![
rows.dc_id,
rows.server_address,
rows.port,
rows.auth_key.as_bytes().as_slice(),
rows.takeout_id,
Value::Null,
],
)
.map_err(sqlite_error)?;
tx.commit().map_err(sqlite_error)?;
conn.close().map_err(|(_, error)| sqlite_error(error))?;
Ok(full_path)
}
pub fn read_session_file(path: impl AsRef<Path>) -> Result<SessionRows> {
let full_path = session_path(path);
if !full_path.is_file() {
return Err(Error::FileNotFound {
file: full_path.display().to_string(),
});
}
let conn = Connection::open_with_flags(
&full_path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(sqlite_error)?;
if !schema_is_current(&conn)? {
return Err(Error::QDataStreamFailed { context: None });
}
let version = conn
.query_row("select version from version", [], |row| {
row.get::<_, Value>(0)
})
.optional()
.map_err(sqlite_error)?;
if version != Some(Value::Integer(VERSION)) {
return Err(Error::QDataStreamFailed { context: None });
}
let row = conn
.query_row(
"select dc_id, server_address, port, auth_key, takeout_id from sessions",
[],
|row| {
Ok([
row.get::<_, Value>(0)?,
row.get::<_, Value>(1)?,
row.get::<_, Value>(2)?,
row.get::<_, Value>(3)?,
row.get::<_, Value>(4)?,
])
},
)
.optional()
.map_err(sqlite_error)?
.ok_or(Error::AuthKeyNotFound)?;
let [dc_id, server_address, port, auth_key, takeout_id] = row;
let auth_key = match auth_key {
Value::Null => return Err(Error::AuthKeyNotFound),
Value::Blob(bytes) if bytes.is_empty() => return Err(Error::AuthKeyNotFound),
Value::Blob(bytes) => AuthKey::from_slice(&bytes)?,
_ => return Err(Error::QDataStreamFailed { context: None }),
};
let null = |column| Error::BadConfigData {
context: ConfigFailure::NullSessionColumn { column },
};
let dc_id = match dc_id {
Value::Integer(id) => i32::try_from(id).map_err(|_| Error::BadConfigData {
context: ConfigFailure::DcIdOutOfRange { dc_id: id },
})?,
Value::Null => return Err(null("dc_id")),
_ => return Err(Error::QDataStreamFailed { context: None }),
};
let server_address = match server_address {
Value::Text(text) => text,
Value::Null => return Err(null("server_address")),
_ => return Err(Error::QDataStreamFailed { context: None }),
};
let port = match port {
Value::Integer(port) => u16::try_from(port).map_err(|_| Error::BadConfigData {
context: ConfigFailure::PortOutOfRange { port },
})?,
Value::Null => return Err(null("port")),
_ => return Err(Error::QDataStreamFailed { context: None }),
};
let takeout_id = match takeout_id {
Value::Integer(id) => Some(id),
Value::Null => None,
_ => return Err(Error::QDataStreamFailed { context: None }),
};
Ok(SessionRows {
dc_id,
server_address,
port,
auth_key,
takeout_id,
})
}
}