dbl/
lib.rs

1//! # dbl-rs
2//!
3//! Rust bindings for the [top.gg](https://top.gg) / discordbots.org API.
4//! ## Usage
5//!
6//! Add this to your `Cargo.toml`
7//! ```toml
8//! [dependencies]
9//! dbl-rs = "0.3"
10//! ```
11//!
12//! ## Example
13//!
14//! ```no_run
15//! use dbl::types::ShardStats;
16//! use dbl::Client;
17//!
18//! #[tokio::main]
19//! async fn main() {
20//!     let token = match std::env::var("DBL_TOKEN") {
21//!         Ok(token) => token,
22//!         _ => panic!("missing token"),
23//!     };
24//!
25//!     let client = Client::new(token).expect("failed client");
26//!
27//!     let bot = 565_030_624_499_466_240;
28//!     let stats = ShardStats::Cumulative {
29//!         server_count: 1234,
30//!         shard_count: None,
31//!     };
32//!
33//!     match client.update_stats(bot, stats).await {
34//!         Ok(_) => println!("Update successful"),
35//!         Err(e) => eprintln!("{}", e),
36//!     }
37//! }
38//! ```
39#![doc(html_root_url = "https://docs.rs/dbl-rs/0.4.0")]
40#![deny(rust_2018_idioms)]
41
42use reqwest::header::AUTHORIZATION;
43use reqwest::{Client as ReqwestClient, Response};
44use reqwest::{Method, StatusCode};
45use url::Url;
46
47macro_rules! api {
48    ($e:expr) => {
49        concat!("https://top.gg/api", $e)
50    };
51    ($e:expr, $($rest:tt)*) => {
52        format!(api!($e), $($rest)*)
53    };
54}
55
56mod error;
57pub mod types;
58pub mod widget;
59
60pub use error::Error;
61
62use types::*;
63
64/// Endpoint interface to Discord Bot List API.
65#[derive(Clone)]
66pub struct Client {
67    client: ReqwestClient,
68    token: String,
69}
70
71impl Client {
72    /// Constructs a new `Client`.
73    pub fn new(token: String) -> Result<Self, Error> {
74        let client = ReqwestClient::builder().build().map_err(error::from)?;
75        Ok(Client { client, token })
76    }
77
78    /// Constructs a new `Client` with a `reqwest` client.
79    pub fn new_with_client(client: ReqwestClient, token: String) -> Self {
80        Client { client, token }
81    }
82
83    /// Get information about a specific bot.
84    pub async fn get<T>(&self, bot: T) -> Result<Bot, Error>
85    where
86        T: Into<BotId>,
87    {
88        let url = api!("/bots/{}", bot.into());
89        get(self, url).await
90    }
91
92    /// Search for bots.
93    ///
94    /// # Example
95    ///
96    /// ```no_run
97    /// use dbl::types::Filter;
98    ///
99    /// let filter = Filter::new().search("lib:serenity foobar");
100    /// ```
101    pub async fn search(&self, filter: &Filter) -> Result<Listing, Error> {
102        let url = Url::parse_with_params(api!("/bots"), &filter.0).map_err(Error::Url)?;
103        get(self, url.to_string()).await
104    }
105
106    /// Get the stats of a bot.
107    pub async fn stats<T>(&self, bot: T) -> Result<Stats, Error>
108    where
109        T: Into<BotId>,
110    {
111        let url = api!("/bots/{}/stats", bot.into());
112        get(self, url).await
113    }
114
115    /// Update the stats of a bot.
116    ///
117    /// # Example
118    ///
119    /// ```no_run
120    /// use dbl::types::ShardStats;
121    ///
122    /// let new_stats = ShardStats::Cumulative {
123    ///     server_count: 1234,
124    ///     shard_count: None,
125    /// };
126    /// ```
127    pub async fn update_stats<T>(&self, bot: T, stats: ShardStats) -> Result<(), Error>
128    where
129        T: Into<BotId>,
130    {
131        let url = api!("/bots/{}/stats", bot.into());
132        post(self, url, Some(stats)).await
133    }
134
135    /// Get the last 1000 votes for a bot.
136    pub async fn votes<T>(&self, bot: T) -> Result<Vec<User>, Error>
137    where
138        T: Into<BotId>,
139    {
140        let url = api!("/bots/{}/votes", bot.into());
141        get(self, url).await
142    }
143
144    /// Check if a user has voted for a bot in the past 24 hours.
145    pub async fn has_voted<T, U>(&self, bot: T, user: U) -> Result<bool, Error>
146    where
147        T: Into<BotId>,
148        U: Into<UserId>,
149    {
150        let bot = bot.into();
151        let user = user.into();
152        let url = api!("/bots/{}/check?userId={}", bot, user);
153        let v: UserVoted = get(self, url).await?;
154        Ok(v.voted > 0)
155    }
156
157    /// Get information about a user.
158    pub async fn user<T>(&self, user: T) -> Result<DetailedUser, Error>
159    where
160        T: Into<UserId>,
161    {
162        let url = api!("/users/{}", user.into());
163        get(self, url).await
164    }
165}
166
167async fn request<T>(
168    client: &Client,
169    method: Method,
170    url: String,
171    data: Option<T>,
172) -> Result<Response, Error>
173where
174    T: serde::Serialize + Sized,
175{
176    let mut req = client
177        .client
178        .request(method, &url)
179        .header(AUTHORIZATION, &*client.token);
180
181    if let Some(data) = data {
182        req = req.json(&data);
183    }
184
185    let resp = match req.send().await {
186        Ok(resp) => resp,
187        Err(e) => return Err(error::from(e)),
188    };
189    match resp.status() {
190        StatusCode::TOO_MANY_REQUESTS => {
191            let rl = match resp.json::<Ratelimit>().await {
192                Ok(rl) => rl,
193                Err(e) => return Err(error::from(e)),
194            };
195            Err(error::ratelimit(rl.retry_after))
196        }
197        _ => resp.error_for_status().map_err(error::from),
198    }
199}
200
201async fn get<T>(client: &Client, url: String) -> Result<T, Error>
202where
203    T: serde::de::DeserializeOwned + Sized,
204{
205    let resp = request(client, Method::GET, url, None::<()>).await?;
206    match resp.json().await {
207        Ok(data) => Ok(data),
208        Err(e) => Err(error::from(e)),
209    }
210}
211
212async fn post<T>(client: &Client, url: String, data: Option<T>) -> Result<(), Error>
213where
214    T: serde::Serialize + Sized,
215{
216    request(client, Method::POST, url, data).await?;
217    Ok(())
218}