Skip to main content

simploxide_client/bot/
mod.rs

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