Skip to main content

simploxide_client/
bot.rs

1use simploxide_api_types::{
2    AddressSettings, AutoAccept, CIDeleteMode, ChatListQuery, ChatPeerType, Contact,
3    CreatedConnLink, GroupInfo, GroupMember, GroupMemberRole, GroupProfile, JsonObject,
4    LocalProfile, MsgContent, NewUser, PaginationByTime, PendingContactConnection, Preferences,
5    Profile, User,
6    client_api::{ClientApi, ClientApiError as _, UndocumentedResponse},
7    commands::{
8        ApiAddContact, ApiConnectPlan, ApiGetChats, ApiNewGroup, ApiNewPublicGroup,
9        ApiSetActiveUser, ApiSetProfileAddress, ApiSetUserAutoAcceptMemberContacts,
10    },
11    responses::{
12        AcceptingContactRequestResponse, ApiChatsResponse, ApiDeleteChatResponse,
13        ApiNewPublicGroupResponse, ApiUpdateChatItemResponse, ApiUpdateProfileResponse,
14        CancelFileResponse, ChatItemReactionResponse, ChatItemsDeletedResponse, CmdOkResponse,
15        ConnectResponse, ConnectionPlanResponse, ContactPrefsUpdatedResponse,
16        ContactRequestRejectedResponse, GroupCreatedResponse, GroupLinkCreatedResponse,
17        GroupLinkDeletedResponse, GroupUpdatedResponse, LeftMemberUserResponse,
18        MemberAcceptedResponse, MembersBlockedForAllUserResponse, MembersRoleUserResponse,
19        SentGroupInvitationResponse, UserAcceptedGroupSentResponse, UserDeletedMembersResponse,
20        UserProfileUpdatedResponse,
21    },
22};
23
24use std::sync::Arc;
25
26use crate::{
27    ext::{
28        AcceptFileBuilder, AddGroupRelaysResponse, ClientApiExt as _, DeleteMode,
29        GetGroupRelaysResponse, GroupLinkResult, Reaction,
30    },
31    id::{
32        ChatId, ContactId, ContactRequestId, FileId, GroupId, MemberId, MessageId, RelayId, UserId,
33    },
34    messages::{MessageBuilder, MessageLike, MulticastBuilder},
35    preferences,
36    preview::ImagePreview,
37};
38
39/// A cheaply cloneable handle to initialized SimpleX bot.
40#[derive(Clone)]
41pub struct Bot<C> {
42    client: C,
43    user_id: i64,
44}
45
46impl<C> Bot<C> {
47    pub fn client(&self) -> &C {
48        &self.client
49    }
50
51    pub fn user_id(&self) -> UserId {
52        UserId(self.user_id)
53    }
54}
55
56impl<C: ClientApi> Bot<C> {
57    pub async fn init(client: C, settings: BotSettings) -> Result<Self, C::Error> {
58        let avatar = if let Some(preview) = settings.avatar {
59            Some(preview.resolve().await)
60        } else {
61            None
62        };
63
64        let mut users = client.users().await?;
65
66        match users.iter_mut().find_map(|info| {
67            (info.user.profile.display_name == settings.display_name).then_some(&mut info.user)
68        }) {
69            Some(user) => {
70                if !user.active_user {
71                    client
72                        .api_set_active_user(ApiSetActiveUser::new(user.user_id))
73                        .await?;
74                }
75
76                let bot = Bot {
77                    client,
78                    user_id: user.user_id,
79                };
80
81                bot.setup_auto_accept(settings.auto_accept, user.profile.contact_link.is_some())
82                    .await?;
83
84                let mut profile = match settings.profile_settings {
85                    Some(BotProfileSettings::Preferences(preferences)) => {
86                        let mut current_profile = extract_profile(&mut user.profile);
87                        current_profile.preferences = Some(preferences);
88                        current_profile
89                    }
90                    Some(BotProfileSettings::FullProfile(profile)) => profile,
91                    None => Self::default_profile(settings.display_name),
92                };
93                profile.image = avatar;
94                bot.client.api_update_profile(user.user_id, profile).await?;
95
96                Ok(bot)
97            }
98            None => {
99                let mut bot_profile = match settings.profile_settings {
100                    Some(BotProfileSettings::Preferences(preferences)) => {
101                        let mut profile = Self::default_profile(settings.display_name.clone());
102                        profile.preferences = Some(preferences);
103                        profile
104                    }
105                    Some(BotProfileSettings::FullProfile(profile)) => profile,
106                    None => Self::default_profile(settings.display_name.clone()),
107                };
108                bot_profile.image = avatar;
109
110                let response = client
111                    .new_user(NewUser {
112                        profile: Some(bot_profile),
113                        past_timestamp: false,
114                        user_chat_relay: false,
115                        undocumented: Default::default(),
116                    })
117                    .await?;
118
119                let bot = Bot {
120                    client,
121                    user_id: response.user.user_id,
122                };
123
124                bot.setup_auto_accept(settings.auto_accept, false).await?;
125
126                Ok(bot)
127            }
128        }
129    }
130
131    async fn setup_auto_accept(
132        &self,
133        auto_accept: Option<String>,
134        has_existing_address: bool,
135    ) -> Result<(), C::Error> {
136        if let Some(welcome_message) = auto_accept {
137            if !has_existing_address {
138                self.get_or_create_address().await?;
139                self.publish_address().await?;
140            }
141
142            self.configure_address(AddressSettings {
143                business_address: false,
144                auto_accept: Some(AutoAccept {
145                    accept_incognito: false,
146                    undocumented: Default::default(),
147                }),
148                auto_reply: (!welcome_message.is_empty())
149                    .then(|| MsgContent::make_text(welcome_message)),
150                undocumented: Default::default(),
151            })
152            .await?;
153        } else if has_existing_address {
154            self.configure_address(AddressSettings {
155                business_address: false,
156                auto_accept: None,
157                auto_reply: None,
158                undocumented: Default::default(),
159            })
160            .await?;
161
162            self.hide_address().await?;
163        }
164
165        Ok(())
166    }
167
168    /// This method allows ot wrap or replace the underlying bot client.
169    ///
170    /// You can define your own clients implementing the [`ClientApi`] trait and then you can
171    /// extend the bot functionalitty by implementing extension methods on `Bot<YourCustomClient>`
172    /// type.
173    pub fn wrap_client<W, F>(self, wrap: F) -> Bot<W>
174    where
175        W: ClientApi,
176        F: FnOnce(C) -> W,
177    {
178        let new_client = wrap(self.client);
179
180        Bot {
181            client: new_client,
182            user_id: self.user_id,
183        }
184    }
185
186    /// Returns a minimal bot profile with conservative defaults: no files, calls, reactions, or voice.
187    pub fn default_profile(name: impl Into<String>) -> Profile {
188        Profile {
189            display_name: name.into(),
190            full_name: String::default(),
191            short_descr: None,
192            image: None,
193            contact_link: None,
194            preferences: Some(Preferences {
195                timed_messages: preferences::timed_messages::NO,
196                full_delete: preferences::YES,
197                reactions: preferences::NO,
198                voice: preferences::NO,
199                files: preferences::NO,
200                calls: preferences::NO,
201                sessions: preferences::NO,
202                commands: None,
203                undocumented: Default::default(),
204            }),
205            peer_type: Some(ChatPeerType::Bot),
206            undocumented: serde_json::Value::Null,
207        }
208    }
209
210    /// Get full bot user info
211    pub async fn info(&self) -> Result<Arc<User>, C::Error> {
212        let response = self.client.show_active_user().await?;
213        Ok(Arc::new(response.user.clone()))
214    }
215
216    /// Initiates the connection sequence.
217    ///
218    /// - If contact is already connected returns either [UndocumentedResponse::Documented] with
219    ///   [ConnectResponse::ContactAlreadyExists] or [UndocumentedResponse::Undocumented] with some
220    ///   other responses(_this is an upstream mistake, SimpleX docs don't list all possible
221    ///   responses for this method_).
222    ///
223    /// - If contact is not connected returns [UndocumentedResponse::Documented] with one of the
224    ///   remaining [ConnectResponse] variants. The implementation must listen for
225    ///   [crate::events::ContactConnected] or [crate::events::UserJoinedGroup] to confirm the
226    ///   connection.
227    pub async fn initiate_connection(
228        &self,
229        link: impl Into<String>,
230    ) -> Result<UndocumentedResponse<ConnectResponse>, C::Error> {
231        self.client.initiate_connection(link).await
232    }
233
234    /// Inspect a SimpleX link before connecting: resolves its type (contact address, group link,
235    /// or 1-time invitation) and reports whether the bot is already connected via it.
236    pub async fn check_connection_plan(
237        &self,
238        link: impl Into<String>,
239    ) -> Result<Arc<ConnectionPlanResponse>, C::Error> {
240        self.client
241            .api_connect_plan(ApiConnectPlan {
242                user_id: self.user_id,
243                connection_link: Some(link.into()),
244                resolve_known: false,
245                link_owner_sig: None,
246            })
247            .await
248    }
249
250    /// Create one-time-invitation link. Can be used for admin-access or for private connections
251    /// with other bots. The [PendingContactConnection::pcc_conn_id] can be matched with
252    /// [crate::types::Connection::conn_id] to recognize the user connected by this link when handling the
253    /// [crate::events::ContactConnected] event(see [crate::events::ContactConnected::contact])
254    pub async fn create_invitation_link(
255        &self,
256    ) -> Result<(String, Arc<PendingContactConnection>), C::Error> {
257        let response = self
258            .client
259            .api_add_contact(ApiAddContact::new(self.user_id))
260            .await?;
261
262        let link = extract_address(&response.conn_link_invitation);
263        let pcc = Arc::new(response.connection.clone());
264
265        Ok((link, pcc))
266    }
267
268    pub async fn create_address(&self) -> Result<String, C::Error> {
269        let response = self.client.api_create_my_address(self.user_id).await?;
270        Ok(extract_address(&response.conn_link_contact))
271    }
272
273    /// Throws [crate::types::errors::StoreError::UserContactLinkNotFound] if bot doesn't have an address. Use
274    /// [Self::get_or_create_address] to ensure that address is available
275    pub async fn address(&self) -> Result<String, C::Error> {
276        let response = self.client.api_show_my_address(self.user_id).await?;
277        Ok(extract_address(&response.contact_link.conn_link_contact))
278    }
279
280    pub async fn get_or_create_address(&self) -> Result<String, C::Error> {
281        match self.address().await {
282            Ok(address) => Ok(address),
283            Err(e)
284                if e.bad_response()
285                    .and_then(|e| {
286                        e.chat_error().and_then(|e| {
287                            e.error_store().map(|e| e.is_user_contact_link_not_found())
288                        })
289                    })
290                    .unwrap_or(false) =>
291            {
292                self.create_address().await
293            }
294            Err(e) => Err(e),
295        }
296    }
297
298    pub async fn configure_address(&self, settings: AddressSettings) -> Result<(), C::Error> {
299        self.client
300            .api_set_address_settings(self.user_id, settings)
301            .await
302            .map(drop)
303    }
304
305    /// Make address visible in bot/user profile
306    pub async fn publish_address(&self) -> Result<Arc<UserProfileUpdatedResponse>, C::Error> {
307        self.client
308            .api_set_profile_address(ApiSetProfileAddress {
309                user_id: self.user_id,
310                enable: true,
311            })
312            .await
313    }
314
315    /// Hide address from bot/user profile
316    pub async fn hide_address(&self) -> Result<Arc<UserProfileUpdatedResponse>, C::Error> {
317        self.client
318            .api_set_profile_address(ApiSetProfileAddress {
319                user_id: self.user_id,
320                enable: false,
321            })
322            .await
323    }
324
325    pub async fn delete_address(&self) -> Result<(), C::Error> {
326        self.client.api_delete_my_address(self.user_id).await?;
327        Ok(())
328    }
329
330    /// Fetches the current profile and applies `updater` to it before saving.
331    pub async fn update_profile<F>(&self, updater: F) -> Result<ApiUpdateProfileResponse, C::Error>
332    where
333        F: 'static + Send + FnOnce(&mut Profile),
334    {
335        let mut response = self.client.show_active_user().await?;
336        let response = Arc::get_mut(&mut response).unwrap();
337
338        let mut profile = extract_profile(&mut response.user.profile);
339        updater(&mut profile);
340
341        self.client.api_update_profile(self.user_id, profile).await
342    }
343
344    pub async fn set_display_name(
345        &self,
346        name: impl Into<String>,
347    ) -> Result<ApiUpdateProfileResponse, C::Error> {
348        let name = name.into();
349        self.update_profile(move |profile| profile.display_name = name)
350            .await
351    }
352
353    pub async fn set_full_name(
354        &self,
355        full_name: impl Into<String>,
356    ) -> Result<ApiUpdateProfileResponse, C::Error> {
357        let full_name = full_name.into();
358        self.update_profile(move |profile| profile.full_name = full_name)
359            .await
360    }
361
362    pub async fn set_bio(
363        &self,
364        bio: impl Into<String>,
365    ) -> Result<ApiUpdateProfileResponse, C::Error> {
366        let bio = bio.into();
367        self.update_profile(move |profile| profile.short_descr = Some(bio))
368            .await
369    }
370
371    /// Set the bot/user avatar
372    pub async fn set_avatar(
373        &self,
374        avatar: ImagePreview,
375    ) -> Result<ApiUpdateProfileResponse, C::Error> {
376        let image = avatar.resolve().await;
377        self.update_profile(move |profile| profile.image = Some(image))
378            .await
379    }
380
381    /// Set account type `Bot` or `Person`
382    pub async fn set_peer_type(
383        &self,
384        peer_type: ChatPeerType,
385    ) -> Result<ApiUpdateProfileResponse, C::Error> {
386        self.update_profile(move |profile| profile.peer_type = Some(peer_type))
387            .await
388    }
389
390    /// Set global preferences
391    pub async fn set_preferences(
392        &self,
393        preferences: Preferences,
394    ) -> Result<ApiUpdateProfileResponse, C::Error> {
395        self.update_profile(move |profile| profile.preferences = Some(preferences))
396            .await
397    }
398
399    /// Update global preferences via closure accepting current preferences
400    pub async fn update_preferences<F>(
401        &self,
402        updater: F,
403    ) -> Result<ApiUpdateProfileResponse, C::Error>
404    where
405        F: 'static + Send + FnOnce(&mut Preferences),
406    {
407        let mut response = self.client.show_active_user().await?;
408        let response = Arc::get_mut(&mut response).unwrap();
409
410        let mut profile = extract_profile(&mut response.user.profile);
411        let mut preferences = extract_preferences(&mut profile.preferences);
412        updater(&mut preferences);
413        profile.preferences = Some(preferences);
414
415        self.client.api_update_profile(self.user_id, profile).await
416    }
417
418    /// Set preferences for particular contact
419    pub async fn set_contact_preferences<CID: Into<ContactId>>(
420        &self,
421        contact_id: CID,
422        preferences: Preferences,
423    ) -> Result<Arc<ContactPrefsUpdatedResponse>, C::Error> {
424        self.client
425            .api_set_contact_prefs(contact_id.into().0, preferences)
426            .await
427    }
428
429    /// Tweak global preferences for particular contact via closure accepting current global
430    /// preferences
431    pub async fn tweak_preferences_for_contact<CID: Into<ContactId>, F>(
432        &self,
433        contact_id: CID,
434        updater: F,
435    ) -> Result<Arc<ContactPrefsUpdatedResponse>, C::Error>
436    where
437        F: 'static + Send + FnOnce(&mut Preferences),
438    {
439        let mut response = self.client.show_active_user().await?;
440        let response = Arc::get_mut(&mut response).unwrap();
441
442        let mut preferences = extract_preferences(&mut response.user.profile.preferences);
443        updater(&mut preferences);
444
445        self.client
446            .api_set_contact_prefs(contact_id.into().0, preferences)
447            .await
448    }
449
450    /// Get all contacts known to the bot(connected or not)
451    pub async fn contacts(&self) -> Result<Vec<Contact>, C::Error> {
452        self.client.contacts(self.user_id()).await
453    }
454
455    /// Get all groups known to the bot
456    pub async fn groups(&self) -> Result<Vec<GroupInfo>, C::Error> {
457        self.client.groups(self.user_id()).await
458    }
459
460    /// Accept contact request
461    pub async fn accept_contact<CRID: Into<ContactRequestId>>(
462        &self,
463        contact_request_id: CRID,
464    ) -> Result<Arc<AcceptingContactRequestResponse>, <C as ClientApi>::Error> {
465        self.client.accept_contact(contact_request_id).await
466    }
467
468    /// Reject contact request
469    pub async fn reject_contact<CRID: Into<ContactRequestId>>(
470        &self,
471        contact_request_id: CRID,
472    ) -> Result<Arc<ContactRequestRejectedResponse>, <C as ClientApi>::Error> {
473        self.client.reject_contact(contact_request_id).await
474    }
475
476    /// Send a message. See the [`messages`](crate::messages) module for details
477    pub fn send_msg<CID: Into<ChatId>, M: MessageLike>(
478        &self,
479        chat_id: CID,
480        msg: M,
481    ) -> MessageBuilder<'_, C, M::Kind> {
482        self.client.send_message(chat_id.into(), msg)
483    }
484
485    /// Send the same message to multiple recepients
486    pub fn multicast<I, M>(&self, chat_ids: I, msg: M) -> MulticastBuilder<'_, I, C, M::Kind>
487    where
488        I: IntoIterator<Item = ChatId>,
489        M: MessageLike,
490    {
491        self.client.multicast_message(chat_ids, msg)
492    }
493
494    /// Returns a list of all known chat IDs
495    pub async fn chat_ids(&self) -> Result<impl Iterator<Item = ChatId>, C::Error> {
496        self.chat_ids_with(|_| true).await
497    }
498
499    /// Returns a list of all known chat IDs matching the filter `f`.
500    pub async fn chat_ids_with<F>(
501        &self,
502        f: F,
503    ) -> Result<impl 'static + Send + Iterator<Item = ChatId>, C::Error>
504    where
505        F: 'static + Send + FnMut(&ChatId) -> bool,
506    {
507        let (contacts, groups) = futures::future::try_join(self.contacts(), self.groups()).await?;
508
509        Ok(contacts
510            .into_iter()
511            .map(ChatId::from)
512            .chain(groups.into_iter().map(ChatId::from))
513            .filter(f))
514    }
515
516    /// Generate a [MulticastBuilder] that is ready to send messages to all known chats
517    ///
518    /// ```rust
519    /// bot.prepare_broadcast("Hey, what's up?!")
520    ///    .await
521    ///    .send()
522    ///    .await?;
523    /// ```
524    pub async fn prepare_broadcast<M: MessageLike>(
525        &self,
526        msg: M,
527    ) -> Result<
528        MulticastBuilder<'_, impl 'static + Send + Iterator<Item = ChatId>, C, M::Kind>,
529        C::Error,
530    > {
531        self.prepare_broadcast_with(msg, |_| true).await
532    }
533
534    /// Generate a [MulticastBuilder] that is ready to send messages to chats matching the filter
535    ///
536    /// ```rust
537    /// bot.prepare_broadcast_with("What do you think about this logo?", |chat| chat.is_direct())
538    ///    .await
539    ///    .with_image(Image::new("logo.jpg"))
540    ///    .send()
541    ///    .await?;
542    /// ```
543    pub async fn prepare_broadcast_with<M, F>(
544        &self,
545        msg: M,
546        f: F,
547    ) -> Result<
548        MulticastBuilder<'_, impl 'static + Send + Iterator<Item = ChatId>, C, M::Kind>,
549        C::Error,
550    >
551    where
552        F: 'static + Send + FnMut(&ChatId) -> bool,
553        M: MessageLike,
554    {
555        let ids = self.chat_ids_with(f).await?;
556        let (msg, kind) = msg.into_builder_parts();
557
558        Ok(MulticastBuilder {
559            client: self.client(),
560            chat_ids: ids,
561            ttl: None,
562            msg,
563            kind,
564        })
565    }
566
567    pub async fn update_msg<CID: Into<ChatId>, MID: Into<MessageId>>(
568        &self,
569        chat_id: CID,
570        message_id: MID,
571        new_content: MsgContent,
572    ) -> Result<ApiUpdateChatItemResponse, C::Error> {
573        self.client
574            .update_message(chat_id, message_id, new_content)
575            .await
576    }
577
578    pub async fn delete_msg<CID: Into<ChatId>, MID: Into<MessageId>>(
579        &self,
580        chat_id: CID,
581        message_id: MID,
582        mode: CIDeleteMode,
583    ) -> Result<Arc<ChatItemsDeletedResponse>, C::Error> {
584        self.client.delete_message(chat_id, message_id, mode).await
585    }
586
587    pub async fn batch_delete_msgs<CID: Into<ChatId>, I: IntoIterator<Item = MessageId>>(
588        &self,
589        chat_id: CID,
590        message_ids: I,
591        mode: CIDeleteMode,
592    ) -> Result<Arc<ChatItemsDeletedResponse>, C::Error> {
593        self.client
594            .batch_delete_messages(chat_id, message_ids, mode)
595            .await
596    }
597
598    /// Applies multiple reactions to a message. Returns one result per reaction.
599    pub async fn batch_msg_reactions<
600        CID: Into<ChatId>,
601        MID: Into<MessageId>,
602        I: IntoIterator<Item = Reaction>,
603    >(
604        &self,
605        chat_id: CID,
606        message_id: MID,
607        reactions: I,
608    ) -> Vec<Result<Arc<ChatItemReactionResponse>, C::Error>> {
609        self.client
610            .batch_message_reactions(chat_id, message_id, reactions)
611            .await
612    }
613
614    pub async fn update_msg_reaction<CID: Into<ChatId>, MID: Into<MessageId>>(
615        &self,
616        chat_id: CID,
617        message_id: MID,
618        reaction: Reaction,
619    ) -> Vec<Result<Arc<ChatItemReactionResponse>, C::Error>> {
620        self.client
621            .update_message_reaction(chat_id, message_id, reaction)
622            .await
623    }
624
625    pub fn accept_file<FID: Into<FileId>>(&self, file_id: FID) -> AcceptFileBuilder<'_, C> {
626        self.client.accept_file(file_id)
627    }
628
629    pub async fn reject_file<FID: Into<FileId>>(
630        &self,
631        file_id: FID,
632    ) -> Result<CancelFileResponse, C::Error> {
633        self.client.reject_file(file_id).await
634    }
635
636    /// [ChatId] can be created from various types. See [ChatId] docs for the full list of `From`
637    /// impls.
638    pub async fn delete_chat<CID: Into<ChatId>>(
639        &self,
640        chat_id: CID,
641        mode: DeleteMode,
642    ) -> Result<ApiDeleteChatResponse, C::Error> {
643        self.client.delete_chat(chat_id, mode).await
644    }
645
646    /// Create a new group. The bot's user becomes the owner.
647    pub async fn create_group(
648        &self,
649        profile: GroupProfile,
650    ) -> Result<Arc<GroupCreatedResponse>, C::Error> {
651        self.client
652            .api_new_group(ApiNewGroup::new(self.user_id, profile))
653            .await
654    }
655
656    /// Create a new public group with relay members. The bot's user becomes the owner.
657    /// Relay IDs can be obtained from [`Bot::get_group_relays`]
658    pub async fn create_public_group<I: IntoIterator<Item = RelayId>>(
659        &self,
660        relay_ids: I,
661        profile: GroupProfile,
662    ) -> Result<ApiNewPublicGroupResponse, C::Error> {
663        self.client
664            .api_new_public_group(ApiNewPublicGroup::new(
665                self.user_id,
666                relay_ids.into_iter().map(|id| id.0).collect(),
667                profile,
668            ))
669            .await
670    }
671
672    /// Enable or disable automatically accepting contacts from group members.
673    pub async fn set_auto_accept_member_contacts(
674        &self,
675        on: bool,
676    ) -> Result<Arc<CmdOkResponse>, C::Error> {
677        self.client
678            .api_set_user_auto_accept_member_contacts(ApiSetUserAutoAcceptMemberContacts {
679                user_id: self.user_id,
680                on_off: on,
681            })
682            .await
683    }
684
685    /// Sends a group invitation to a contact.
686    pub async fn add_member<GID: Into<GroupId>, CID: Into<ContactId>>(
687        &self,
688        group_id: GID,
689        contact_id: CID,
690        role: GroupMemberRole,
691    ) -> Result<Arc<SentGroupInvitationResponse>, C::Error> {
692        self.client.add_member(group_id, contact_id, role).await
693    }
694
695    /// Accepts a pending group invitation.
696    pub async fn join_group<GID: Into<GroupId>>(
697        &self,
698        group_id: GID,
699    ) -> Result<Arc<UserAcceptedGroupSentResponse>, C::Error> {
700        self.client.join_group(group_id).await
701    }
702
703    /// Confirms a pending group membership request.
704    pub async fn accept_member<GID: Into<GroupId>, MID: Into<MemberId>>(
705        &self,
706        group_id: GID,
707        member_id: MID,
708        role: GroupMemberRole,
709    ) -> Result<Arc<MemberAcceptedResponse>, C::Error> {
710        self.client.accept_member(group_id, member_id, role).await
711    }
712
713    pub async fn set_members_role<GID: Into<GroupId>, I: IntoIterator<Item = MemberId>>(
714        &self,
715        group_id: GID,
716        member_ids: I,
717        role: GroupMemberRole,
718    ) -> Result<Arc<MembersRoleUserResponse>, C::Error> {
719        self.client
720            .set_members_role(group_id, member_ids, role)
721            .await
722    }
723
724    pub async fn set_member_role<GID: Into<GroupId>, MID: Into<MemberId>>(
725        &self,
726        group_id: GID,
727        member_id: MID,
728        role: GroupMemberRole,
729    ) -> Result<Arc<MembersRoleUserResponse>, C::Error> {
730        self.client.set_member_role(group_id, member_id, role).await
731    }
732
733    /// Blocks members so their messages are hidden for everyone in the group.
734    pub async fn block_members_for_all<GID: Into<GroupId>, I: IntoIterator<Item = MemberId>>(
735        &self,
736        group_id: GID,
737        member_ids: I,
738    ) -> Result<Arc<MembersBlockedForAllUserResponse>, C::Error> {
739        self.client
740            .block_members_for_all(group_id, member_ids)
741            .await
742    }
743
744    /// Reverses a previous [`block_members_for_all`](Self::block_members_for_all).
745    pub async fn unblock_members_for_all<GID: Into<GroupId>, I: IntoIterator<Item = MemberId>>(
746        &self,
747        group_id: GID,
748        member_ids: I,
749    ) -> Result<Arc<MembersBlockedForAllUserResponse>, C::Error> {
750        self.client
751            .unblock_members_for_all(group_id, member_ids)
752            .await
753    }
754
755    /// Blocks a member so their messages are hidden for everyone in the group.
756    pub async fn block_member_for_all<GID: Into<GroupId>, MID: Into<MemberId>>(
757        &self,
758        group_id: GID,
759        member_id: MID,
760    ) -> Result<Arc<MembersBlockedForAllUserResponse>, C::Error> {
761        self.client.block_member_for_all(group_id, member_id).await
762    }
763
764    /// Reverses a previous [`block_member_for_all`](Self::block_member_for_all).
765    pub async fn unblock_member_for_all<GID: Into<GroupId>, MID: Into<MemberId>>(
766        &self,
767        group_id: GID,
768        member_id: MID,
769    ) -> Result<Arc<MembersBlockedForAllUserResponse>, C::Error> {
770        self.client
771            .unblock_member_for_all(group_id, member_id)
772            .await
773    }
774
775    /// Removes members from the group, preserving their past messages.
776    pub async fn remove_members<GID: Into<GroupId>, I: IntoIterator<Item = MemberId>>(
777        &self,
778        group_id: GID,
779        member_ids: I,
780    ) -> Result<Arc<UserDeletedMembersResponse>, C::Error> {
781        self.client.remove_members(group_id, member_ids).await
782    }
783
784    /// Removes members from the group and deletes their messages.
785    pub async fn remove_members_with_messages<
786        GID: Into<GroupId>,
787        I: IntoIterator<Item = MemberId>,
788    >(
789        &self,
790        group_id: GID,
791        member_ids: I,
792    ) -> Result<Arc<UserDeletedMembersResponse>, C::Error> {
793        self.client
794            .remove_members_with_messages(group_id, member_ids)
795            .await
796    }
797
798    /// Removes a member from the group, preserving their past messages.
799    pub async fn remove_member<GID: Into<GroupId>, MID: Into<MemberId>>(
800        &self,
801        group_id: GID,
802        member_id: MID,
803    ) -> Result<Arc<UserDeletedMembersResponse>, C::Error> {
804        self.client.remove_member(group_id, member_id).await
805    }
806
807    /// Removes a member from the group and deletes their messages.
808    pub async fn remove_member_with_messages<GID: Into<GroupId>, MID: Into<MemberId>>(
809        &self,
810        group_id: GID,
811        member_id: MID,
812    ) -> Result<Arc<UserDeletedMembersResponse>, C::Error> {
813        self.client
814            .remove_member_with_messages(group_id, member_id)
815            .await
816    }
817
818    pub async fn leave_group<GID: Into<GroupId>>(
819        &self,
820        group_id: GID,
821    ) -> Result<Arc<LeftMemberUserResponse>, C::Error> {
822        self.client.leave_group(group_id).await
823    }
824
825    pub async fn list_members<GID: Into<GroupId>>(
826        &self,
827        group_id: GID,
828    ) -> Result<Vec<GroupMember>, C::Error> {
829        self.client.list_members(group_id).await
830    }
831
832    /// Deletes messages for all group members. Requires admin or owner role.
833    pub async fn moderate_messages<GID: Into<GroupId>, I: IntoIterator<Item = MessageId>>(
834        &self,
835        group_id: GID,
836        message_ids: I,
837    ) -> Result<Arc<ChatItemsDeletedResponse>, C::Error> {
838        self.client.moderate_messages(group_id, message_ids).await
839    }
840
841    /// Deletes a message for all group members. Requires admin or owner role.
842    pub async fn moderate_message<GID: Into<GroupId>, MID: Into<MessageId>>(
843        &self,
844        group_id: GID,
845        message_id: MID,
846    ) -> Result<Arc<ChatItemsDeletedResponse>, C::Error> {
847        self.client.moderate_message(group_id, message_id).await
848    }
849
850    pub async fn update_group_profile<GID: Into<GroupId>>(
851        &self,
852        group_id: GID,
853        profile: GroupProfile,
854    ) -> Result<Arc<GroupUpdatedResponse>, C::Error> {
855        self.client.update_group_profile(group_id, profile).await
856    }
857
858    /// Stores arbitrary app-defined JSON on the group. Pass `None` to clear it.
859    pub async fn set_group_custom_data<GID: Into<GroupId>>(
860        &self,
861        group_id: GID,
862        data: Option<JsonObject>,
863    ) -> Result<Arc<CmdOkResponse>, C::Error> {
864        self.client.set_group_custom_data(group_id, data).await
865    }
866
867    /// Stores arbitrary app-defined JSON on the contact. Pass `None` to clear it.
868    pub async fn set_contact_custom_data<CID: Into<ContactId>>(
869        &self,
870        contact_id: CID,
871        data: Option<JsonObject>,
872    ) -> Result<Arc<CmdOkResponse>, C::Error> {
873        self.client.set_contact_custom_data(contact_id, data).await
874    }
875
876    pub async fn create_group_link<GID: Into<GroupId>>(
877        &self,
878        group_id: GID,
879        role: GroupMemberRole,
880    ) -> Result<Arc<GroupLinkCreatedResponse>, C::Error> {
881        self.client.create_group_link(group_id, role).await
882    }
883
884    /// Changes the default role assigned to members who join via the group link.
885    pub async fn set_group_link_role<GID: Into<GroupId>>(
886        &self,
887        group_id: GID,
888        role: GroupMemberRole,
889    ) -> GroupLinkResult<C> {
890        self.client.set_group_link_role(group_id, role).await
891    }
892
893    pub async fn delete_group_link<GID: Into<GroupId>>(
894        &self,
895        group_id: GID,
896    ) -> Result<Arc<GroupLinkDeletedResponse>, C::Error> {
897        self.client.delete_group_link(group_id).await
898    }
899
900    pub async fn get_group_link<GID: Into<GroupId>>(&self, group_id: GID) -> GroupLinkResult<C> {
901        self.client.get_group_link(group_id).await
902    }
903
904    pub async fn get_group_relays<GID: Into<GroupId>>(
905        &self,
906        group_id: GID,
907    ) -> GetGroupRelaysResponse<C> {
908        self.client.get_group_relays(group_id).await
909    }
910
911    pub async fn add_group_relays<GID: Into<GroupId>, I: IntoIterator<Item = RelayId>>(
912        &self,
913        group_id: GID,
914        relay_ids: I,
915    ) -> AddGroupRelaysResponse<C> {
916        self.client.add_group_relays(group_id, relay_ids).await
917    }
918
919    pub async fn add_group_relay<GID: Into<GroupId>, RID: Into<RelayId>>(
920        &self,
921        group_id: GID,
922        relay_id: RID,
923    ) -> AddGroupRelaysResponse<C> {
924        self.client.add_group_relay(group_id, relay_id).await
925    }
926
927    /// Get chats with time-based pagination. Prefer this over [`Bot::contacts`] / [`Bot::groups`]
928    /// for large databases as it avoids loading all records into memory at once.
929    pub async fn get_chats(
930        &self,
931        pagination: PaginationByTime,
932        query: ChatListQuery,
933    ) -> Result<Arc<ApiChatsResponse>, C::Error> {
934        self.client
935            .api_get_chats(ApiGetChats::new(self.user_id, pagination, query))
936            .await
937    }
938}
939
940#[cfg(feature = "xftp")]
941impl<C: crate::xftp::XftpExt> Bot<C> {
942    pub fn download_file<FID: Into<FileId>>(
943        &self,
944        file_id: FID,
945    ) -> crate::xftp::DownloadFileBuilder<'_, C> {
946        self.client.download_file(file_id)
947    }
948}
949
950#[cfg(feature = "websocket")]
951impl crate::ws::Bot {
952    pub fn shutdown(self) -> impl Future<Output = ()> {
953        self.client.disconnect()
954    }
955}
956
957#[cfg(feature = "ffi")]
958impl crate::ffi::Bot {
959    pub fn shutdown(self) -> impl Future<Output = ()> {
960        self.client.disconnect()
961    }
962}
963
964/// Passed to [`Bot::init`] to configure bot identity and startup behaviour.
965#[derive(Debug, Clone)]
966pub struct BotSettings {
967    pub display_name: String,
968    /// If string is empty creates an auto-accepting address without a message. If string is not
969    /// empty adds a welcome message to the address
970    pub auto_accept: Option<String>,
971    pub profile_settings: Option<BotProfileSettings>,
972    pub avatar: Option<ImagePreview>,
973}
974
975impl BotSettings {
976    pub fn new(name: impl Into<String>) -> Self {
977        Self {
978            display_name: name.into(),
979            auto_accept: None,
980            profile_settings: None,
981            avatar: None,
982        }
983    }
984
985    pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
986        self.avatar = Some(avatar);
987        self
988    }
989
990    /// Create a public auto-accepting address during the intialisation
991    pub fn auto_accept(mut self) -> Self {
992        self.auto_accept = Some(String::default());
993        self
994    }
995
996    /// Create a public auto-accepting address with a welcome meesage during the intialisation
997    pub fn auto_accept_with(mut self, welcome_message: impl Into<String>) -> Self {
998        self.auto_accept = Some(welcome_message.into());
999        self
1000    }
1001
1002    pub fn with_profile_settings(mut self, settings: BotProfileSettings) -> Self {
1003        self.profile_settings = Some(settings);
1004        self
1005    }
1006}
1007
1008#[derive(Debug, Clone)]
1009pub enum BotProfileSettings {
1010    /// Apply only the given preferences; leave all other profile fields unchanged.
1011    Preferences(Preferences),
1012    /// Replace the entire profile.
1013    FullProfile(Profile),
1014}
1015
1016fn extract_address(link: &CreatedConnLink) -> String {
1017    link.conn_short_link
1018        .clone()
1019        .unwrap_or_else(|| link.conn_full_link.clone())
1020}
1021
1022fn extract_profile(local: &mut LocalProfile) -> Profile {
1023    Profile {
1024        display_name: std::mem::take(&mut local.display_name),
1025        full_name: std::mem::take(&mut local.full_name),
1026        short_descr: local.short_descr.take(),
1027        image: local.image.take(),
1028        contact_link: local.contact_link.take(),
1029        preferences: local.preferences.take(),
1030        peer_type: local.peer_type.take(),
1031        undocumented: std::mem::take(&mut local.undocumented),
1032    }
1033}
1034
1035fn extract_preferences(preferences: &mut Option<Preferences>) -> Preferences {
1036    match preferences.as_mut() {
1037        Some(prefs) => Preferences {
1038            timed_messages: prefs.timed_messages.take(),
1039            full_delete: prefs.full_delete.take(),
1040            reactions: prefs.reactions.take(),
1041            voice: prefs.voice.take(),
1042            files: prefs.files.take(),
1043            calls: prefs.calls.take(),
1044            sessions: prefs.sessions.take(),
1045            commands: prefs.commands.take(),
1046            undocumented: std::mem::take(&mut prefs.undocumented),
1047        },
1048        None => Preferences {
1049            timed_messages: None,
1050            full_delete: None,
1051            reactions: None,
1052            voice: None,
1053            files: None,
1054            calls: None,
1055            sessions: None,
1056            commands: None,
1057            undocumented: Default::default(),
1058        },
1059    }
1060}