Skip to main content

simploxide_client/bot/
mod.rs

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