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, ¶ms)?;
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}