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}