1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//! # dbl-rs
//!
//! Rust bindings for the [discordbots.org](https://discordbots.org) API.
//! ## Usage
//!
//! Add this to your `Cargo.toml`
//! ```toml
//! [dependencies]
//! dbl-rs = "0.1"
//! ```
//!
//! ## Example
//!
//! ```no_run
//! use dbl::types::ShardStats;
//! use dbl::Client;
//! use tokio::runtime::Runtime;
//!
//! fn main() {
//!     let token = match std::env::var("DBL_TOKEN") {
//!         Ok(token) => token,
//!         _ => panic!("missing token"),
//!     };
//!
//!     let mut rt = Runtime::new().expect("failed rt");
//!     let client = Client::new(token).expect("failed client");
//!
//!     let bot = 565_030_624_499_466_240;
//!     let stats = ShardStats::Cumulative {
//!         server_count: 1234,
//!         shard_count: None,
//!     };
//!
//!     let task = client.update_stats(bot, stats);
//!
//!     match rt.block_on(task) {
//!         Ok(_) => println!("Update successful"),
//!         Err(e) => eprintln!("{}", e),
//!     }
//! }
//! ```
#![doc(html_root_url = "https://docs.rs/dbl-rs/0.1.1")]
#![deny(rust_2018_idioms)]

use std::sync::Arc;

use futures::future::{self, Future};
use reqwest::header::AUTHORIZATION;
use reqwest::r#async::{Client as ReqwestClient, Response};
use reqwest::{Method, StatusCode};
use url::Url;

macro_rules! api {
    ($e:expr) => {
        concat!("https://discordbots.org/api", $e)
    };
    ($e:expr, $($rest:tt)*) => {
        format!(api!($e), $($rest)*)
    };
}

mod error;
pub mod types;
pub mod widget;

pub use error::Error;

use types::*;

type BoxFuture<T> = Box<dyn Future<Item = T, Error = Error> + Send>;

/// Endpoint interface to Discord Bot List API.
pub struct Client {
    client: Arc<ReqwestClient>,
    token: String,
}

impl Client {
    /// Constructs a new `Client`.
    pub fn new(token: String) -> Result<Self, Error> {
        let client = Arc::new(ReqwestClient::builder().build().map_err(error::from)?);
        Ok(Client { client, token })
    }

    /// Constructs a new `Client` with a `reqwest` client.
    pub fn new_with(client: Arc<ReqwestClient>, token: String) -> Self {
        Client { client, token }
    }

    /// Get information about a specific bot.
    pub fn get<T>(&self, bot: T) -> impl Future<Item = Bot, Error = Error>
    where
        T: Into<BotId>,
    {
        self.get2(&api!("/bots/{}", bot.into()))
            .and_then(|mut resp| resp.json().map_err(error::from))
    }

    /// Search for bots.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use dbl::types::Filter;
    ///
    /// let filter = Filter::new().search("lib:serenity foobar");
    /// ```
    pub fn search(&self, filter: &Filter) -> impl Future<Item = Listing, Error = Error> {
        let url = match Url::parse_with_params(&api!("/bots"), &filter.0) {
            Ok(url) => url,
            Err(e) => return future::Either::A(future::err(Error::Url(e))),
        };
        future::Either::B(
            self.get2(&url.to_string())
                .and_then(|mut resp| resp.json().map_err(error::from)),
        )
    }

    /// Get the stats of a bot.
    pub fn stats<T>(&self, bot: T) -> impl Future<Item = Stats, Error = Error>
    where
        T: Into<BotId>,
    {
        self.get2(&api!("/bots/{}/stats", bot.into()))
            .and_then(|mut resp| resp.json().map_err(error::from))
    }

    /// Update the stats of a bot.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use dbl::types::ShardStats;
    ///
    /// let new_stats = ShardStats::Cumulative {
    ///     server_count: 1234,
    ///     shard_count: None,
    /// };
    /// ```
    pub fn update_stats<T>(&self, bot: T, stats: ShardStats) -> impl Future<Item = (), Error = Error>
    where
        T: Into<BotId>,
    {
        self.post(&api!("/bots/{}/stats", bot.into()), Some(stats))
            .map(|_| ())
    }

    /// Get the last 1000 votes for a bot.
    pub fn votes<T>(&self, bot: T) -> impl Future<Item = Vec<User>, Error = Error>
    where
        T: Into<BotId>,
    {
        self.get2(&api!("/bots/{}/votes", bot.into()))
            .and_then(|mut resp| resp.json().map_err(error::from))
    }

    /// Check if a user has voted for a bot in the past 24 hours.
    pub fn has_voted<T, U>(&self, bot: T, user: U) -> impl Future<Item = bool, Error = Error>
    where
        T: Into<BotId>,
        U: Into<UserId>,
    {
        self.get2(&api!("/bots/{}/check?userId={}", bot.into(), user.into()))
            .and_then(|mut resp| resp.json::<UserVoted>().map_err(error::from))
            .map(|v| v.voted > 0)
    }

    /// Get information about a user.
    pub fn user<T>(&self, user: T) -> impl Future<Item = DetailedUser, Error = Error>
    where
        T: Into<UserId>,
    {
        self.get2(&api!("/users/{}", user.into()))
            .and_then(|mut resp| resp.json().map_err(error::from))
    }

    fn request<T>(
        &self,
        method: Method,
        url: &str,
        data: Option<T>,
    ) -> impl Future<Item = Response, Error = Error>
    where
        T: ::serde::ser::Serialize + Sized,
    {
        let mut req = self
            .client
            .request(method, url)
            .header(AUTHORIZATION, &*self.token);

        if let Some(data) = data {
            req = req.json(&data);
        }

        req.send()
            .map_err(error::from)
            .and_then(|mut resp| -> BoxFuture<Response> {
                match resp.status() {
                    StatusCode::TOO_MANY_REQUESTS => Box::new(
                        resp.json::<Ratelimit>()
                            .map_err(error::from)
                            .and_then(|r| Err(error::ratelimit(r.retry_after))),
                    ),
                    _ => Box::new(future::result(resp.error_for_status().map_err(error::from))),
                }
            })
    }

    fn get2(&self, url: &str) -> impl Future<Item = Response, Error = Error> {
        self.request(Method::GET, url, None::<()>)
    }

    fn post<T>(&self, url: &str, data: Option<T>) -> impl Future<Item = Response, Error = Error>
    where
        T: ::serde::Serialize + Sized,
    {
        self.request(Method::POST, url, data)
    }
}