grammers_client/client/
client.rs

1// Copyright 2020 - developers of the `grammers` project.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8use grammers_mtproto::{mtp, transport};
9use grammers_mtsender::{self as sender, ReconnectionPolicy, Sender};
10use grammers_session::{ChatHashCache, MessageBox, Session};
11use grammers_tl_types as tl;
12use sender::Enqueuer;
13use std::collections::{HashMap, VecDeque};
14use std::fmt;
15use std::net::SocketAddr;
16use std::sync::atomic::AtomicU32;
17use std::sync::{Arc, RwLock};
18use std::time::Instant;
19use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
20
21/// When no locale is found, use this one instead.
22const DEFAULT_LOCALE: &str = "en";
23
24/// Configuration required to create a [`Client`] instance.
25///
26/// [`Client`]: struct.Client.html
27pub struct Config {
28    /// Session storage where data should persist, such as authorization key, server address,
29    /// and other required information by the client.
30    pub session: Session,
31
32    /// Developer's API ID, required to interact with the Telegram's API.
33    ///
34    /// You may obtain your own in <https://my.telegram.org/auth>.
35    pub api_id: i32,
36
37    /// Developer's API hash, required to interact with Telegram's API.
38    ///
39    /// You may obtain your own in <https://my.telegram.org/auth>.
40    pub api_hash: String,
41
42    /// Additional initialization parameters that can have sane defaults.
43    pub params: InitParams,
44}
45
46/// Optional initialization parameters, required when initializing a connection to Telegram's
47/// API.
48#[derive(Clone)]
49pub struct InitParams {
50    pub device_model: String,
51    pub system_version: String,
52    pub app_version: String,
53    pub system_lang_code: String,
54    pub lang_code: String,
55    /// Should the client catch-up on updates sent to it while it was offline?
56    ///
57    /// By default, updates sent while the client was offline are ignored.
58    // TODO catch up doesn't occur until we get an update that tells us if there was a gap, but
59    // maybe we should forcibly try to get difference even if we didn't miss anything?
60    pub catch_up: bool,
61    /// Server address to connect to. By default, the library will connect to the address stored
62    /// in the session file (or a default production address if no such address exists). This
63    /// field can be used to override said address, and is most commonly used to connect to one
64    /// of Telegram's test servers instead.
65    pub server_addr: Option<SocketAddr>,
66    /// The threshold below which the library should automatically sleep on flood-wait and slow
67    /// mode wait errors (inclusive). For instance, if an
68    /// `RpcError { name: "FLOOD_WAIT", value: Some(17) }` (flood, must wait 17 seconds) occurs
69    /// and `flood_sleep_threshold` is 20 (seconds), the library will `sleep` automatically for
70    /// 17 seconds. If the error was for 21s, it would propagate the error instead.
71    ///
72    /// By default, the library will sleep on flood-waits below or equal to one minute (60
73    /// seconds), but this can be disabled by passing `0` (since all flood errors would be
74    /// higher and exceed the threshold).
75    ///
76    /// On flood, the library will retry *once*. If the flood error occurs a second time after
77    /// sleeping, the error will be returned.
78    pub flood_sleep_threshold: u32,
79    /// How many updates may be buffered by the client at any given time.
80    ///
81    /// Telegram passively sends updates to the client through the open connection, so they must
82    /// be buffered until the application has the capacity to consume them.
83    ///
84    /// Upon reaching this limit, updates will be dropped, and a warning log message will be
85    /// emitted (but not too often, to avoid spamming the log), in order to let the developer
86    /// know that they should either change how they handle updates or increase the limit.
87    ///
88    /// A limit of zero (`0`) indicates that updates should not be buffered. They will be
89    /// immediately dropped, and no warning will ever be emitted.
90    ///
91    /// A limit of `None` disables the upper bound for the buffer. This is not recommended, as it
92    /// could eventually lead to memory exhaustion. This option will also not emit any warnings.
93    ///
94    /// The default limit, which may change at any time, should be enough for user accounts,
95    /// although bot accounts may need to increase the limit depending on their capacity.
96    ///
97    /// When the limit is `Some`, a buffer to hold that many updates will be pre-allocated.
98    pub update_queue_limit: Option<usize>,
99    /// URL of the proxy to use. Requires the `proxy` feature to be enabled.
100    ///
101    /// The scheme must be `socks5`. Username and password are optional.
102    ///
103    /// Both a host and port must be provided. If a domain is used for the host, domain, its address will be looked up,
104    /// and the first IP address found will be used. If a different IP address should be used, consider resolving the
105    /// host manually and selecting an IP address of your choice.
106    #[cfg(feature = "proxy")]
107    pub proxy_url: Option<String>,
108
109    /// specify the reconnection policy which will be used by client to determine whether to re-connect on failure or not.
110    ///
111    ///it can be one of the 2 default implementation [`NoReconnect`] and [`FixedReconnect`];
112    ///
113    /// **OR** your own custom implementation of trait [`ReconnectionPolicy`].
114    ///
115    /// for more details refer to [`examples`](lib/grammers-client/examples/reconnection.rs)
116    ///
117    /// [`NoReconnect`]: grammers_mtsender::NoReconnect
118    /// [`FixedReconnect`]: grammers_mtsender::FixedReconnect
119    /// [`ReconnectionPolicy`]: grammers_mtsender::ReconnectionPolicy
120    pub reconnection_policy: &'static dyn ReconnectionPolicy,
121}
122
123pub(crate) struct ClientInner {
124    // Used to implement `PartialEq`.
125    pub(crate) id: i64,
126    pub(crate) config: Config,
127    pub(crate) conn: Connection,
128    pub(crate) state: RwLock<ClientState>,
129    // Stores per-datacenter downloader instances
130    pub(crate) downloader_map: AsyncRwLock<HashMap<i32, Arc<Connection>>>,
131}
132
133pub(crate) struct ClientState {
134    pub(crate) dc_id: i32,
135    pub(crate) message_box: MessageBox,
136    pub(crate) chat_hashes: ChatHashCache,
137    // When did we last warn the user that the update queue filled up?
138    // This is used to avoid spamming the log.
139    pub(crate) last_update_limit_warn: Option<Instant>,
140    pub(crate) updates: VecDeque<(tl::enums::Update, Arc<crate::types::ChatMap>)>,
141}
142
143pub(crate) struct Connection {
144    pub(crate) sender: AsyncMutex<Sender<transport::Full, mtp::Encrypted>>,
145    pub(crate) request_tx: RwLock<Enqueuer>,
146    pub(crate) step_counter: AtomicU32,
147}
148
149/// A client capable of connecting to Telegram and invoking requests.
150///
151/// This structure is the "entry point" of the library, from which you can start using the rest.
152///
153/// This structure owns all the necessary connections to Telegram, and has implementations for the
154/// most basic methods, such as connecting, signing in, or processing network events.
155///
156/// On drop, all state is synchronized to the session. The [`Session`] must be explicitly saved
157/// to disk with [`Session::save_to_file`] for persistence
158///
159/// [`Session`]: grammers_session::Session
160#[derive(Clone)]
161pub struct Client(pub(crate) Arc<ClientInner>);
162
163impl Default for InitParams {
164    fn default() -> Self {
165        let info = os_info::get();
166
167        let mut system_lang_code = String::new();
168        let mut lang_code = String::new();
169
170        #[cfg(not(target_os = "android"))]
171        {
172            system_lang_code.push_str(&locate_locale::system());
173            lang_code.push_str(&locate_locale::user());
174        }
175        if system_lang_code.is_empty() {
176            system_lang_code.push_str(DEFAULT_LOCALE);
177        }
178        if lang_code.is_empty() {
179            lang_code.push_str(DEFAULT_LOCALE);
180        }
181
182        Self {
183            device_model: format!("{} {}", info.os_type(), info.bitness()),
184            system_version: info.version().to_string(),
185            app_version: env!("CARGO_PKG_VERSION").to_string(),
186            system_lang_code,
187            lang_code,
188            catch_up: false,
189            server_addr: None,
190            flood_sleep_threshold: 60,
191            update_queue_limit: Some(100),
192            #[cfg(feature = "proxy")]
193            proxy_url: None,
194            reconnection_policy: &grammers_mtsender::NoReconnect,
195        }
196    }
197}
198
199// TODO move some stuff like drop into ClientInner?
200impl Drop for Client {
201    fn drop(&mut self) {
202        self.sync_update_state();
203    }
204}
205
206impl fmt::Debug for Client {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        // TODO show more info, like user id and session name if present
209        f.debug_struct("Client")
210            .field("dc_id", &self.0.state.read().unwrap().dc_id)
211            .finish()
212    }
213}
214
215impl PartialEq for Client {
216    fn eq(&self, other: &Self) -> bool {
217        self.0.id == other.0.id
218    }
219}