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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
use crate::{
bot::{Bot, IsWeekend},
user::{User, Voted, Voter},
util, Error, Result, Snowflake, Stats,
};
use reqwest::{header, IntoUrl, Method, Response, StatusCode, Version};
use serde::{de::DeserializeOwned, Deserialize};
cfg_if::cfg_if! {
if #[cfg(feature = "autoposter")] {
use crate::autoposter;
use std::sync::Arc;
type SyncedClient = Arc<InnerClient>;
} else {
type SyncedClient = InnerClient;
}
}
#[derive(Deserialize)]
#[serde(rename = "kebab-case")]
struct Ratelimit {
retry_after: u16,
}
macro_rules! api {
($e:literal) => {
concat!("https://top.gg/api", $e)
};
($e:literal, $($rest:tt)*) => {
format!(api!($e), $($rest)*)
};
}
#[derive(Debug)]
pub struct InnerClient {
http: reqwest::Client,
token: String,
}
// this is implemented here because autoposter needs to access this struct from a different thread.
impl InnerClient {
pub(crate) fn new(mut token: String) -> Self {
token.insert_str(0, "Bearer ");
Self {
http: reqwest::Client::new(),
token,
}
}
async fn send_inner(&self, method: Method, url: impl IntoUrl, body: Vec<u8>) -> Result<Response> {
match self
.http
.execute(
self
.http
.request(method, url)
.header(header::AUTHORIZATION, &self.token)
.header(header::CONNECTION, "close")
.header(header::CONTENT_LENGTH, body.len())
.header(header::CONTENT_TYPE, "application/json")
.header(
header::USER_AGENT,
"topgg (https://github.com/top-gg/rust-sdk) Rust",
)
.version(Version::HTTP_11)
.body(body)
.build()
.unwrap(),
)
.await
{
Ok(response) => {
let status = response.status();
if status.is_success() {
Ok(response)
} else {
Err(match status {
StatusCode::UNAUTHORIZED => panic!("Invalid Top.gg API token."),
StatusCode::NOT_FOUND => Error::NotFound,
StatusCode::TOO_MANY_REQUESTS => match util::parse_json::<Ratelimit>(response).await {
Ok(ratelimit) => Error::Ratelimit {
retry_after: ratelimit.retry_after,
},
_ => Error::InternalServerError,
},
_ => Error::InternalServerError,
})
}
}
Err(err) => Err(Error::InternalClientError(err)),
}
}
#[inline(always)]
pub(crate) async fn send<T>(
&self,
method: Method,
url: impl IntoUrl,
body: Option<Vec<u8>>,
) -> Result<T>
where
T: DeserializeOwned,
{
match self.send_inner(method, url, body.unwrap_or_default()).await {
Ok(response) => util::parse_json(response).await,
Err(err) => Err(err),
}
}
pub(crate) async fn post_stats(&self, new_stats: &Stats) -> Result<()> {
self
.send_inner(
Method::POST,
api!("/bots/stats"),
serde_json::to_vec(new_stats).unwrap(),
)
.await
.map(|_| ())
}
}
/// A struct representing a [Top.gg API](https://docs.top.gg) client instance.
#[must_use]
#[derive(Debug)]
pub struct Client {
inner: SyncedClient,
}
impl Client {
/// Creates a brand new client instance from a [Top.gg](https://top.gg) token.
///
/// To get your [Top.gg](https://top.gg) token, [view this tutorial](https://github.com/top-gg/rust-sdk/assets/60427892/d2df5bd3-bc48-464c-b878-a04121727bff).
#[inline(always)]
pub fn new(token: String) -> Self {
let inner = InnerClient::new(token);
#[cfg(feature = "autoposter")]
let inner = Arc::new(inner);
Self { inner }
}
/// Fetches a user from a Discord ID.
///
/// # Panics
///
/// Panics if any of the following conditions are met:
/// - The ID argument is a string but not numeric
/// - The client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The requested user does not exist ([`NotFound`][crate::Error::NotFound])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
pub async fn get_user<I>(&self, id: I) -> Result<User>
where
I: Snowflake,
{
self
.inner
.send(Method::GET, api!("/users/{}", id.as_snowflake()), None)
.await
}
/// Fetches a listed Discord bot from a Discord ID.
///
/// # Panics
///
/// Panics if any of the following conditions are met:
/// - The ID argument is a string but not numeric
/// - The client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The requested Discord bot is not listed on [Top.gg](https://top.gg) ([`NotFound`][crate::Error::NotFound])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
pub async fn get_bot<I>(&self, id: I) -> Result<Bot>
where
I: Snowflake,
{
self
.inner
.send(Method::GET, api!("/bots/{}", id.as_snowflake()), None)
.await
}
/// Fetches your Discord bot's statistics.
///
/// # Panics
///
/// Panics if the client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
pub async fn get_stats(&self) -> Result<Stats> {
self
.inner
.send(Method::GET, api!("/bots/stats"), None)
.await
}
/// Posts your Discord bot's statistics.
///
/// # Panics
///
/// Panics if the client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
#[inline(always)]
pub async fn post_stats(&self, new_stats: Stats) -> Result<()> {
self.inner.post_stats(&new_stats).await
}
/// Fetches your Discord bot's last 1000 voters.
///
/// # Panics
///
/// Panics if the client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
pub async fn get_voters(&self) -> Result<Vec<Voter>> {
self
.inner
.send(Method::GET, api!("/bots/votes"), None)
.await
}
/// Checks if the specified user has voted your Discord bot.
///
/// # Panics
///
/// Panics if any of the following conditions are met:
/// - The user ID argument is a string and it's not a valid ID (expected things like `"123456789"`)
/// - The client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
pub async fn has_voted<I>(&self, user_id: I) -> Result<bool>
where
I: Snowflake,
{
self
.inner
.send::<Voted>(
Method::GET,
api!("/bots/check?userId={}", user_id.as_snowflake()),
None,
)
.await
.map(|res| res.voted != 0)
}
/// Checks if the weekend multiplier is active.
///
/// # Panics
///
/// Panics if the client uses an invalid [Top.gg API](https://docs.top.gg) token (unauthorized)
///
/// # Errors
///
/// Errors if any of the following conditions are met:
/// - An internal error from the client itself preventing it from sending a HTTP request to [Top.gg](https://top.gg) ([`InternalClientError`][crate::Error::InternalClientError])
/// - An unexpected response from the [Top.gg](https://top.gg) servers ([`InternalServerError`][crate::Error::InternalServerError])
/// - The client is being ratelimited from sending more HTTP requests ([`Ratelimit`][crate::Error::Ratelimit])
pub async fn is_weekend(&self) -> Result<bool> {
self
.inner
.send::<IsWeekend>(Method::GET, api!("/weekend"), None)
.await
.map(|res| res.is_weekend)
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "autoposter")] {
impl autoposter::AsClientSealed for Client {
#[inline(always)]
fn as_client(&self) -> Arc<InnerClient> {
Arc::clone(&self.inner)
}
}
impl autoposter::AsClient for Client {}
}
}