Skip to main content

stoat/
client.rs

1use std::{panic::AssertUnwindSafe, sync::Arc, time::Duration};
2
3use futures::FutureExt;
4use stoat_database::events::client::EventV1;
5use tokio::{
6    select,
7    sync::{Mutex, mpsc},
8};
9
10use crate::{
11    CacheConfig, Context, Error,
12    cache::GlobalCache,
13    context::Events,
14    events::{EventHandler, update_state},
15    http::HttpClient,
16    notifiers::Notifiers,
17    websocket::run,
18};
19
20/// # Stoat Client
21///
22/// Entrypoint for connecting to the Stoat API.
23///
24/// Create a client via [`Self::new`] or [`Self::new_with_api_url`] if you are connecting to a 3rd party instance.
25/// Clients require an implementation of [`EventHandler`] which contains all the event methods.
26///
27/// ## Example
28///
29/// While it may seem like setting up a client requires a lot, the boilerplate required here pays off in larger projects ensuring everything works smoothly together.
30/// ```
31/// #[derive(Debug, Clone)]
32/// pub enum Error {
33///     StoatError(stoat::Error),
34/// }
35///
36/// impl From<stoat::Error> for Error {
37///     fn from(value: stoat::Error) -> Self {
38///         Self::StoatError(value)
39///     }
40/// }
41///
42/// #[derive(Clone)]
43/// struct Events;
44///
45/// #[async_trait]
46/// impl EventHandler for Events {
47///     type Error = Error;
48///
49///     async fn ready(&self, _context: Context) -> Result<(), Self::Error> {
50///         println!("Ready!");
51///         Ok(())
52///     }
53/// }
54///
55/// #[tokio::main]
56/// async fn main() -> Result<(), Error> {
57///     Client::new(Events).await?.run("TOKEN HERE").await
58/// }
59/// ```
60#[derive(Clone)]
61pub struct Client<H> {
62    pub state: GlobalCache,
63    pub handler: Arc<H>,
64    pub http: HttpClient,
65    pub waiters: Notifiers,
66    pub events: Option<Events>,
67}
68
69impl<H: EventHandler + Clone + Send + Sync + 'static> Client<H> {
70    /// Constructs a client with the official instance.
71    ///
72    /// Use the `new_with_*` functions to customise the creation.
73    pub async fn new(handler: H) -> Result<Self, H::Error> {
74        Self::new_with_config(handler, CacheConfig::default()).await
75    }
76
77    /// Constructs a client with a custom cache config.
78    pub async fn new_with_config(handler: H, config: CacheConfig) -> Result<Self, H::Error> {
79        Self::new_with_api_url(handler, config, "https://api.stoat.chat").await
80    }
81
82    /// Constructs a client with a custom Stoat instance.
83    pub async fn new_with_api_url(
84        handler: H,
85        config: CacheConfig,
86        base_url: impl Into<String>,
87    ) -> Result<Self, H::Error> {
88        let http = HttpClient::new(base_url.into(), None, None).await?;
89
90        Ok(Self {
91            state: GlobalCache::new((*http.api_config).clone(), config),
92            handler: Arc::new(handler),
93            http,
94            waiters: Notifiers::default(),
95            events: None,
96        })
97    }
98
99    /// Connects to the api and sets the current user.
100    ///
101    /// You will usually not need to call this directly as [`Self::run`] handles this.
102    pub async fn start(&mut self, token: impl Into<String>) -> Result<(), H::Error> {
103        let token = token.into();
104
105        self.http.token = Some(token.clone());
106        self.http.user_id = Some(self.http.fetch_self().await?.id);
107
108        Ok(())
109    }
110
111    /// Connects and starts the bot, this connects to the websocket to receive events, this is the main entry point for starting the bot.
112    ///
113    /// Reconnects are handled automatically.
114    pub async fn run(&mut self, token: impl Into<String>) -> Result<(), H::Error> {
115        let token = token.into();
116
117        self.start(token.clone()).await?;
118
119        let (client_sender, client_receiver) = mpsc::unbounded_channel();
120        self.events = Some(Events(Arc::new(client_sender)));
121
122        let (sender, receiver) = mpsc::unbounded_channel();
123
124        let handle = {
125            let sender = sender.clone();
126            let state = self.state.clone();
127            let token = token.clone();
128            let client_receiver = Arc::new(Mutex::new(client_receiver));
129
130            async move {
131                loop {
132                    if let Err(e) = run(
133                        sender.clone(),
134                        client_receiver.clone(),
135                        state.clone(),
136                        token.clone(),
137                    )
138                    .await
139                    {
140                        log::error!("{e:?}");
141
142                        if let Error::Close = e {
143                            return Ok(());
144                        }
145                    }
146
147                    log::info!("Disconnected! Reconnecting in 10 seconds.");
148
149                    tokio::time::sleep(Duration::from_secs(10)).await;
150                }
151            }
152        };
153
154        let res = select! {
155            e = handle => e,
156            _ = tokio::signal::ctrl_c() => {
157                log::info!("Received ctrl+c. exiting.");
158                Ok(())
159            }
160            _ = self.handle_events(receiver) => {
161                Ok(())
162            }
163        };
164
165        self.cleanup().await;
166
167        res
168    }
169
170    /// Clears the internal cache.
171    pub async fn cleanup(&mut self) {
172        self.state.cleanup().await;
173        self.waiters.clear_all_waiters().await;
174        self.events = None;
175    }
176
177    async fn handle_events(&self, mut receiver: mpsc::UnboundedReceiver<EventV1>) {
178        while let Some(event) = receiver.recv().await {
179            let this = self.clone();
180
181            tokio::spawn(async move {
182                this.handle_event(event).await;
183            });
184        }
185    }
186
187    /// Handles a Stoat event with updating the local state, invoking notifiers and calling the event callbacks.
188    ///
189    /// You shouldn't need to call this yourself unless your mocking events or debugging.
190    pub async fn handle_event(&self, event: EventV1) {
191        let wrapper = AssertUnwindSafe(async {
192            let context = Context {
193                cache: self.state.clone(),
194                http: self.http.clone(),
195                notifiers: self.waiters.clone(),
196                events: self.events.clone().unwrap(),
197            };
198
199            update_state(event.clone(), context.clone(), self.handler.clone()).await
200        });
201
202        if let Err(e) = wrapper.catch_unwind().await {
203            log::error!("{e:?}");
204        }
205    }
206}