Skip to main content

layer_client/
types.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// NOTE:
5// The "Layer" project is no longer maintained or supported.
6// Its original purpose for personal SDK/APK experimentation and learning
7// has been fulfilled.
8//
9// Please use Ferogram instead:
10// https://github.com/ankit-chaubey/ferogram
11// Ferogram will receive future updates and development, although progress
12// may be slower.
13//
14// Ferogram is an async Telegram MTProto client library written in Rust.
15// Its implementation follows the behaviour of the official Telegram clients,
16// particularly Telegram Desktop and TDLib, and aims to provide a clean and
17// modern async interface for building Telegram clients and tools.
18
19//! Typed wrappers over raw TL user and chat types.
20//!
21//! The raw TL layer has `tl::enums::User` (two variants: `Empty` / `User`) and
22//! `tl::enums::Chat` (five variants: `Empty`, `Chat`, `Forbidden`,
23//! `Channel`, `ChannelForbidden`).  Working with them directly requires constant
24//! pattern-matching.  This module provides three thin wrappers: [`User`],
25//! [`Group`], and [`Channel`]: with uniform accessor APIs.
26
27use layer_tl_types as tl;
28
29// User
30
31/// Typed wrapper over `tl::enums::User`.
32#[derive(Debug, Clone)]
33pub struct User {
34    pub raw: tl::enums::User,
35}
36
37impl User {
38    /// Wrap a raw TL user.
39    pub fn from_raw(raw: tl::enums::User) -> Option<Self> {
40        match &raw {
41            tl::enums::User::Empty(_) => None,
42            tl::enums::User::User(_) => Some(Self { raw }),
43        }
44    }
45
46    fn inner(&self) -> &tl::types::User {
47        match &self.raw {
48            tl::enums::User::User(u) => u,
49            tl::enums::User::Empty(_) => unreachable!("User::Empty filtered in from_raw"),
50        }
51    }
52
53    /// Telegram user ID.
54    pub fn id(&self) -> i64 {
55        self.inner().id
56    }
57
58    /// Access hash needed for API calls.
59    pub fn access_hash(&self) -> Option<i64> {
60        self.inner().access_hash
61    }
62
63    /// First name.
64    pub fn first_name(&self) -> Option<&str> {
65        self.inner().first_name.as_deref()
66    }
67
68    /// Last name.
69    pub fn last_name(&self) -> Option<&str> {
70        self.inner().last_name.as_deref()
71    }
72
73    /// Username (without `@`).
74    pub fn username(&self) -> Option<&str> {
75        self.inner().username.as_deref()
76    }
77
78    /// Phone number, if visible.
79    pub fn phone(&self) -> Option<&str> {
80        self.inner().phone.as_deref()
81    }
82
83    /// `true` if this is a verified account.
84    pub fn verified(&self) -> bool {
85        self.inner().verified
86    }
87
88    /// `true` if this is a bot account.
89    pub fn bot(&self) -> bool {
90        self.inner().bot
91    }
92
93    /// `true` if the account is deleted.
94    pub fn deleted(&self) -> bool {
95        self.inner().deleted
96    }
97
98    /// `true` if the current user has blocked this user.
99    pub fn blocked(&self) -> bool {
100        false
101    }
102
103    /// `true` if this is a premium account.
104    pub fn premium(&self) -> bool {
105        self.inner().premium
106    }
107
108    /// Full display name (`first_name [last_name]`).
109    pub fn full_name(&self) -> String {
110        match (self.first_name(), self.last_name()) {
111            (Some(f), Some(l)) => format!("{f} {l}"),
112            (Some(f), None) => f.to_string(),
113            (None, Some(l)) => l.to_string(),
114            (None, None) => String::new(),
115        }
116    }
117
118    /// All active usernames (including the primary username).
119    pub fn usernames(&self) -> Vec<&str> {
120        let mut names = Vec::new();
121        // Primary username
122        if let Some(u) = self.inner().username.as_deref() {
123            names.push(u);
124        }
125        // Additional usernames
126        if let Some(extras) = &self.inner().usernames {
127            for u in extras {
128                let tl::enums::Username::Username(un) = u;
129                if un.active {
130                    names.push(un.username.as_str());
131                }
132            }
133        }
134        names
135    }
136
137    /// The user's current online status.
138    pub fn status(&self) -> Option<&tl::enums::UserStatus> {
139        self.inner().status.as_ref()
140    }
141
142    /// Profile photo, if set.
143    pub fn photo(&self) -> Option<&tl::types::UserProfilePhoto> {
144        match self.inner().photo.as_ref()? {
145            tl::enums::UserProfilePhoto::UserProfilePhoto(p) => Some(p),
146            _ => None,
147        }
148    }
149
150    /// `true` if this is the currently logged-in user.
151    pub fn is_self(&self) -> bool {
152        self.inner().is_self
153    }
154
155    /// `true` if this user is in the logged-in user's contact list.
156    pub fn contact(&self) -> bool {
157        self.inner().contact
158    }
159
160    /// `true` if the logged-in user is also in this user's contact list.
161    pub fn mutual_contact(&self) -> bool {
162        self.inner().mutual_contact
163    }
164
165    /// `true` if this account has been flagged as a scam.
166    pub fn scam(&self) -> bool {
167        self.inner().scam
168    }
169
170    /// `true` if this account has been restricted (e.g. spam-banned).
171    pub fn restricted(&self) -> bool {
172        self.inner().restricted
173    }
174
175    /// `true` if the bot does not display in inline mode publicly.
176    pub fn bot_privacy(&self) -> bool {
177        self.inner().bot_nochats
178    }
179
180    /// `true` if the bot supports being added to groups.
181    pub fn bot_supports_chats(&self) -> bool {
182        !self.inner().bot_nochats
183    }
184
185    /// `true` if the bot can be used inline even without a location share.
186    pub fn bot_inline_geo(&self) -> bool {
187        self.inner().bot_inline_geo
188    }
189
190    /// `true` if this account belongs to Telegram support staff.
191    pub fn support(&self) -> bool {
192        self.inner().support
193    }
194
195    /// Language code reported by the user's client.
196    pub fn lang_code(&self) -> Option<&str> {
197        self.inner().lang_code.as_deref()
198    }
199
200    /// Restriction reasons (why this account is unavailable in certain regions).
201    pub fn restriction_reason(&self) -> Vec<&tl::enums::RestrictionReason> {
202        self.inner()
203            .restriction_reason
204            .as_deref()
205            .unwrap_or(&[])
206            .iter()
207            .collect()
208    }
209
210    /// Bot inline placeholder text (shown in the compose bar when the user activates inline mode).
211    pub fn bot_inline_placeholder(&self) -> Option<&str> {
212        self.inner().bot_inline_placeholder.as_deref()
213    }
214
215    /// Convert to a `Peer` for use in API calls.
216    pub fn as_peer(&self) -> tl::enums::Peer {
217        tl::enums::Peer::User(tl::types::PeerUser { user_id: self.id() })
218    }
219
220    /// Convert to an `InputPeer` for API calls (requires access hash).
221    pub fn as_input_peer(&self) -> tl::enums::InputPeer {
222        match self.inner().access_hash {
223            Some(ah) => tl::enums::InputPeer::User(tl::types::InputPeerUser {
224                user_id: self.id(),
225                access_hash: ah,
226            }),
227            None => tl::enums::InputPeer::PeerSelf,
228        }
229    }
230}
231
232impl std::fmt::Display for User {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        let name = self.full_name();
235        if let Some(uname) = self.username() {
236            write!(f, "{name} (@{uname})")
237        } else {
238            write!(f, "{name} [{}]", self.id())
239        }
240    }
241}
242
243// Group
244
245/// Typed wrapper over `tl::types::Chat`.
246#[derive(Debug, Clone)]
247pub struct Group {
248    pub raw: tl::types::Chat,
249}
250
251impl Group {
252    /// Wrap from a raw `tl::enums::Chat`, returning `None` if it is not a
253    /// basic group (i.e. empty, forbidden, or a channel).
254    pub fn from_raw(raw: tl::enums::Chat) -> Option<Self> {
255        match raw {
256            tl::enums::Chat::Chat(c) => Some(Self { raw: c }),
257            tl::enums::Chat::Empty(_)
258            | tl::enums::Chat::Forbidden(_)
259            | tl::enums::Chat::Channel(_)
260            | tl::enums::Chat::ChannelForbidden(_) => None,
261        }
262    }
263
264    /// Group ID.
265    pub fn id(&self) -> i64 {
266        self.raw.id
267    }
268
269    /// Group title.
270    pub fn title(&self) -> &str {
271        &self.raw.title
272    }
273
274    /// Member count.
275    pub fn participants_count(&self) -> i32 {
276        self.raw.participants_count
277    }
278
279    /// `true` if the logged-in user is the creator.
280    pub fn creator(&self) -> bool {
281        self.raw.creator
282    }
283
284    /// `true` if the group has been migrated to a supergroup.
285    pub fn migrated_to(&self) -> Option<&tl::enums::InputChannel> {
286        self.raw.migrated_to.as_ref()
287    }
288
289    /// Convert to a `Peer`.
290    pub fn as_peer(&self) -> tl::enums::Peer {
291        tl::enums::Peer::Chat(tl::types::PeerChat { chat_id: self.id() })
292    }
293
294    /// Convert to an `InputPeer`.
295    pub fn as_input_peer(&self) -> tl::enums::InputPeer {
296        tl::enums::InputPeer::Chat(tl::types::InputPeerChat { chat_id: self.id() })
297    }
298}
299
300impl std::fmt::Display for Group {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        write!(f, "{} [group {}]", self.title(), self.id())
303    }
304}
305
306// Channel
307
308/// The kind of a channel or supergroup.
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum ChannelKind {
311    /// A broadcast channel (posts only, no member replies by default).
312    Broadcast,
313    /// A supergroup (all members can post).
314    Megagroup,
315    /// A gigagroup / broadcast group (large public broadcast supergroup).
316    Gigagroup,
317}
318
319/// Typed wrapper over `tl::types::Channel`.
320#[derive(Debug, Clone)]
321pub struct Channel {
322    pub raw: tl::types::Channel,
323}
324
325impl Channel {
326    /// Wrap from a raw `tl::enums::Chat`, returning `None` if it is not a channel.
327    pub fn from_raw(raw: tl::enums::Chat) -> Option<Self> {
328        match raw {
329            tl::enums::Chat::Channel(c) => Some(Self { raw: c }),
330            _ => None,
331        }
332    }
333
334    /// Channel ID.
335    pub fn id(&self) -> i64 {
336        self.raw.id
337    }
338
339    /// Access hash.
340    pub fn access_hash(&self) -> Option<i64> {
341        self.raw.access_hash
342    }
343
344    /// Channel / supergroup title.
345    pub fn title(&self) -> &str {
346        &self.raw.title
347    }
348
349    /// Username (without `@`), if public.
350    pub fn username(&self) -> Option<&str> {
351        self.raw.username.as_deref()
352    }
353
354    /// `true` if this is a supergroup (not a broadcast channel).
355    pub fn megagroup(&self) -> bool {
356        self.raw.megagroup
357    }
358
359    /// `true` if this is a broadcast channel.
360    pub fn broadcast(&self) -> bool {
361        self.raw.broadcast
362    }
363
364    /// `true` if this is a verified channel.
365    pub fn verified(&self) -> bool {
366        self.raw.verified
367    }
368
369    /// `true` if the channel is restricted.
370    pub fn restricted(&self) -> bool {
371        self.raw.restricted
372    }
373
374    /// `true` if the channel has signatures on posts.
375    pub fn signatures(&self) -> bool {
376        self.raw.signatures
377    }
378
379    /// Approximate member count (may be `None` for private channels).
380    pub fn participants_count(&self) -> Option<i32> {
381        self.raw.participants_count
382    }
383
384    /// The kind of this channel.
385    ///
386    /// Returns `ChannelKind::Megagroup` for supergroups, `ChannelKind::Broadcast` for
387    /// broadcast channels, and `ChannelKind::Gigagroup` for large broadcast groups.
388    pub fn kind(&self) -> ChannelKind {
389        if self.raw.megagroup {
390            ChannelKind::Megagroup
391        } else if self.raw.gigagroup {
392            ChannelKind::Gigagroup
393        } else {
394            ChannelKind::Broadcast
395        }
396    }
397
398    /// All active usernames (including the primary username).
399    pub fn usernames(&self) -> Vec<&str> {
400        let mut names = Vec::new();
401        if let Some(u) = self.raw.username.as_deref() {
402            names.push(u);
403        }
404        if let Some(extras) = &self.raw.usernames {
405            for u in extras {
406                let tl::enums::Username::Username(un) = u;
407                if un.active {
408                    names.push(un.username.as_str());
409                }
410            }
411        }
412        names
413    }
414
415    /// Profile photo, if set.
416    pub fn photo(&self) -> Option<&tl::types::ChatPhoto> {
417        match &self.raw.photo {
418            tl::enums::ChatPhoto::ChatPhoto(p) => Some(p),
419            _ => None,
420        }
421    }
422
423    /// Admin rights granted to the logged-in user in this channel, if any.
424    pub fn admin_rights(&self) -> Option<&tl::types::ChatAdminRights> {
425        match self.raw.admin_rights.as_ref()? {
426            tl::enums::ChatAdminRights::ChatAdminRights(r) => Some(r),
427        }
428    }
429
430    /// Restriction reasons (why this channel is unavailable in certain regions).
431    pub fn restriction_reason(&self) -> Vec<&tl::enums::RestrictionReason> {
432        self.raw
433            .restriction_reason
434            .as_deref()
435            .unwrap_or(&[])
436            .iter()
437            .collect()
438    }
439
440    /// Convert to a `Peer`.
441    pub fn as_peer(&self) -> tl::enums::Peer {
442        tl::enums::Peer::Channel(tl::types::PeerChannel {
443            channel_id: self.id(),
444        })
445    }
446
447    /// Convert to an `InputPeer` (requires access hash).
448    pub fn as_input_peer(&self) -> tl::enums::InputPeer {
449        match self.raw.access_hash {
450            Some(ah) => tl::enums::InputPeer::Channel(tl::types::InputPeerChannel {
451                channel_id: self.id(),
452                access_hash: ah,
453            }),
454            None => tl::enums::InputPeer::Empty,
455        }
456    }
457
458    /// Convert to an `InputChannel` for channel-specific RPCs.
459    pub fn as_input_channel(&self) -> tl::enums::InputChannel {
460        match self.raw.access_hash {
461            Some(ah) => tl::enums::InputChannel::InputChannel(tl::types::InputChannel {
462                channel_id: self.id(),
463                access_hash: ah,
464            }),
465            None => tl::enums::InputChannel::Empty,
466        }
467    }
468}
469
470impl std::fmt::Display for Channel {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        if let Some(uname) = self.username() {
473            write!(f, "{} (@{uname})", self.title())
474        } else {
475            write!(f, "{} [channel {}]", self.title(), self.id())
476        }
477    }
478}
479
480// Chat enum (unified)
481
482/// A unified chat type: either a basic [`Group`] or a [`Channel`]/supergroup.
483#[derive(Debug, Clone)]
484pub enum Chat {
485    Group(Group),
486    Channel(Box<Channel>),
487}
488
489impl Chat {
490    /// Attempt to construct from a raw `tl::enums::Chat`.
491    pub fn from_raw(raw: tl::enums::Chat) -> Option<Self> {
492        match &raw {
493            tl::enums::Chat::Chat(_) => Group::from_raw(raw).map(Chat::Group),
494            tl::enums::Chat::Channel(_) => {
495                Channel::from_raw(raw).map(|c| Chat::Channel(Box::new(c)))
496            }
497            _ => None,
498        }
499    }
500
501    /// Common ID regardless of variant.
502    pub fn id(&self) -> i64 {
503        match self {
504            Chat::Group(g) => g.id(),
505            Chat::Channel(c) => c.id(),
506        }
507    }
508
509    /// Common title regardless of variant.
510    pub fn title(&self) -> &str {
511        match self {
512            Chat::Group(g) => g.title(),
513            Chat::Channel(c) => c.title(),
514        }
515    }
516
517    /// Convert to a `Peer`.
518    pub fn as_peer(&self) -> tl::enums::Peer {
519        match self {
520            Chat::Group(g) => g.as_peer(),
521            Chat::Channel(c) => c.as_peer(),
522        }
523    }
524
525    /// Convert to an `InputPeer`.
526    pub fn as_input_peer(&self) -> tl::enums::InputPeer {
527        match self {
528            Chat::Group(g) => g.as_input_peer(),
529            Chat::Channel(c) => c.as_input_peer(),
530        }
531    }
532}