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