top_gg/client/
mod.rs

1//! The client implementation around the API, and finalizer requests for
2//! configuring optional parameters.
3//!
4//! Refer to the [`Client`] struct for more information and examples.
5//!
6//! [`Client`]: struct.Client.html
7
8pub mod request;
9
10use crate::{
11    endpoints,
12    error::{ChunkingText, Deserializing, InvalidHeaderValue, InvalidUrl, Result, TokenMissing},
13    model::*,
14};
15use request::SearchBots;
16use reqwest::{
17    header::{HeaderValue, AUTHORIZATION},
18    Client as HttpClient, Url,
19};
20use serde::de::DeserializeOwned;
21use snafu::{OptionExt, ResultExt};
22use std::sync::Arc;
23
24#[derive(Debug)]
25struct ClientRef {
26    http: Arc<HttpClient>,
27    token: Option<String>,
28}
29
30/// Struct which defines the methods necessary to interact with the service.
31#[derive(Clone, Debug)]
32pub struct Client(Arc<ClientRef>);
33
34impl Client {
35    /// Creates a new client to interact with the API.
36    ///
37    /// This accepts an existing reqwest Client so a single HTTP client may be
38    /// shared across your application.
39    ///
40    /// This method doesn't require authentication.
41    ///
42    /// # Examples
43    ///
44    /// Create a new API client:
45    ///
46    /// ```no_run
47    /// use reqwest::Client as HttpClient;
48    /// use top_gg::Client;
49    ///
50    /// let http_client = HttpClient::new();
51    /// let client = Client::new(http_client, None);
52    /// ```
53    pub fn new(http_client: impl Into<Arc<HttpClient>>, token: impl Into<Option<String>>) -> Self {
54        Self(Arc::new(ClientRef {
55            http: http_client.into(),
56            token: token.into(),
57        }))
58    }
59
60    /// Retrieves information about a bot.
61    ///
62    /// This method doesn't require authentication.
63    ///
64    /// # Examples
65    ///
66    /// ```no_run
67    /// use reqwest::Client as HttpClient;
68    /// use top_gg::Client;
69    ///
70    /// # #[tokio::main]
71    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
72    /// let http_client = HttpClient::new();
73    /// let client = Client::new(http_client, None);
74    ///
75    /// let bot = client.get_bot(270198738570444801).await?;
76    ///
77    /// println!("Bot's name: {}", bot.username);
78    /// # Ok(()) }
79    /// ```
80    ///
81    /// # Errors
82    ///
83    /// Returns [`Error::ChunkingText`] when the response body couldn't be
84    /// chunked as a valid UTF-8 string.
85    ///
86    /// Returns [`Error::Deserializing`] if there was an issue deserializing the
87    /// response body.
88    ///
89    /// Returns [`Error::Request`] if there was an issue building the request.
90    /// This probably won't happen.
91    ///
92    /// [`Error::ChunkingText`]: ../error/enum.Error.html#variant.ChunkingText
93    /// [`Error::Deserializing`]: ../error/enum.Error.html#variant.Deserializing
94    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
95    pub async fn get_bot(&self, user_id: u64) -> Result<Bot> {
96        let url = url(endpoints::bot(user_id))?;
97
98        self.get(url).await
99    }
100
101    /// Retrieves a list of bots via a search.
102    ///
103    /// This method doesn't require authentication.
104    ///
105    /// # Examples
106    ///
107    /// Get 500 bots, skipping the first 250 bots:
108    ///
109    /// ```no_run
110    /// use reqwest::Client as HttpClient;
111    /// use top_gg::Client;
112    ///
113    /// # #[tokio::main]
114    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
115    /// let http_client = HttpClient::new();
116    /// let client = Client::new(http_client, None);
117    ///
118    /// let bots = client.get_bots().limit(500).offset(250).await?;
119    ///
120    /// println!("Got {} bots", bots.total);
121    /// # Ok(()) }
122    /// ```
123    ///
124    /// # Errors
125    ///
126    /// Returns [`Error::ChunkingText`] when the response body couldn't be
127    /// chunked as a valid UTF-8 string.
128    ///
129    /// Returns [`Error::Deserializing`] if there was an issue deserializing the
130    /// response body.
131    ///
132    /// Returns [`Error::Request`] if there was an issue building the request.
133    /// This probably won't happen.
134    ///
135    /// [`Error::ChunkingText`]: ../error/enum.Error.html#variant.ChunkingText
136    /// [`Error::Deserializing`]: ../error/enum.Error.html#variant.Deserializing
137    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
138    pub fn get_bots(&self) -> SearchBots<'_> {
139        SearchBots::new(self)
140    }
141
142    /// Retrieves information about a bot's specific stats.
143    ///
144    /// This method doesn't require authentication.
145    ///
146    /// # Examples
147    ///
148    /// ```no_run
149    /// use reqwest::Client as HttpClient;
150    /// use top_gg::Client;
151    ///
152    /// # #[tokio::main]
153    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
154    /// let http_client = HttpClient::new();
155    /// let client = Client::new(http_client, None);
156    ///
157    /// let bot = client.get_bot_stats(270_198_738_570_444_801).await?;
158    ///
159    /// if let Some(server_count) = bot.server_count {
160    ///     println!("This bot is in {} servers", server_count);
161    /// }
162    /// # Ok(()) }
163    /// ```
164    ///
165    /// # Errors
166    ///
167    /// Returns [`Error::ChunkingText`] when the response body couldn't be
168    /// chunked as a valid UTF-8 string.
169    ///
170    /// Returns [`Error::Deserializing`] if there was an issue deserializing the
171    /// response body.
172    ///
173    /// Returns [`Error::Request`] if there was an issue building the request.
174    /// This probably won't happen.
175    ///
176    /// [`Error::ChunkingText`]: ../error/enum.Error.html#variant.ChunkingText
177    /// [`Error::Deserializing`]: ../error/enum.Error.html#variant.Deserializing
178    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
179    pub async fn get_bot_stats(&self, user_id: u64) -> Result<BotStats> {
180        let url = url(endpoints::bot_stats(user_id))?;
181
182        self.get(url).await
183    }
184
185    /// Retrieve whether a user has upvoted a bot in the last 24 hours.
186    ///
187    /// You can use this if your bot has over 1000 votes.
188    ///
189    /// This method requires authentication.
190    ///
191    /// # Examples
192    ///
193    /// ```no_run
194    /// use reqwest::Client as HttpClient;
195    /// use std::env;
196    /// use top_gg::Client;
197    ///
198    /// # #[tokio::main]
199    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
200    /// let http_client = HttpClient::new();
201    /// let token = env::var("TOP_GG_TOKEN")?;
202    /// let client = Client::new(http_client, token);
203    ///
204    /// let voted = client.get_bot_vote_check(270_198_738_570_444_801, 114_941_315_417_899_012).await?;
205    ///
206    /// if voted {
207    ///     println!("This user voted");
208    /// } else {
209    ///     println!("This user has not voted");
210    /// }
211    /// # Ok(()) }
212    /// ```
213    ///
214    /// # Errors
215    ///
216    /// Returns [`Error::ChunkingText`] when the response body couldn't be
217    /// chunked as a valid UTF-8 string.
218    ///
219    /// Returns [`Error::Deserializing`] if there was an issue deserializing the
220    /// response body.
221    ///
222    /// Returns [`Error::Request`] if there was an issue building the request.
223    /// This probably won't happen.
224    ///
225    /// Returns [`Error::TokenMissing`] if a token is needed for authentication
226    /// but it wasn't present.
227    ///
228    /// [`Error::ChunkingText`]: ../error/enum.Error.html#variant.ChunkingText
229    /// [`Error::Deserializing`]: ../error/enum.Error.html#variant.Deserializing
230    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
231    /// [`Error::TokenMissing`]: ../error/enum.Error.html#variant.TokenMissing
232    pub async fn get_bot_vote_check(&self, bot_id: u64, user_id: u64) -> Result<bool> {
233        let token = self.0.token.as_ref().context(TokenMissing)?;
234
235        let path = endpoints::bot_vote_check(bot_id, user_id);
236        let user_id = user_id.to_string();
237        let params = [("userId", user_id.as_ref())];
238        let url = url_params(path, &params)?;
239        let header_value = auth(token)?;
240
241        let text = self
242            .0
243            .http
244            .get(url)
245            .header(AUTHORIZATION, header_value)
246            .send()
247            .await?
248            .text()
249            .await
250            .context(ChunkingText)?;
251        let body = deser::<ResponseUserVoted>(text)?;
252
253        Ok(body.voted == 1)
254    }
255
256    /// Retrieves information to see who has upvoted a bot.
257    ///
258    /// This method does not require authentication.
259    ///
260    /// **Note**: If your bot has over 1000 votes per month, then this can not
261    /// be used. Webhooks must instead be used.
262    ///
263    /// # Examples
264    ///
265    /// ```no_run
266    /// use reqwest::Client as HttpClient;
267    /// use top_gg::{
268    ///     model::BotVotes,
269    ///     Client,
270    /// };
271    ///
272    /// # #[tokio::main]
273    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
274    /// let http_client = HttpClient::new();
275    /// let client = Client::new(http_client, None);
276    ///
277    /// let votes = client.get_bot_votes(270_198_738_570_444_801).await?;
278    ///
279    /// match votes {
280    ///     BotVotes::Ids(ids) => println!("There are {} votes", ids.len()),
281    ///     BotVotes::Users(users) => println!("There are {} votes", users.len()),
282    /// }
283    /// # Ok(()) }
284    /// ```
285    ///
286    /// # Errors
287    ///
288    /// Returns [`Error::ChunkingText`] when the response body couldn't be
289    /// chunked as a valid UTF-8 string.
290    ///
291    /// Returns [`Error::Deserializing`] if there was an issue deserializing the
292    /// response body.
293    ///
294    /// Returns [`Error::Request`] if there was an issue building the request.
295    /// This probably won't happen.
296    ///
297    /// [`Error::ChunkingText`]: ../error/enum.Error.html#variant.ChunkingText
298    /// [`Error::Deserializing`]: ../error/enum.Error.html#variant.Deserializing
299    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
300    pub async fn get_bot_votes(&self, bot_id: u64) -> Result<BotVotes> {
301        let url = url(endpoints::bot_votes(bot_id))?;
302
303        self.get(url).await
304    }
305
306    /// Retrieves information about a user.
307    ///
308    /// This method doesn't require authentication.
309    ///
310    /// # Examples
311    ///
312    /// ```no_run
313    /// use reqwest::Client as HttpClient;
314    /// use top_gg::Client;
315    ///
316    /// # #[tokio::main]
317    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
318    /// let http_client = HttpClient::new();
319    /// let client = Client::new(http_client, None);
320    ///
321    /// let user = client.get_user(114_941_315_417_899_012).await?;
322    ///
323    /// println!("The user's name is {}", user.username);
324    /// # Ok(()) }
325    /// ```
326    ///
327    /// # Errors
328    ///
329    /// Returns [`Error::ChunkingText`] when the response body couldn't be
330    /// chunked as a valid UTF-8 string.
331    ///
332    /// Returns [`Error::Deserializing`] if there was an issue deserializing the
333    /// response body.
334    ///
335    /// Returns [`Error::Request`] if there was an issue building the request.
336    /// This probably won't happen.
337    ///
338    /// [`Error::ChunkingText`]: ../error/enum.Error.html#variant.ChunkingText
339    /// [`Error::Deserializing`]: ../error/enum.Error.html#variant.Deserializing
340    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
341    pub async fn get_user(&self, user_id: u64) -> Result<User> {
342        let url = url(endpoints::user(user_id))?;
343
344        self.get(url).await
345    }
346
347    /// Posts a bot's shard stats.
348    ///
349    /// This method requires authentication.
350    ///
351    /// # Examples
352    ///
353    /// ```no_run
354    /// use reqwest::Client as HttpClient;
355    /// use std::env;
356    /// use top_gg::{
357    ///     model::ShardStats,
358    ///     Client,
359    /// };
360    ///
361    /// # #[tokio::main]
362    /// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
363    /// let http_client = HttpClient::new();
364    /// let token = env::var("TOP_GG_TOKEN")?;
365    /// let client = Client::new(http_client, token);
366    ///
367    /// let shard_stats = ShardStats::Shard {
368    ///     guild_count: 1123,
369    ///     shard_count: 10,
370    ///     shard_id: 6,
371    /// };
372    ///
373    /// client.post_stats(270_198_738_570_444_801, &shard_stats).await?;
374    /// # Ok(()) }
375    /// ```
376    ///
377    /// # Errors
378    ///
379    /// Returns [`Error::Request`] if there was an issue building the request.
380    /// This probably won't happen.
381    ///
382    /// Returns [`Error::TokenMissing`] if a token is needed for authentication
383    /// but it wasn't present.
384    ///
385    /// [`Error::Request`]: ../error/enum.Error.html#variant.Request
386    /// [`Error::TokenMissing`]: ../error/enum.Error.html#variant.TokenMissing
387    pub async fn post_stats<'a>(&'a self, bot_id: u64, stats: &'a ShardStats) -> Result<()> {
388        let token = self.0.token.as_ref().context(TokenMissing)?;
389        let url = url(endpoints::bot_stats(bot_id))?;
390        let header_value = auth(token)?;
391
392        self.0
393            .http
394            .post(url)
395            .header(AUTHORIZATION, header_value)
396            .json(stats)
397            .send()
398            .await?;
399
400        Ok(())
401    }
402
403    async fn get<T: DeserializeOwned>(&self, url: Url) -> Result<T> {
404        let text = self
405            .0
406            .http
407            .get(url)
408            .send()
409            .await?
410            .text()
411            .await
412            .context(ChunkingText)?;
413
414        deser(text)
415    }
416}
417
418impl From<(Arc<HttpClient>, Option<String>)> for Client {
419    fn from((client, token): (Arc<HttpClient>, Option<String>)) -> Self {
420        Self::new(client, token)
421    }
422}
423
424fn auth(token: &str) -> Result<HeaderValue> {
425    HeaderValue::from_str(token).with_context(|| InvalidHeaderValue {
426        name: AUTHORIZATION,
427        value: token.to_owned(),
428    })
429}
430
431fn deser<T: DeserializeOwned>(text: String) -> Result<T> {
432    serde_json::from_str(&text).with_context(|| Deserializing { text })
433}
434
435fn url(uri: String) -> Result<Url> {
436    Url::parse(&uri).with_context(|| InvalidUrl { uri })
437}
438
439fn url_params<'a>(uri: String, params: &[(&'a str, &'a str)]) -> Result<Url> {
440    Url::parse_with_params(&uri, params.iter()).with_context(|| InvalidUrl { uri })
441}