Skip to main content

layer_client/
participants.rs

1//! Chat participant and member management.
2//!
3//! Provides [`Client::get_participants`], kick, ban, and admin rights management.
4
5use layer_tl_types as tl;
6use layer_tl_types::{Cursor, Deserializable};
7
8use crate::{Client, InvocationError};
9
10// ─── Participant ──────────────────────────────────────────────────────────────
11
12/// A member of a chat, group or channel.
13#[derive(Debug, Clone)]
14pub struct Participant {
15    /// The user object.
16    pub user: tl::types::User,
17    /// Their role/status in the chat.
18    pub status: ParticipantStatus,
19}
20
21/// The role of a participant.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ParticipantStatus {
24    /// Regular member.
25    Member,
26    /// The channel/group creator.
27    Creator,
28    /// Admin (may have custom title).
29    Admin,
30    /// Restricted / banned user.
31    Restricted,
32    /// Left the group.
33    Left,
34    /// Kicked (banned) from the group.
35    Banned,
36}
37
38// ─── Client methods ───────────────────────────────────────────────────────────
39
40impl Client {
41    /// Fetch all participants of a chat, group or channel.
42    ///
43    /// For channels this uses `channels.getParticipants`; for basic groups it
44    /// uses `messages.getFullChat`.
45    ///
46    /// Returns up to `limit` participants; pass `0` for the default (200 for channels).
47    pub async fn get_participants(
48        &self,
49        peer:  tl::enums::Peer,
50        limit: i32,
51    ) -> Result<Vec<Participant>, InvocationError> {
52        match &peer {
53            tl::enums::Peer::Channel(c) => {
54                let cache       = self.inner.peer_cache.lock().await;
55                let access_hash = cache.channels.get(&c.channel_id).copied().unwrap_or(0);
56                drop(cache);
57                self.get_channel_participants(c.channel_id, access_hash, limit).await
58            }
59            tl::enums::Peer::Chat(c) => {
60                self.get_chat_participants(c.chat_id).await
61            }
62            _ => Err(InvocationError::Deserialize("get_participants: peer must be a chat or channel".into())),
63        }
64    }
65
66    async fn get_channel_participants(
67        &self,
68        channel_id:  i64,
69        access_hash: i64,
70        limit:       i32,
71    ) -> Result<Vec<Participant>, InvocationError> {
72        let limit = if limit <= 0 { 200 } else { limit };
73        let req = tl::functions::channels::GetParticipants {
74            channel: tl::enums::InputChannel::InputChannel(tl::types::InputChannel {
75                channel_id, access_hash,
76            }),
77            filter:  tl::enums::ChannelParticipantsFilter::ChannelParticipantsRecent,
78            offset:  0,
79            limit,
80            hash:    0,
81        };
82        let body    = self.rpc_call_raw_pub(&req).await?;
83        let mut cur = Cursor::from_slice(&body);
84        let raw     = match tl::enums::channels::ChannelParticipants::deserialize(&mut cur)? {
85            tl::enums::channels::ChannelParticipants::ChannelParticipants(p) => p,
86            tl::enums::channels::ChannelParticipants::NotModified => return Ok(vec![]),
87        };
88
89        // Build user map
90        let user_map: std::collections::HashMap<i64, tl::types::User> = raw.users.into_iter()
91            .filter_map(|u| match u { tl::enums::User::User(u) => Some((u.id, u)), _ => None })
92            .collect();
93
94        // Cache them
95        {
96            let mut cache = self.inner.peer_cache.lock().await;
97            for u in user_map.values() {
98                if let Some(h) = u.access_hash { cache.users.insert(u.id, h); }
99            }
100        }
101
102        let mut result = Vec::new();
103        for p in raw.participants {
104            let (user_id, status) = match &p {
105                tl::enums::ChannelParticipant::ChannelParticipant(x) => (x.user_id, ParticipantStatus::Member),
106                tl::enums::ChannelParticipant::ParticipantSelf(x)    => (x.user_id, ParticipantStatus::Member),
107                tl::enums::ChannelParticipant::Creator(x)            => (x.user_id, ParticipantStatus::Creator),
108                tl::enums::ChannelParticipant::Admin(x)              => (x.user_id, ParticipantStatus::Admin),
109                tl::enums::ChannelParticipant::Banned(x)             => (x.peer.user_id_or(0), ParticipantStatus::Banned),
110                tl::enums::ChannelParticipant::Left(x)               => (x.peer.user_id_or(0), ParticipantStatus::Left),
111            };
112            if let Some(user) = user_map.get(&user_id).cloned() {
113                result.push(Participant { user, status });
114            }
115        }
116        Ok(result)
117    }
118
119    async fn get_chat_participants(&self, chat_id: i64) -> Result<Vec<Participant>, InvocationError> {
120        let req  = tl::functions::messages::GetFullChat { chat_id };
121        let body = self.rpc_call_raw_pub(&req).await?;
122        let mut cur = Cursor::from_slice(&body);
123        let full: tl::types::messages::ChatFull = match tl::enums::messages::ChatFull::deserialize(&mut cur)? {
124            tl::enums::messages::ChatFull::ChatFull(c) => c,
125        };
126
127        let user_map: std::collections::HashMap<i64, tl::types::User> = full.users.into_iter()
128            .filter_map(|u| match u { tl::enums::User::User(u) => Some((u.id, u)), _ => None })
129            .collect();
130
131        {
132            let mut cache = self.inner.peer_cache.lock().await;
133            for u in user_map.values() {
134                if let Some(h) = u.access_hash { cache.users.insert(u.id, h); }
135            }
136        }
137
138        let participants = match &full.full_chat {
139            tl::enums::ChatFull::ChatFull(cf) => match &cf.participants {
140                tl::enums::ChatParticipants::ChatParticipants(p) => p.participants.clone(),
141                tl::enums::ChatParticipants::Forbidden(_)        => vec![],
142            },
143            tl::enums::ChatFull::ChannelFull(_) => {
144                return Err(InvocationError::Deserialize(
145                    "get_chat_participants: peer is a channel, use get_participants with a Channel peer instead".into()
146                ));
147            }
148        };
149
150        let mut result = Vec::new();
151        for p in participants {
152            let (user_id, status) = match p {
153                tl::enums::ChatParticipant::ChatParticipant(x) => (x.user_id, ParticipantStatus::Member),
154                tl::enums::ChatParticipant::Creator(x)          => (x.user_id, ParticipantStatus::Creator),
155                tl::enums::ChatParticipant::Admin(x)            => (x.user_id, ParticipantStatus::Admin),
156            };
157            if let Some(user) = user_map.get(&user_id).cloned() {
158                result.push(Participant { user, status });
159            }
160        }
161        Ok(result)
162    }
163
164    /// Kick a user from a basic group (chat). For channels, use [`ban_participant`].
165    pub async fn kick_participant(
166        &self,
167        chat_id: i64,
168        user_id: i64,
169    ) -> Result<(), InvocationError> {
170        let cache       = self.inner.peer_cache.lock().await;
171        let access_hash = cache.users.get(&user_id).copied().unwrap_or(0);
172        drop(cache);
173        let req = tl::functions::messages::DeleteChatUser {
174            revoke_history: false,
175            chat_id,
176            user_id: tl::enums::InputUser::InputUser(tl::types::InputUser { user_id, access_hash }),
177        };
178        self.rpc_call_raw_pub(&req).await?;
179        Ok(())
180    }
181
182    /// Ban a user from a channel or supergroup.
183    ///
184    /// Pass `until_date = 0` for a permanent ban.
185    pub async fn ban_participant(
186        &self,
187        channel:    tl::enums::Peer,
188        user_id:    i64,
189        until_date: i32,
190    ) -> Result<(), InvocationError> {
191        let (channel_id, ch_hash) = match &channel {
192            tl::enums::Peer::Channel(c) => {
193                let h = self.inner.peer_cache.lock().await.channels.get(&c.channel_id).copied().unwrap_or(0);
194                (c.channel_id, h)
195            }
196            _ => return Err(InvocationError::Deserialize("ban_participant: peer must be a channel".into())),
197        };
198        let user_hash = self.inner.peer_cache.lock().await.users.get(&user_id).copied().unwrap_or(0);
199
200        let req = tl::functions::channels::EditBanned {
201            channel: tl::enums::InputChannel::InputChannel(tl::types::InputChannel {
202                channel_id: channel_id, access_hash: ch_hash,
203            }),
204            participant: tl::enums::InputPeer::User(tl::types::InputPeerUser {
205                user_id, access_hash: user_hash,
206            }),
207            banned_rights: tl::enums::ChatBannedRights::ChatBannedRights(tl::types::ChatBannedRights {
208                view_messages:   true,
209                send_messages:   true,
210                send_media:      true,
211                send_stickers:   true,
212                send_gifs:       true,
213                send_games:      true,
214                send_inline:     true,
215                embed_links:     true,
216                send_polls:      true,
217                change_info:     true,
218                invite_users:    true,
219                pin_messages:    true,
220                manage_topics:   false,
221                send_photos:     false,
222                send_videos:     false,
223                send_roundvideos: false,
224                send_audios:     false,
225                send_voices:     false,
226                send_docs:       false,
227                send_plain:      false,
228                edit_rank:       false,
229                until_date,
230            }),
231        };
232        self.rpc_call_raw_pub(&req).await?;
233        Ok(())
234    }
235
236    /// Promote (or demote) a user to admin in a channel or supergroup.
237    ///
238    /// Pass `promote = true` to grant admin rights, `false` to remove them.
239    pub async fn promote_participant(
240        &self,
241        channel: tl::enums::Peer,
242        user_id: i64,
243        promote: bool,
244    ) -> Result<(), InvocationError> {
245        let (channel_id, ch_hash) = match &channel {
246            tl::enums::Peer::Channel(c) => {
247                let h = self.inner.peer_cache.lock().await.channels.get(&c.channel_id).copied().unwrap_or(0);
248                (c.channel_id, h)
249            }
250            _ => return Err(InvocationError::Deserialize("promote_participant: peer must be a channel".into())),
251        };
252        let user_hash = self.inner.peer_cache.lock().await.users.get(&user_id).copied().unwrap_or(0);
253
254        let rights = if promote {
255            tl::types::ChatAdminRights {
256                change_info:            true,
257                post_messages:          true,
258                edit_messages:          true,
259                delete_messages:        true,
260                ban_users:              true,
261                invite_users:           true,
262                pin_messages:           true,
263                add_admins:             false,
264                anonymous:              false,
265                manage_call:            true,
266                other:                  false,
267                manage_topics:          false,
268                post_stories:           false,
269                edit_stories:           false,
270                delete_stories:         false,
271                manage_direct_messages: false,
272                manage_ranks:           false,
273            }
274        } else {
275            tl::types::ChatAdminRights {
276                change_info:            false,
277                post_messages:          false,
278                edit_messages:          false,
279                delete_messages:        false,
280                ban_users:              false,
281                invite_users:           false,
282                pin_messages:           false,
283                add_admins:             false,
284                anonymous:              false,
285                manage_call:            false,
286                other:                  false,
287                manage_topics:          false,
288                post_stories:           false,
289                edit_stories:           false,
290                delete_stories:         false,
291                manage_direct_messages: false,
292                manage_ranks:           false,
293            }
294        };
295
296        let req = tl::functions::channels::EditAdmin {
297            channel: tl::enums::InputChannel::InputChannel(tl::types::InputChannel {
298                channel_id, access_hash: ch_hash,
299            }),
300            user_id: tl::enums::InputUser::InputUser(tl::types::InputUser { user_id, access_hash: user_hash }),
301            admin_rights: tl::enums::ChatAdminRights::ChatAdminRights(rights),
302            rank: None,
303        };
304        self.rpc_call_raw_pub(&req).await?;
305        Ok(())
306    }
307
308    /// Iterate profile photos of a user or channel.
309    ///
310    /// Returns a list of photo objects (up to `limit`).
311    pub async fn get_profile_photos(
312        &self,
313        peer:  tl::enums::Peer,
314        limit: i32,
315    ) -> Result<Vec<tl::enums::Photo>, InvocationError> {
316        let input_peer = {
317            let cache = self.inner.peer_cache.lock().await;
318            cache.peer_to_input(&peer)
319        };
320
321        let req = tl::functions::photos::GetUserPhotos {
322            user_id: match &input_peer {
323                tl::enums::InputPeer::User(u) => tl::enums::InputUser::InputUser(
324                    tl::types::InputUser { user_id: u.user_id, access_hash: u.access_hash }
325                ),
326                tl::enums::InputPeer::PeerSelf => tl::enums::InputUser::UserSelf,
327                _ => return Err(InvocationError::Deserialize("get_profile_photos: peer must be a user".into())),
328            },
329            offset: 0,
330            max_id: 0,
331            limit,
332        };
333        let body    = self.rpc_call_raw_pub(&req).await?;
334        let mut cur = Cursor::from_slice(&body);
335        match tl::enums::photos::Photos::deserialize(&mut cur)? {
336            tl::enums::photos::Photos::Photos(p)  => Ok(p.photos),
337            tl::enums::photos::Photos::Slice(p)   => Ok(p.photos),
338        }
339    }
340
341    /// Search for a peer (user, group, or channel) by name prefix.
342    ///
343    /// Searches contacts, dialogs, and globally. Returns combined results.
344    pub async fn search_peer(
345        &self,
346        query: &str,
347    ) -> Result<Vec<tl::enums::Peer>, InvocationError> {
348        let req  = tl::functions::contacts::Search {
349            q:   query.to_string(),
350            limit: 20,
351        };
352        let body = self.rpc_call_raw_pub(&req).await?;
353        let mut cur = Cursor::from_slice(&body);
354        let found = match tl::enums::contacts::Found::deserialize(&mut cur)? {
355            tl::enums::contacts::Found::Found(f) => f,
356        };
357
358        self.cache_users_slice_pub(&found.users).await;
359        self.cache_chats_slice_pub(&found.chats).await;
360
361        let mut peers = Vec::new();
362        for r in found.my_results.iter().chain(found.results.iter()) {
363            peers.push(r.clone());
364        }
365        Ok(peers)
366    }
367
368    /// Send a reaction to a message.
369    ///
370    /// `reaction` should be an emoji string like `"👍"` or an empty string to remove.
371    pub async fn send_reaction(
372        &self,
373        peer:       tl::enums::Peer,
374        message_id: i32,
375        reaction:   &str,
376    ) -> Result<(), InvocationError> {
377        let input_peer = {
378            let cache = self.inner.peer_cache.lock().await;
379            cache.peer_to_input(&peer)
380        };
381
382        let reactions = if reaction.is_empty() {
383            vec![]
384        } else {
385            vec![tl::enums::Reaction::Emoji(tl::types::ReactionEmoji {
386                emoticon: reaction.to_string(),
387            })]
388        };
389
390        let req = tl::functions::messages::SendReaction {
391            big:        false,
392            add_to_recent: false,
393            peer:       input_peer,
394            msg_id:     message_id,
395            reaction:   Some(reactions),
396        };
397        self.rpc_call_raw_pub(&req).await?;
398        Ok(())
399    }
400}
401
402// ─── Helper extension for Peer ────────────────────────────────────────────────
403
404trait PeerUserIdExt {
405    fn user_id_or(&self, default: i64) -> i64;
406}
407
408impl PeerUserIdExt for tl::enums::Peer {
409    fn user_id_or(&self, default: i64) -> i64 {
410        match self {
411            tl::enums::Peer::User(u) => u.user_id,
412            _ => default,
413        }
414    }
415}