ferogram 0.5.1

Production-grade async Telegram MTProto client: updates, bots, flood-wait, dialogs, messages
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
//
// ferogram: async Telegram MTProto client in Rust
// https://github.com/ankit-chaubey/ferogram
//
// Licensed under either the MIT License or the Apache License 2.0.
// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
// https://github.com/ankit-chaubey/ferogram
//
// Feel free to use, modify, and share this code.
// Please keep this notice when redistributing.

//! Async Rust client for the Telegram MTProto API.
//!
//! ferogram talks to Telegram directly over MTProto with no Bot API proxy. It works
//! for both bots and user accounts. Most things you'd want to do with Telegram
//! are already covered. If something isn't, you can always drop down to
//! [`client.invoke()`](Client::invoke) and call any TL function directly.
//!
//! Still in development but already covers major use cases for production.
//! Check the [CHANGELOG] before upgrading.
//!
//! [CHANGELOG]: https://github.com/ankit-chaubey/ferogram/blob/main/CHANGELOG.md
//!
//! # Quick start: bot
//!
//! ```rust,no_run
//! use ferogram::{Client, update::Update};
//!
//! const API_ID: i32 = 0; // from https://my.telegram.org
//! const API_HASH: &str = ""; // from https://my.telegram.org
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//!     let (client, _) = Client::quick_connect("bot.session", API_ID, API_HASH).await?;
//!
//!     let mut stream = client.stream_updates();
//!     while let Some(upd) = stream.next().await {
//!         if let Update::NewMessage(msg) = upd {
//!             if !msg.outgoing() {
//!                 msg.reply(msg.text().unwrap_or_default()).await.ok();
//!             }
//!         }
//!     }
//!     Ok(())
//! }
//! ```
//!
//! # Quick start: user account
//!
//! ```rust,no_run
//! use ferogram::Client;
//!
//! const API_ID: i32 = 0; // from https://my.telegram.org
//! const API_HASH: &str = ""; // from https://my.telegram.org
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//!     let (client, _) = Client::quick_connect("my.session", API_ID, API_HASH).await?;
//!
//!     client.send_message("me", "Hello from ferogram!").await?;
//!     Ok(())
//! }
//! ```
//!
//! # Dispatcher and filters
//!
//! ```rust,ignore
//! use ferogram::filters::{Dispatcher, command, private, text_contains};
//!
//! let mut dp = Dispatcher::new();
//!
//! dp.on_message(command("start"), |msg| async move {
//!     msg.reply("Hello!").await.ok();
//! });
//!
//! dp.on_message(private() & text_contains("help"), |msg| async move {
//!     msg.reply("Type /start to begin.").await.ok();
//! });
//!
//! while let Some(upd) = stream.next().await {
//!     dp.dispatch(upd).await;
//! }
//! # }
//! ```
//!
//! Filters compose with `&`, `|`, `!`. Built-ins: `command`, `private`, `group`,
//! `channel`, `text`, `media`, `photo`, `forwarded`, `reply`, `album`, `regex`, and more.
//!
//! # FSM
//!
//! ```rust,ignore
//! use std::sync::Arc;
//!
//! #[derive(FsmState, Clone, Debug, PartialEq)]
//! enum Form { Name, Age }
//!
//! dp.with_state_storage(Arc::new(MemoryStorage::new()));
//!
//! dp.on_message_fsm(text(), Form::Name, |msg, state| async move {
//!     state.set_data("name", msg.text().unwrap()).await.ok();
//!     state.transition(Form::Age).await.ok();
//!     msg.reply("How old are you?").await.ok();
//! });
//! ```
//!
//! # Raw API
//!
//! If something isn't wrapped yet, you can call any Layer 225 TL function directly:
//!
//! ```rust,ignore
//! use ferogram::tl;
//!
//! let req = tl::functions::messages::SendMessage {
//!     peer: peer.into(),
//!     message: "Hello!".into(),
//!     random_id: ferogram::random_i64_pub(),
//!     ..Default::default()
//! };
//! client.invoke(&req).await?;
//! ```
//!
//! # Session backends
//!
//! Binary file by default. Switch to SQLite, libSQL, or a base64 string with a
//! feature flag. Bring your own backend by implementing [`SessionBackend`].
//!
//! ```rust,ignore
//! // Portable string session, useful for serverless or env-var setups
//! let s = client.export_session_string().await?;
//! let (client, _) = Client::builder().session_string(s).connect().await?;
//! ```
//!
//! # Features
//!
//! Most common use cases are already covered. Full list in
//! [FEATURES.md](https://github.com/ankit-chaubey/ferogram/blob/main/FEATURES.md).
//!
//! If something's missing, feel free to open a feature request or PR.
//! Check the [contributing guidelines](https://github.com/ankit-chaubey/ferogram#contributing) first.
//!
//! # Community
//!
//! - Channel (releases, news): [t.me/Ferogram](https://t.me/Ferogram)
//! - Chat (questions, help): [t.me/FerogramChat](https://t.me/FerogramChat)
//! - Guide: [ferogram.ankitchaubey.in](https://ferogram.ankitchaubey.in)
//! - GitHub: [ankit-chaubey/ferogram](https://github.com/ankit-chaubey/ferogram)

#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(unsafe_code)]

pub mod builder;
mod client;
mod dialog;
mod errors;
mod input_message;
pub mod media;
pub use media::DownloadIter;
pub mod message_box;
mod mini_app;
pub mod parsers;
pub mod participants;
mod peer_cache;
pub mod persist;
mod quick_connect;
mod restart;
mod retry;
mod session;
mod two_factor_auth;
pub mod update;

pub mod cdn_download;
pub mod conversation;
pub mod dc_pool;
pub mod dns_resolver;
pub mod filters;
pub mod inline_iter;
pub mod keyboard;
pub mod search;
pub mod session_backend;
pub mod socks5;
pub mod special_config;
pub mod transport_intermediate;
pub mod transport_obfuscated;
pub mod types;
pub mod typing_guard;

#[macro_use]
pub mod macros;
pub mod guest_chat;
pub mod peer_ext;
pub mod peer_ref;
pub mod poll;
pub mod reactions;

pub mod dc_migration;
pub mod proxy;

pub mod fsm;
pub mod middleware;
pub mod update_config;
pub mod util;

pub(crate) mod builder_util;

/// Portable string-session encoding/decoding (V1/V2 binary base64 format).
///
/// Most users never need this module. Just pass any session string
/// directly to [`ClientBuilder::session_string`] and it is handled automatically.
///
/// Use this module only when you need to inspect or construct a
/// [`StringSession`] value programmatically.
pub mod string_session {
    pub use ferogram_session::string_session::{
        FullSession, Session, StringSession, StringSessionError,
    };
}

// Re-export FsmState at the crate root for convenience.
pub use fsm::FsmState;

// Re-export the derive macro when the feature is enabled.
#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use ferogram_derive::FsmState;

pub use builder::{BuilderError, ClientBuilder};
pub use client::Client;
pub use client::{Config, ShutdownToken, UpdateStream};
pub use dialog::{Dialog, DialogIter, MessageIter};
pub use errors::{InvocationError, LoginToken, PasswordToken, RpcError, SignInError};
pub use ferogram_connect::TransportKind;
pub use ferogram_connect::random_i64 as random_i64_pub;
pub use guest_chat::GuestChatQuery;
pub use input_message::{ForwardOptions, InputMessage, InvoiceOptions, LinkKind};
pub use keyboard::{Button, InlineKeyboard, ReplyKeyboard};
pub use media::{Document, Downloadable, Photo, Sticker, UploadedFile};
pub use mini_app::{MiniApp, MiniAppSession};
pub use participants::{Participant, ParticipantStatus, ProfilePhotoIter};
pub use peer_cache::{ExperimentalFeatures, PeerCache, PeerType};
pub use peer_ext::{OptionPeerExt, PeerExt};
pub use peer_ref::PeerRef;
pub use poll::PollBuilder;
pub use proxy::{MtProxyConfig, parse_proxy_link};
pub use quick_connect::QuickConnectError;
pub use restart::{ConnectionRestartPolicy, ExponentialBackoff, FixedInterval, NeverRestart};
pub use retry::{AutoSleep, CircuitBreaker, NoRetries, RetryContext, RetryPolicy};
pub use search::{GlobalSearchBuilder, SearchBuilder};
pub use session::{DcEntry, DcFlags};
#[cfg(feature = "libsql-session")]
#[cfg_attr(docsrs, doc(cfg(feature = "libsql-session")))]
pub use session_backend::LibSqlBackend;
#[cfg(feature = "sqlite-session")]
#[cfg_attr(docsrs, doc(cfg(feature = "sqlite-session")))]
pub use session_backend::SqliteBackend;
pub use session_backend::{
    BinaryFileBackend, InMemoryBackend, SessionBackend, StringSessionBackend, UpdateStateChange,
};
pub use socks5::Socks5Config;
pub use types::ChannelKind;
pub use types::{Channel, Chat, Group, User};
pub use typing_guard::TypingGuard;
pub use update::{BotStoppedUpdate, MessageReactionUpdate, PollVoteUpdate};
pub use update::{ButtonFilter, Update};
pub use update::{ChatActionUpdate, JoinRequestUpdate, ParticipantUpdate, UserStatusUpdate};
pub use update::{ChatBoostUpdate, PreCheckoutQueryUpdate, ShippingQueryUpdate};
pub use update_config::{OverflowStrategy, UpdateConfig};

/// Re-export of `ferogram_tl_types`.
pub use ferogram_tl_types as tl;

/// Re-export of [`ferogram_mtproto`].
pub use ferogram_mtproto as mtproto;

/// Re-export of [`ferogram_crypto`].
pub use ferogram_crypto as crypto;

#[cfg(feature = "parser")]
pub use ferogram_tl_parser as parser;

#[cfg(feature = "codegen")]
pub use ferogram_tl_gen as codegen;

pub use ferogram_crypto::AuthKey;
pub use ferogram_mtproto::authentication::{self, Finished, finish, step1, step2, step3};
pub use ferogram_tl_types::{Identifiable, LAYER, Serializable};
/// Return type of [`Client::stats`].
pub enum ChannelStats {
    /// Stats for a broadcast channel.
    Broadcast(tl::enums::stats::BroadcastStats),
    /// Stats for a supergroup (megagroup).
    Megagroup(tl::enums::stats::MegagroupStats),
}

/// Builder returned by [`Client::set_profile`].
///
/// Call `.send().await` to apply changes. Only fields you set are touched;
/// everything else is left exactly as it is.
///
/// `.bio()` and `.name()` work for both users and chats/channels:
/// - On a user peer: `.bio()` sets the account bio, `.name(first, last)` sets
///   the display name.
/// - On a channel/group peer: `.bio()` sets the about text, `.name(first, _)`
///   sets the title (the second argument is ignored for channels).
///
/// The older `.title()` and `.about()` setters are kept for explicit usage and
/// they take priority over `.name()`/`.bio()` when both are set.
pub struct SetProfileBuilder {
    client: Client,
    peer: PeerRef,
    // User fields
    first_name: Option<String>,
    last_name: Option<String>,
    bio: Option<String>,
    emoji_status: Option<(Option<i64>, Option<i32>)>,
    // Chat/channel fields (explicit overrides)
    title: Option<String>,
    about: Option<String>,
    chat_photo: Option<tl::enums::InputChatPhoto>,
    // Shared fields
    username: Option<String>,
    photo: Option<media::UploadedFile>,
    photo_path: Option<std::path::PathBuf>,
}

impl SetProfileBuilder {
    #[doc(hidden)]
    pub fn new(client: Client, peer: PeerRef) -> Self {
        Self {
            client,
            peer,
            first_name: None,
            last_name: None,
            bio: None,
            emoji_status: None,
            title: None,
            about: None,
            chat_photo: None,
            username: None,
            photo: None,
            photo_path: None,
        }
    }

    /// Set the display name.
    ///
    /// For user accounts: sets first and last name.
    /// For channels/groups: sets the title (`first` is used; `last` is ignored).
    pub fn name(mut self, first: impl Into<String>, last: impl Into<String>) -> Self {
        self.first_name = Some(first.into());
        self.last_name = Some(last.into());
        self
    }

    /// Set bio or about text.
    ///
    /// For user accounts: sets the account bio shown on the profile page.
    /// For channels/groups: sets the about/description text.
    pub fn bio(mut self, bio: impl Into<String>) -> Self {
        self.bio = Some(bio.into());
        self
    }

    /// Set username.
    pub fn username(mut self, u: impl Into<String>) -> Self {
        self.username = Some(u.into());
        self
    }

    /// Set profile photo from an already-uploaded file.
    ///
    /// Use [`photo_path`] if you have a local file path and want the upload
    /// handled automatically inside [`send`].
    ///
    /// [`photo_path`]: SetProfileBuilder::photo_path
    /// [`send`]: SetProfileBuilder::send
    pub fn photo(mut self, file: media::UploadedFile) -> Self {
        self.photo = Some(file);
        self
    }

    /// Set profile photo from a local file path.
    ///
    /// The file is uploaded automatically when [`send`] is called.
    /// If you already have an [`UploadedFile`] use [`photo`] instead.
    ///
    /// [`send`]: SetProfileBuilder::send
    /// [`photo`]: SetProfileBuilder::photo
    /// [`UploadedFile`]: media::UploadedFile
    pub fn photo_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
        self.photo_path = Some(path.into());
        self
    }

    /// Set emoji status. Pass `None` for `document_id` to clear the status.
    pub fn emoji_status(mut self, document_id: Option<i64>, until: Option<i32>) -> Self {
        self.emoji_status = Some((document_id, until));
        self
    }

    /// Explicitly set chat/channel title, overriding `.name()` for channel peers.
    pub fn title(mut self, t: impl Into<String>) -> Self {
        self.title = Some(t.into());
        self
    }

    /// Explicitly set chat/channel about text, overriding `.bio()` for channel peers.
    pub fn about(mut self, a: impl Into<String>) -> Self {
        self.about = Some(a.into());
        self
    }

    /// Set chat/channel photo from a raw [`InputChatPhoto`].
    ///
    /// [`InputChatPhoto`]: tl::enums::InputChatPhoto
    pub fn chat_photo(mut self, p: tl::enums::InputChatPhoto) -> Self {
        self.chat_photo = Some(p);
        self
    }

    /// Apply all changes.
    pub async fn send(mut self) -> Result<(), InvocationError> {
        use ferogram_tl_types as tl;
        // Handle photo_path: upload before resolving anything else.
        if let Some(path) = self.photo_path.take() {
            let uploaded = self.client.upload_file_from_path(path).await?;
            self.photo = Some(uploaded);
        }

        let peer = self.peer.resolve(&self.client).await?;
        let input_peer = self
            .client
            .inner
            .peer_cache
            .read()
            .await
            .peer_to_input(&peer)?;

        let is_channel_or_chat = matches!(
            &input_peer,
            tl::enums::InputPeer::Channel(_) | tl::enums::InputPeer::Chat(_)
        );

        if is_channel_or_chat {
            // channel or group

            // Title: explicit .title() wins; fall back to .name(first, _).
            let effective_title = self.title.or(self.first_name);
            if let Some(t) = effective_title {
                match &input_peer {
                    tl::enums::InputPeer::Channel(c) => {
                        let req = tl::functions::channels::EditTitle {
                            channel: tl::enums::InputChannel::InputChannel(
                                tl::types::InputChannel {
                                    channel_id: c.channel_id,
                                    access_hash: c.access_hash,
                                },
                            ),
                            title: t,
                        };
                        self.client.rpc_write(&req).await?;
                    }
                    tl::enums::InputPeer::Chat(c) => {
                        let req = tl::functions::messages::EditChatTitle {
                            chat_id: c.chat_id,
                            title: t,
                        };
                        self.client.rpc_write(&req).await?;
                    }
                    _ => {}
                }
            }

            // About: explicit .about() wins; fall back to .bio().
            let effective_about = self.about.or(self.bio);
            if let Some(a) = effective_about {
                let req = tl::functions::messages::EditChatAbout {
                    peer: input_peer.clone(),
                    about: a,
                };
                self.client.rpc_write(&req).await?;
            }

            // Username
            if let Some(u) = self.username {
                let req = tl::functions::account::UpdateUsername { username: u };
                self.client.rpc_write(&req).await?;
            }

            // Photo
            if let Some(file) = self.photo {
                if let Some(chat_photo) = self.chat_photo {
                    // Explicit InputChatPhoto takes priority.
                    match &input_peer {
                        tl::enums::InputPeer::Channel(c) => {
                            let req = tl::functions::channels::EditPhoto {
                                channel: tl::enums::InputChannel::InputChannel(
                                    tl::types::InputChannel {
                                        channel_id: c.channel_id,
                                        access_hash: c.access_hash,
                                    },
                                ),
                                photo: chat_photo,
                            };
                            self.client.rpc_write(&req).await?;
                        }
                        tl::enums::InputPeer::Chat(c) => {
                            let req = tl::functions::messages::EditChatPhoto {
                                chat_id: c.chat_id,
                                photo: chat_photo,
                            };
                            self.client.rpc_write(&req).await?;
                        }
                        _ => {}
                    }
                } else {
                    // UploadedFile: wrap as InputChatPhotoUploaded.
                    let chat_photo = tl::enums::InputChatPhoto::InputChatUploadedPhoto(
                        tl::types::InputChatUploadedPhoto {
                            video: None,
                            file: Some(file.inner),
                            video_start_ts: None,
                            video_emoji_markup: None,
                        },
                    );
                    match &input_peer {
                        tl::enums::InputPeer::Channel(c) => {
                            let req = tl::functions::channels::EditPhoto {
                                channel: tl::enums::InputChannel::InputChannel(
                                    tl::types::InputChannel {
                                        channel_id: c.channel_id,
                                        access_hash: c.access_hash,
                                    },
                                ),
                                photo: chat_photo,
                            };
                            self.client.rpc_write(&req).await?;
                        }
                        tl::enums::InputPeer::Chat(c) => {
                            let req = tl::functions::messages::EditChatPhoto {
                                chat_id: c.chat_id,
                                photo: chat_photo,
                            };
                            self.client.rpc_write(&req).await?;
                        }
                        _ => {}
                    }
                }
            } else if let Some(chat_photo) = self.chat_photo {
                match &input_peer {
                    tl::enums::InputPeer::Channel(c) => {
                        let req = tl::functions::channels::EditPhoto {
                            channel: tl::enums::InputChannel::InputChannel(
                                tl::types::InputChannel {
                                    channel_id: c.channel_id,
                                    access_hash: c.access_hash,
                                },
                            ),
                            photo: chat_photo,
                        };
                        self.client.rpc_write(&req).await?;
                    }
                    tl::enums::InputPeer::Chat(c) => {
                        let req = tl::functions::messages::EditChatPhoto {
                            chat_id: c.chat_id,
                            photo: chat_photo,
                        };
                        self.client.rpc_write(&req).await?;
                    }
                    _ => {}
                }
            }
        } else {
            // user or self

            if self.first_name.is_some() || self.last_name.is_some() || self.bio.is_some() {
                let req = tl::functions::account::UpdateProfile {
                    first_name: self.first_name,
                    last_name: self.last_name,
                    about: self.bio,
                };
                self.client.rpc_write(&req).await?;
            }
            if let Some(u) = self.username {
                let req = tl::functions::account::UpdateUsername { username: u };
                self.client.rpc_write(&req).await?;
            }
            if let Some(file) = self.photo {
                let req = tl::functions::photos::UploadProfilePhoto {
                    fallback: false,
                    bot: None,
                    file: Some(file.inner),
                    video: None,
                    video_start_ts: None,
                    video_emoji_markup: None,
                };
                self.client.rpc_write(&req).await?;
            }
            if let Some((doc_id, until)) = self.emoji_status {
                let emoji_status = match doc_id {
                    None => tl::enums::EmojiStatus::Empty,
                    Some(id) => tl::enums::EmojiStatus::EmojiStatus(tl::types::EmojiStatus {
                        document_id: id,
                        until,
                    }),
                };
                let req = tl::functions::account::UpdateEmojiStatus { emoji_status };
                self.client.rpc_write(&req).await?;
            }
        }

        Ok(())
    }
}