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