Skip to main content

simploxide_client/bot/farm/
mod.rs

1//! Bot farm managing multiple bots on the same SimpleX instance
2
3use serde::Deserialize;
4use simploxide_api_types::{
5    NewUser, User,
6    client_api::ClientApi,
7    commands::{ApiDeleteUser, ApiSetActiveUser, CancelFile, ListUsers, ReceiveFile},
8    responses::{CancelFileResponse, ListUsersResponse, ReceiveFileResponse, UsersListResponse},
9};
10use tokio::sync::{
11    mpsc::{self, UnboundedReceiver, UnboundedSender},
12    oneshot,
13};
14
15use std::{
16    collections::{HashMap, hash_map::Entry},
17    sync::Arc,
18};
19
20use crate::{EventParser, EventStream, bot::BotSettings, ext::ClientApiExt as _, id::UserId};
21
22mod demux;
23mod mux;
24
25use demux::{BotMap, Channel};
26
27use super::Bot;
28
29#[cfg(feature = "xftp")]
30pub type FarmBot<C> = Bot<crate::xftp::XftpClient<DelegateClient<C>>>;
31
32#[cfg(not(feature = "xftp"))]
33pub type FarmBot<C> = Bot<DelegateClient<C>>;
34
35pub type InitFarm<C, P> = BotFarm<Init<C, P>>;
36
37pub type RunningFarm<C, P> = BotFarm<Running<C, P>>;
38
39#[derive(Clone)]
40pub struct BotFarm<S> {
41    state: S,
42}
43
44impl<C: ClientApi, P: EventParser> BotFarm<Init<C, P>> {
45    /// The `farm_name` is the name of the special bot managing the bot farm, it cannot be accessed
46    /// directly. It is mostly used as an intermediary user deleting other users under the hood.
47    pub async fn init(
48        farm_name: String,
49        client: C,
50        events: EventStream<P>,
51    ) -> Result<Self, C::Error> {
52        let mut farm_id = BotId::anybot();
53        let mut active_name = String::new();
54        let mut cache = HashMap::new();
55        let bots = demux::FxDashMap::with_hasher(rustc_hash::FxBuildHasher);
56
57        let resp = client.list_users().await?;
58
59        for info in &resp.users {
60            let bot_id: BotId = UserId::from(info).into();
61
62            if info.user.active_user {
63                active_name = info.user.profile.display_name.clone();
64            }
65
66            if info.user.profile.display_name == farm_name {
67                farm_id = bot_id;
68                continue;
69            }
70
71            bots.insert(bot_id, Channel::Ghost);
72            cache.insert(info.user.profile.display_name.clone(), info.user.clone());
73        }
74
75        let farm_id = match farm_id.get() {
76            Some(user_id) => user_id,
77            None => {
78                let resp = client
79                    .create_active_user(NewUser {
80                        profile: Some(Bot::<C>::default_profile(farm_name.clone())),
81                        past_timestamp: false,
82                        user_chat_relay: false,
83                        undocumented: Default::default(),
84                    })
85                    .await?;
86
87                active_name = farm_name.clone();
88                UserId::from(&resp.user)
89            }
90        };
91
92        let state = Init {
93            client,
94            events,
95            farm_id,
96            farm_name,
97            active_name,
98            bots,
99            cache,
100        };
101
102        Ok(Self { state })
103    }
104
105    /// Total users count on the farm excluding the farm user
106    pub fn users_count(&self) -> usize {
107        self.state.cache.len()
108    }
109
110    /// Iterate over all users excluding the farm user
111    pub fn users(&self) -> impl Iterator<Item = &User> {
112        self.state.cache.values()
113    }
114
115    pub fn user(&self, name: &str) -> Option<&User> {
116        self.state.cache.get(name)
117    }
118
119    pub async fn remove(&mut self, user_id: UserId) -> Result<(), C::Error> {
120        self.state
121            .client
122            .api_set_active_user(ApiSetActiveUser::new(self.state.farm_id.raw()))
123            .await?;
124
125        self.state.active_name = self.state.farm_name.clone();
126
127        let resp = self
128            .state
129            .client
130            .api_delete_user(ApiDeleteUser {
131                user_id: user_id.raw(),
132                del_smp_queues: true,
133                view_pwd: None,
134            })
135            .await?;
136
137        let user = resp.user.as_ref().unwrap();
138
139        self.state.bots.remove(&user_id.into());
140        self.state.cache.remove(&user.profile.display_name);
141
142        Ok(())
143    }
144
145    pub async fn remove_by_name(&mut self, name: &str) -> Result<(), C::Error> {
146        let Some(user) = self.state.cache.remove(name) else {
147            return Ok(());
148        };
149
150        let result = self.remove(UserId::from(&user)).await;
151
152        if result.is_err() {
153            self.state.cache.insert(name.to_owned(), user);
154        }
155
156        result
157    }
158
159    /// Prepare a user with its own event stream. Use `take_bot` to extract the bot then.
160    pub async fn prepare_bot(
161        &mut self,
162        settings: BotSettings,
163    ) -> Result<UserId, CreateError<C::Error>>
164    where
165        C: Clone,
166    {
167        let user_id = self.prepare_inner(settings).await?;
168        self.state.bots.insert(user_id.into(), Channel::new_bot());
169
170        Ok(user_id)
171    }
172
173    /// Prepare a ghost user. Ghosts don't have their own event streams, all their events end up in
174    /// the general bot farm stream.
175    pub async fn prepare_ghost(
176        &mut self,
177        settings: BotSettings,
178    ) -> Result<UserId, CreateError<C::Error>>
179    where
180        C: Clone,
181    {
182        let user_id = self.prepare_inner(settings).await?;
183        self.state.bots.insert(user_id.into(), Channel::Ghost);
184
185        Ok(user_id)
186    }
187
188    /// Transition the farm to the running state by starting dispatching and routing events.
189    ///
190    /// Returns a running farm and a general [`EventStream`] that receives:
191    /// - events belonging to ghost users
192    /// - general events not addressed to a specific user(events without [`User`] struct)
193    ///
194    /// Farm user events are filtered out
195    ///
196    /// Handle events or [discard](EventStream::discard) the returned event stream to avoid memory leaks.
197    pub fn run(self) -> (BotFarm<Running<C, P>>, EventStream<P>)
198    where
199        C: 'static + Send,
200        C::Error: Send,
201        P: 'static + Send,
202    {
203        let (delegate_client, rx) = DelegateClient::new(self.state.farm_id.into());
204        mux::start(self.state.client, rx);
205
206        let bots = Arc::new(self.state.bots);
207        let (suspender, mut unmuxed_events) = demux::start(bots.clone(), self.state.events);
208
209        unmuxed_events.exclude_user(self.state.farm_id);
210
211        #[cfg(feature = "xftp")]
212        let (xftp_client, unmuxed_events) = unmuxed_events.hook_xftp(delegate_client.clone());
213
214        let state = Running {
215            farm_name: self.state.farm_name,
216            client: delegate_client,
217            suspender,
218            bots,
219            #[cfg(feature = "xftp")]
220            xftp: xftp_client.manager(),
221        };
222
223        (BotFarm { state }, unmuxed_events)
224    }
225
226    async fn prepare_inner(
227        &mut self,
228        settings: BotSettings,
229    ) -> Result<UserId, CreateError<C::Error>>
230    where
231        C: Clone,
232    {
233        if settings.display_name == self.state.farm_name {
234            return Err(CreateError::FarmUser);
235        }
236
237        match self.state.cache.entry(settings.display_name.clone()) {
238            Entry::Occupied(mut occupied) => {
239                let bot = Bot::<C>::init_existing(
240                    self.state.client.clone(),
241                    occupied.get_mut(),
242                    settings,
243                )
244                .await?;
245                let update = bot.info().await?;
246
247                *occupied.get_mut() = update.user.clone();
248                self.change_active_user(update.user.profile.display_name.clone());
249
250                Ok(bot.user_id())
251            }
252            Entry::Vacant(vacant) => {
253                let bot = Bot::<C>::init_new(self.state.client.clone(), settings).await?;
254                let update = bot.info().await?;
255
256                vacant.insert(update.user.clone());
257
258                self.change_active_user(update.user.profile.display_name.clone());
259                Ok(bot.user_id())
260            }
261        }
262    }
263
264    fn change_active_user(&mut self, new_active_username: String) {
265        if new_active_username == self.state.active_name {
266            return;
267        }
268
269        if let Some(user) = self.state.cache.get_mut(&self.state.active_name) {
270            user.active_user = false;
271        }
272
273        self.state.active_name = new_active_username;
274    }
275}
276
277impl<C: 'static + ClientApi, P: EventParser> BotFarm<Running<C, P>>
278where
279    C::Error: Send,
280{
281    /// Return a ghost handle for `user_id`, or `None` if the user does not exist or is a bot.
282    ///
283    /// Each call produces a new independent [`FarmBot`] handle. Multiple handles for the same
284    /// ghost share the underlying command channel and with the `xftp` feature enabled the same
285    /// download table, so concurrent `download_file` calls on different handles will work
286    /// correctly.
287    pub fn ghost(&self, user_id: UserId) -> Option<FarmBot<C>> {
288        let chan = self.state.bots.get(&user_id.into())?;
289
290        if let Channel::Ghost = chan.value() {
291            Some(self.make_ghost(user_id))
292        } else {
293            None
294        }
295    }
296
297    /// Take the bot handle and its [`EventStream`] out of the farm.
298    ///
299    /// This is a one-shot operation: the internal event receiver is consumed and cannot be taken
300    /// again. Panics if `user_id` is unknown, was registered as a ghost, or was already taken.
301    /// Use [`take_bot_checked`](Self::take_bot_checked) to avoid the panic.
302    pub fn take_bot(&self, user_id: UserId) -> (FarmBot<C>, EventStream<P>) {
303        let mut chan = self.state.bots.get_mut(&user_id.into()).unwrap();
304
305        if chan.is_ghost() {
306            panic!("The {user_id:?} was not initialized as bot");
307        }
308
309        let receiver = chan
310            .take_receiver()
311            .unwrap_or_else(|| panic!("The {user_id:?} was already taken"));
312
313        self.make_bot(user_id, receiver)
314    }
315
316    /// Non-panicking variant of [`take_bot`](Self::take_bot). Returns `None` if the user is
317    /// unknown, is a ghost, or was already taken.
318    pub fn take_bot_checked(&self, user_id: UserId) -> Option<(FarmBot<C>, EventStream<P>)> {
319        self.state
320            .bots
321            .get_mut(&user_id.into())
322            .and_then(|mut chan| chan.take_receiver())
323            .map(|receiver| self.make_bot(user_id, receiver))
324    }
325
326    /// Create a new SimpleX user, as a bot, and return its handle and event stream.
327    ///
328    /// Unlike `prepare_bot`, this is available at runtime after [`run`](crate::bot::BotFarm::run).
329    /// In order to route events correctly **all** event streams are paused and don't receive any
330    /// events during the bot creation process.
331    pub async fn create_bot(
332        &self,
333        settings: BotSettings,
334    ) -> Result<(FarmBot<C>, EventStream<P>), CreateError<C::Error>> {
335        let user_id = self.create_inner(settings, true).await?;
336        let (bot, stream) = self.take_bot(user_id);
337        Ok((bot, stream))
338    }
339
340    /// Return the existing bot if a user with the given display name is already known, otherwise
341    /// create one via [`create_bot`](Self::create_bot).
342    ///
343    /// # Eventual consistency
344    ///
345    /// This method is eventually consistent and may return [CreateError::Desync] if the same bot
346    /// is getting created/deleted from multiple threads. You're supposed to retry this call to get
347    /// the actual result on [CreateError::Desync]
348    pub async fn get_or_create_bot(
349        &self,
350        settings: BotSettings,
351    ) -> Result<(FarmBot<C>, EventStream<P>), CreateError<C::Error>> {
352        let resp = self.state.client.list_users().await?;
353
354        match resp.users.iter().find_map(|info| {
355            (info.user.profile.display_name == settings.display_name).then_some(UserId::from(info))
356        }) {
357            Some(user_id) => match self.state.bots.get_mut(&user_id.into()) {
358                Some(mut entry) => match entry.value_mut() {
359                    Channel::Bot(pipe) => {
360                        let receiver = pipe.take_receiver().ok_or(CreateError::BotAlreadyTaken)?;
361                        Ok(self.make_bot(user_id, receiver))
362                    }
363                    Channel::Ghost => Err(CreateError::BotIsGhost),
364                },
365                None => Err(CreateError::Desync),
366            },
367            None => self.create_bot(settings).await,
368        }
369    }
370
371    /// Create a new SimpleX user, register it as a ghost, and return a handle to it.
372    ///
373    /// The ghost's events are routed to the general [`EventStream`] returned when running a farm.
374    pub async fn create_ghost(
375        &self,
376        settings: BotSettings,
377    ) -> Result<FarmBot<C>, CreateError<C::Error>> {
378        let user_id = self.create_inner(settings, false).await?;
379        Ok(self.ghost(user_id).unwrap())
380    }
381
382    /// Return a ghost handle for the named user if it already exists, otherwise create one via
383    /// [`create_ghost`](Self::create_ghost).
384    ///
385    /// Same eventual consistency caveats as [`get_or_create_bot`](Self::get_or_create_bot).
386    pub async fn get_or_create_ghost(
387        &self,
388        settings: BotSettings,
389    ) -> Result<FarmBot<C>, CreateError<C::Error>> {
390        let resp = self.state.client.list_users().await?;
391
392        match resp.users.iter().find_map(|info| {
393            (info.user.profile.display_name == settings.display_name)
394                .then_some(UserId::from(&info.user))
395        }) {
396            Some(user_id) => match self.state.bots.get(&user_id.into()) {
397                Some(entry) => match entry.value() {
398                    Channel::Bot(_) => Err(CreateError::GhostIsBot),
399                    Channel::Ghost => Ok(self.make_ghost(user_id)),
400                },
401                None => Err(CreateError::Desync),
402            },
403            None => self.create_ghost(settings).await,
404        }
405    }
406
407    /// Permanently delete a user and remove it from the routing table.
408    ///
409    /// Any [`FarmBot`] handles that were already taken for this user remain alive but all
410    /// subsequent commands on them will fail with an API error.
411    pub async fn delete(&self, user_id: UserId) -> Result<(), C::Error> {
412        self.state
413            .client
414            .api_delete_user(ApiDeleteUser {
415                user_id: user_id.raw(),
416                del_smp_queues: true,
417                view_pwd: None,
418            })
419            .await?;
420
421        self.state.bots.remove(&user_id.into());
422        Ok(())
423    }
424
425    async fn create_inner(
426        &self,
427        settings: BotSettings,
428        is_bot: bool,
429    ) -> Result<UserId, CreateError<C::Error>> {
430        if settings.display_name == self.state.farm_name {
431            return Err(CreateError::FarmUser);
432        }
433
434        let (_guard, suspension) = oneshot::channel();
435        let _ = self.state.suspender.send(suspension);
436
437        let mut resp = self
438            .state
439            .client
440            .new_user(NewUser {
441                profile: Some(Bot::<C>::default_profile(&settings.display_name)),
442                past_timestamp: false,
443                user_chat_relay: false,
444                undocumented: Default::default(),
445            })
446            .await?;
447
448        if is_bot {
449            self.state
450                .bots
451                .insert(UserId::from(&resp.user).into(), Channel::new_bot());
452        } else {
453            self.state
454                .bots
455                .insert(UserId::from(&resp.user).into(), Channel::Ghost);
456        }
457
458        let resp = Arc::get_mut(&mut resp).unwrap();
459        let client = self.state.client.delegate_to(UserId::from(&resp.user));
460
461        match Bot::init_existing(client, &mut resp.user, settings).await {
462            Ok(bot) => Ok(bot.user_id()),
463            Err(e) => {
464                if let Err(err) = self.delete(UserId::from(&resp.user)).await {
465                    log::warn!("Failed to delete incorrectly initialized bot: {err}")
466                }
467                Err(e.into())
468            }
469        }
470    }
471
472    fn make_bot(
473        &self,
474        user_id: UserId,
475        receiver: UnboundedReceiver<P>,
476    ) -> (FarmBot<C>, EventStream<P>) {
477        let bot_client = self.state.client.delegate_to(user_id);
478        let stream = EventStream::from(receiver);
479
480        #[cfg(feature = "xftp")]
481        let (bot_client, stream) = stream.hook_xftp(bot_client);
482
483        (Bot::new(bot_client, user_id), stream)
484    }
485
486    fn make_ghost(&self, user_id: UserId) -> FarmBot<C> {
487        let bot_client = self.state.client.delegate_to(user_id);
488
489        #[cfg(feature = "xftp")]
490        let bot_client = crate::xftp::XftpClient::new(bot_client, self.state.xftp.clone());
491
492        Bot::new(bot_client, user_id)
493    }
494}
495
496pub struct Init<C, P> {
497    client: C,
498    events: EventStream<P>,
499    farm_id: UserId,
500    farm_name: String,
501    active_name: String,
502    bots: BotMap<P>,
503    cache: HashMap<String, User>,
504}
505
506#[derive(Clone)]
507pub struct Running<C: ClientApi, P> {
508    farm_name: String,
509    client: DelegateClient<C>,
510    suspender: demux::Suspender,
511    bots: Arc<BotMap<P>>,
512    #[cfg(feature = "xftp")]
513    xftp: Arc<crate::xftp::XftpManager>,
514}
515
516pub struct DelegateClient<C: ClientApi> {
517    bot_id: BotId,
518    sender: DelegateSender<C>,
519}
520
521impl<C: ClientApi> Clone for DelegateClient<C> {
522    fn clone(&self) -> Self {
523        Self {
524            bot_id: self.bot_id,
525            sender: self.sender.clone(),
526        }
527    }
528}
529
530impl<C: ClientApi> DelegateClient<C> {
531    fn new(bot_id: BotId) -> (Self, DelegateReceiver<C>) {
532        let (sender, receiver) = mpsc::unbounded_channel();
533        (Self { bot_id, sender }, receiver)
534    }
535
536    fn delegate_to(&self, bot_id: impl Into<BotId>) -> Self {
537        Self {
538            bot_id: bot_id.into(),
539            sender: self.sender.clone(),
540        }
541    }
542}
543
544impl<C: ClientApi> ClientApi for DelegateClient<C>
545where
546    C::Error: Send,
547{
548    type ResponseShape<'de, T: 'de + Deserialize<'de>> = C::ResponseShape<'de, T>;
549    type Error = C::Error;
550
551    async fn send_raw(&self, cmd: String) -> Result<String, Self::Error> {
552        let (responder, response) = oneshot::channel();
553
554        let request = DelegateRequest {
555            bot_id: self.bot_id,
556            cmd,
557            responder,
558        };
559
560        self.sender
561            .send(request)
562            .expect("Delegate client cannot outlive background task");
563
564        response
565            .await
566            .expect("Delegate client cannot outlive background task")
567    }
568
569    async fn list_users(&self) -> Result<Arc<UsersListResponse>, Self::Error> {
570        let client = self.delegate_to(BotId::anybot());
571        let response: ListUsersResponse = client.send(ListUsers {}).await?;
572        Ok(response.into_inner())
573    }
574
575    async fn receive_file(&self, cmd: ReceiveFile) -> Result<ReceiveFileResponse, Self::Error> {
576        let client = self.delegate_to(BotId::anybot());
577        client.send(cmd).await
578    }
579
580    async fn cancel_file(&self, file_id: i64) -> Result<CancelFileResponse, Self::Error> {
581        let client = self.delegate_to(BotId::anybot());
582        client.send(CancelFile { file_id }).await
583    }
584}
585
586#[derive(Debug)]
587pub enum CreateError<E> {
588    /// Farm user cannot be interacted with directly
589    FarmUser,
590    /// Bot cannot be created because ghost with same name already exists
591    BotIsGhost,
592    /// Ghost cannot be craeted because bot with same name already exists
593    GhostIsBot,
594    /// The bot already exists and was already taken with the `take_bot`
595    BotAlreadyTaken,
596    /// The in memory state is not synced with the DB, retry later
597    Desync,
598    Api(E),
599}
600
601impl<E> From<E> for CreateError<E> {
602    fn from(value: E) -> Self {
603        Self::Api(value)
604    }
605}
606
607impl<E> std::fmt::Display for CreateError<E>
608where
609    E: std::fmt::Display,
610{
611    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612        match self {
613            Self::FarmUser => write!(
614                f,
615                "Attempt to create a farm user. Farm user is special and cannot be interacted with directly"
616            ),
617            Self::BotIsGhost => write!(
618                f,
619                "Cannot create a bot because the ghost user with the same name already exists"
620            ),
621            Self::GhostIsBot => write!(
622                f,
623                "Cannot create a ghost because the bot user with the same name already exists"
624            ),
625            Self::BotAlreadyTaken => {
626                write!(
627                    f,
628                    "The bot already exists and has been taken from the farm. Cannot recreate operational bots"
629                )
630            }
631            Self::Desync => {
632                write!(
633                    f,
634                    "The DB state was not in sync with the memory state, try again"
635                )
636            }
637            Self::Api(e) => write!(f, "{e:#}"),
638        }
639    }
640}
641
642impl<E: 'static + std::error::Error> std::error::Error for CreateError<E> {
643    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
644        if let Self::Api(err) = self {
645            Some(err)
646        } else {
647            None
648        }
649    }
650}
651
652struct DelegateRequest<C: ClientApi> {
653    bot_id: BotId,
654    cmd: String,
655    responder: oneshot::Sender<Result<String, C::Error>>,
656}
657
658type DelegateSender<C> = UnboundedSender<DelegateRequest<C>>;
659type DelegateReceiver<C> = UnboundedReceiver<DelegateRequest<C>>;
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
662#[repr(transparent)]
663struct BotId(Option<UserId>);
664
665impl BotId {
666    /// Used as an optimization for commands that can execute from any active bot account
667    fn anybot() -> Self {
668        Self(None)
669    }
670
671    fn get(&self) -> Option<UserId> {
672        self.0
673    }
674}
675
676impl From<UserId> for BotId {
677    fn from(user_id: UserId) -> Self {
678        Self(Some(user_id))
679    }
680}