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