opentele2_grammers/lib.rs
1//! grammers 0.11 sessions and connection params from opentele2 accounts and
2//! API profiles, and back.
3//!
4//! - [`session_data`] turns a tdata [`Account`] into a
5//! [`grammers_session::SessionData`], ready for
6//! `MemorySession::from` or [`SessionData::import_to`].
7//! - [`account_from_session`] is its inverse: the home DC key and the self
8//! user of a [`SessionData`] become an [`Account`].
9//! - [`connection_params`] copies the `initConnection` fields of an
10//! [`ApiProfile`] into a [`grammers_mtsender::ConnectionParams`].
11//! - With the `client` feature (off by default), `client` runs the opentele2
12//! client flows over grammers-client 0.11: QR login with DC migration
13//! and 2FA, the self user id, session admin, the seven consistency
14//! checks and tdata from a client session, over an `Invoker` that
15//! `grammers_client::Client` implements. Its module docs list the limits
16//! grammers 0.11 puts on those flows.
17//!
18//! The core crate never depends on grammers; this crate is the optional
19//! bridge. The profile's `api_id` is passed to grammers separately (for
20//! example to `SenderPool::new`), and its `api_hash` at sign-in.
21//!
22//! # Fingerprint gap
23//!
24//! grammers 0.11 builds `initConnection` itself in `SenderPool`
25//! (`grammers-mtsender` `sender_pool.rs`) and always sends `lang_pack: ""`
26//! and `params: None`. [`ConnectionParams`] has no field for either, so the
27//! profile's [`ApiProfile::lang_pack`] (`tdesktop` for the desktop profile)
28//! is dropped, and no `params` JSON object is sent. A session driven by
29//! grammers with a desktop profile is therefore not byte-identical on the
30//! wire to Telegram Desktop: the device, system, app and language fields
31//! match, those two do not.
32//!
33//! Closing the gap needs an upstream change in `grammers-mtsender`: a
34//! `lang_pack: String` and a `params: Option<JsonValue>` field (the TL
35//! `JSONValue`) on `ConnectionParams`, defaulting to `""` and `None`, which
36//! `SenderPool` copies into `InitConnection` in place of the hardcoded
37//! values. Once released, [`connection_params`] fills both from the
38//! profile.
39//!
40//! # Keys
41//!
42//! [`SessionData`] and its `DcOption` derive `Debug` and print the raw auth
43//! key; do not log them. opentele2's [`AuthKey`] prints its key id only.
44
45use std::collections::HashMap;
46use std::net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6};
47
48use grammers_mtsender::ConnectionParams;
49use grammers_session::SessionData;
50use grammers_session::types::{DcOption, PeerId, PeerInfo, UpdatesState};
51use opentele2_core::api::ApiProfile;
52use opentele2_core::crypto::AuthKey;
53use opentele2_core::session::dc;
54use opentele2_core::tdata::account::Account;
55use opentele2_core::tdata::mtp_config::Endpoint;
56use opentele2_core::{ConfigFailure, Error, Result};
57
58#[cfg(feature = "client")]
59pub mod client;
60
61/// The self user of `account` as grammers caches it: a user peer with
62/// `is_self` set and `bot` false (tdata holds user accounts only), and no
63/// access hash, which the self user does not need. `None` when the user id
64/// is outside grammers' user id range.
65fn self_user(account: &Account) -> Option<PeerInfo> {
66 let id = i64::try_from(account.user_id).ok()?;
67 PeerId::user(id)?;
68 Some(PeerInfo::User {
69 id,
70 auth: None,
71 bot: Some(false),
72 is_self: Some(true),
73 })
74}
75
76/// The address of a config endpoint; `None` when its IP or port does not
77/// parse.
78fn endpoint_addr(endpoint: &Endpoint) -> Option<SocketAddr> {
79 let ip: IpAddr = endpoint.ip.parse().ok()?;
80 let port = u16::try_from(endpoint.port).ok()?;
81 Some(SocketAddr::new(ip, port))
82}
83
84/// An IPv4 and an IPv6 address, or `None` when either is missing or of the
85/// other family.
86fn address_pair(
87 ipv4: Option<SocketAddr>,
88 ipv6: Option<SocketAddr>,
89) -> Option<(SocketAddrV4, SocketAddrV6)> {
90 match (ipv4, ipv6) {
91 (Some(SocketAddr::V4(ipv4)), Some(SocketAddr::V6(ipv6))) => Some((ipv4, ipv6)),
92 _ => None,
93 }
94}
95
96/// The DCs of `account` as (id, IPv4, IPv6): those of its stored config
97/// when it has config bytes, else the built-in production table; see
98/// [`session_data`].
99fn dc_endpoints(account: &Account) -> Vec<(i32, SocketAddrV4, SocketAddrV6)> {
100 if account.config.is_some() {
101 let config = account.mtp_config();
102 config
103 .dc_options
104 .dcs()
105 .filter_map(|(id, _)| {
106 let endpoints = config.endpoints(id)?;
107 let ipv4 = endpoints.select(false).and_then(endpoint_addr);
108 let ipv6 = endpoints.select(true).and_then(endpoint_addr);
109 address_pair(ipv4, ipv6).map(|(ipv4, ipv6)| (id, ipv4, ipv6))
110 })
111 .collect()
112 } else {
113 dc::PRODUCTION_DC_IDS
114 .iter()
115 .filter_map(|&id| {
116 address_pair(dc::production(id, false), dc::production(id, true))
117 .map(|(ipv4, ipv6)| (id, ipv4, ipv6))
118 })
119 .collect()
120 }
121}
122
123/// grammers session data for `account`.
124///
125/// - `home_dc` is the account's main DC.
126/// - `dc_options` holds the DCs with an IPv4 and an IPv6 endpoint, chosen
127/// as [`opentele2_core::session::grammers::SessionData::from_account`]
128/// chooses them: with stored config bytes ([`Account::config`], parsed
129/// by [`Account::mtp_config`]) the DCs of that config whose endpoints
130/// ([`Endpoints::select`](opentele2_core::tdata::mtp_config::Endpoints::select)
131/// without and with IPv6) are an IPv4 and an IPv6 address; without them
132/// every production DC of the core table
133/// ([`dc::PRODUCTION_DC_IDS`]) with its first IPv4 and IPv6 endpoint.
134/// Only the main DC carries a key, the account's main DC key.
135/// - `peer_infos` holds the self user, keyed by its user id as
136/// `Session::cache_peer` keys it.
137/// - `updates_state` is empty, so grammers fetches it on first use.
138///
139/// The conversion cannot fail. An account it cannot fully express yields
140/// data that [`account_from_session`] rejects: a main DC outside the
141/// production table (with a stored config, outside its usable DCs) or a
142/// missing main DC key leaves no key in the data, and
143/// a user id outside grammers' range (1 to `0xffffffffff`) leaves no self
144/// user.
145#[must_use]
146pub fn session_data(account: &Account) -> SessionData {
147 let key = account.auth_key().map(|key| *key.as_bytes());
148 let dc_options = dc_endpoints(account)
149 .into_iter()
150 .map(|(id, ipv4, ipv6)| {
151 let option = DcOption {
152 id,
153 ipv4,
154 ipv6,
155 auth_key: key.filter(|_| id == account.main_dc),
156 };
157 (id, option)
158 })
159 .collect();
160 let peer_infos: HashMap<PeerId, PeerInfo> = self_user(account)
161 .map(|info| (info.id(), info))
162 .into_iter()
163 .collect();
164 SessionData {
165 home_dc: account.main_dc,
166 dc_options,
167 peer_infos,
168 updates_state: UpdatesState::default(),
169 }
170}
171
172/// The account at `index` for grammers session data: the key of the home
173/// DC and the user id of the self user, with the home DC as main DC.
174///
175/// Keys of other DCs, cached peers and the updates state are not part of a
176/// tdata account and are dropped.
177///
178/// # Errors
179/// - [`Error::AuthKeyNotFound`] when the home DC has no option or no key.
180/// - [`Error::BadConfigData`] when no cached user has `is_self` set, when
181/// users with different ids have it set, or when the self user id is
182/// negative.
183pub fn account_from_session(data: &SessionData, index: i32) -> Result<Account> {
184 let key = data
185 .dc_options
186 .get(&data.home_dc)
187 .and_then(|option| option.auth_key)
188 .ok_or(Error::AuthKeyNotFound)?;
189 let mut self_ids = data.peer_infos.values().filter_map(|info| match info {
190 PeerInfo::User {
191 id,
192 is_self: Some(true),
193 ..
194 } => Some(*id),
195 _ => None,
196 });
197 let user_id = self_ids.next().ok_or(Error::BadConfigData {
198 context: ConfigFailure::NoSelfUser,
199 })?;
200 if self_ids.any(|other| other != user_id) {
201 return Err(Error::BadConfigData {
202 context: ConfigFailure::ConflictingSelfUsers,
203 });
204 }
205 let user_id = u64::try_from(user_id).map_err(|_| Error::BadConfigData {
206 context: ConfigFailure::BadUserId { user_id },
207 })?;
208 Account::new(
209 index,
210 user_id,
211 data.home_dc,
212 vec![(data.home_dc, AuthKey::new(key))],
213 )
214}
215
216/// grammers connection params for `profile`: `device_model`,
217/// `system_version`, `app_version`, `system_lang_code` and `lang_code` from
218/// the profile, every other field the grammers default.
219///
220/// `api_id` and `api_hash` are not connection params, and `lang_pack` has
221/// no field in grammers 0.11; see the [crate docs](crate#fingerprint-gap).
222#[must_use]
223pub fn connection_params(profile: &ApiProfile) -> ConnectionParams {
224 ConnectionParams {
225 device_model: profile.device_model.clone(),
226 system_version: profile.system_version.clone(),
227 app_version: profile.app_version.clone(),
228 system_lang_code: profile.system_lang_code.clone(),
229 lang_code: profile.lang_code.clone(),
230 ..ConnectionParams::default()
231 }
232}