Skip to content

Commit

Permalink
feat: use reqwest (closes #11, #12, #13)
Browse files Browse the repository at this point in the history
  • Loading branch information
null8626 committed Dec 15, 2023
1 parent 84363b9 commit dc40dcb
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 101 deletions.
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ exclude = [".gitattributes", ".github/", ".gitignore", "rustfmt.toml"]

[dependencies]
cfg-if = "1"
hyper = { version = "1", features = ["client", "http1"], optional = true }
hyper-tls = { version = "0.6", optional = true }
reqwest = { version = "0.11", features = ["json"], optional = true }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt", "sync", "time"], optional = true }
urlencoding = "2"

chrono = { version = "0.4", default-features = false, optional = true, features = ["serde"] }
serde_json = { version = "1", optional = true }
Expand All @@ -35,7 +33,7 @@ rustc-args = ["--cfg", "docsrs"]

[features]
default = ["api"]
api = ["chrono", "hyper", "hyper-tls", "serde_json"]
api = ["chrono", "reqwest", "serde_json"]
autoposter = ["api", "tokio"]

webhook = []
Expand Down
166 changes: 70 additions & 96 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ use crate::{
Error, Query, Result, Snowflake,
};
use core::{mem::transmute, str};
use hyper::{
body::{Body, Buf, HttpBody},
client::connect::HttpConnector,
http::{request, uri::Scheme, version::Version},
Request, Response, Uri,
};
use hyper_tls::HttpsConnector;
use reqwest::{RequestBuilder, Response, StatusCode, Version};
use serde::{de::DeserializeOwned, Deserialize};

cfg_if::cfg_if! {
Expand All @@ -25,75 +19,38 @@ cfg_if::cfg_if! {
}
}

fn build_uri(path: &str) -> Uri {
// SAFETY: the URI here should always be valid
unsafe {
Uri::builder()
.scheme(Scheme::HTTPS)
.authority("top.gg")
.path_and_query(path)
.build()
.unwrap_unchecked()
}
}

async fn retrieve_body(response: Response<Body>) -> Result<Vec<u8>> {
let content_length = response
.headers()
.get("Content-Length")
.and_then(|value| {
// SAFETY: Content-Length should always be valid ASCII
unsafe { str::from_utf8_unchecked(value.as_bytes()) }
.parse::<usize>()
.ok()
})
.unwrap_or_default();

let mut content = Vec::with_capacity(content_length);
let mut body = response.into_body();

while let Some(buf) = body.data().await {
match buf {
Ok(buf) => {
if buf.has_remaining() {
content.extend_from_slice(&buf);
}
}
Err(err) => return Err(Error::InternalClientError(err)),
};
}

Ok(content)
}

macro_rules! req(
($method:ident,$path:expr) => {
Request::$method(build_uri($path))
}
);

#[derive(Deserialize)]
#[serde(rename = "kebab-case")]
pub(crate) struct Ratelimit {
pub(crate) retry_after: u16,
}

macro_rules! api {
($e:expr) => {
concat!("https://top.gg/api", $e)
};

($e:expr,$($rest:tt)*) => {
format!(api!($e), $($rest)*)
};
}

pub(crate) struct InnerClient {
http: hyper::Client<HttpsConnector<HttpConnector>, Body>,
http: reqwest::Client,
token: String,
}

// this is implemented here because autoposter needs to access this function from a different thread.
impl InnerClient {
async fn send_inner(&self, builder: request::Builder, body: Vec<u8>) -> Result<Vec<u8>> {
async fn send_inner(&self, request: RequestBuilder, body: Vec<u8>) -> Result<Response> {
let mut auth = String::with_capacity(self.token.len() + 7);
auth.push_str("Bearer ");
auth.push_str(&self.token);

// SAFETY: the header keys and values should be valid ASCII
match self
.http
.request(unsafe {
.execute(unsafe {
builder
.header("Authorization", &auth)
.header("Connection", "close")
Expand All @@ -104,31 +61,27 @@ impl InnerClient {
"topgg (https://github.com/top-gg/rust-sdk) Rust",
)
.version(Version::HTTP_11)
.body(body.into())
.body(body)
.build()
.unwrap_unchecked()
})
.await
{
Ok(response) => {
let status = response.status().as_u16();
let status = response.status();

if status < 400 {
retrieve_body(response).await
if status.is_success() {
Ok(response)
} else {
Err(match status {
401 => panic!("Invalid Top.gg API token."),
404 => Error::NotFound,
429 => {
if let Ok(parsed) = retrieve_body(response).await {
if let Ok(ratelimit) = serde_json::from_slice::<Ratelimit>(&parsed) {
return Err(Error::Ratelimit {
retry_after: ratelimit.retry_after,
});
}
}

Error::InternalServerError
}
StatusCode::UNAUTHORIZED => panic!("Invalid Top.gg API token."),
StatusCode::NOT_FOUND => Error::NotFound,
StatusCode::TOO_MANY_REQUESTS => match response.json() {
Ok(Ratelimit { retry_after }) => Error::Ratelimit {
retry_after: ratelimit.retry_after,
},
_ => Error::InternalServerError,
},
_ => Error::InternalServerError,
})
}
Expand All @@ -139,24 +92,23 @@ impl InnerClient {
}

#[inline(always)]
pub(crate) async fn send<T>(&self, builder: request::Builder, body: Option<Vec<u8>>) -> Result<T>
pub(crate) async fn send<T>(&self, request: RequestBuilder, body: Option<Vec<u8>>) -> Result<T>
where
T: DeserializeOwned,
{
self
.send_inner(builder, body.unwrap_or_default())
.send_inner(request, body.unwrap_or_default())
.await
.and_then(|response| {
serde_json::from_slice(&response).map_err(|_| Error::InternalServerError)
})
.json()
.map_err(|_| Error::InternalServerError)
}

pub(crate) async fn post_stats(&self, new_stats: &Stats) -> Result<()> {
// SAFETY: no part of the Stats struct would cause an error in the serialization process.
let body = unsafe { serde_json::to_vec(new_stats).unwrap_unchecked() };

self
.send_inner(req!(post, "/bots/stats"), body)
.send_inner(self.http.post(api!("/bots/stats")), body)
.await
.map(|_| ())
}
Expand Down Expand Up @@ -185,7 +137,7 @@ impl Client {
#[inline(always)]
pub fn new(token: String) -> Self {
let inner = InnerClient {
http: hyper::Client::builder().build(HttpsConnector::new()),
http: reqwest::Client::new(),
token,
};

Expand Down Expand Up @@ -230,9 +182,13 @@ impl Client {
where
I: Snowflake,
{
let path = format!("/users/{}", id.as_snowflake());

self.inner.send(req!(get, &path), None).await
self
.inner
.send(
self.inner.http.get(api!("/users/{}", id.as_snowflake())),
None,
)
.await
}

/// Fetches a listed Discord bot from a Discord ID.
Expand Down Expand Up @@ -271,9 +227,13 @@ impl Client {
where
I: Snowflake,
{
let path = format!("/bots/{}", id.as_snowflake());

self.inner.send(req!(get, &path), None).await
self
.inner
.send(
self.inner.http.get(api!("/bots/{}", id.as_snowflake())),
None,
)
.await
}

/// Fetches your Discord bot's statistics.
Expand Down Expand Up @@ -303,7 +263,10 @@ impl Client {
/// ```
#[inline(always)]
pub async fn get_stats(&self) -> Result<Stats> {
self.inner.send(req!(get, "/bots/stats"), None).await
self
.inner
.send(self.inner.http.get(api!("/bots/stats")), None)
.await
}

/// Posts your Discord bot's statistics.
Expand Down Expand Up @@ -409,7 +372,10 @@ impl Client {
/// ```
#[inline(always)]
pub async fn get_voters(&self) -> Result<Vec<Voter>> {
self.inner.send(req!(get, "/bots/votes"), None).await
self
.inner
.send(self.inner.http.get(api!("/bots/votes")), None)
.await
}

/// Queries/searches through the [Top.gg](https://top.gg) database to look for matching listed Discord bots.
Expand Down Expand Up @@ -455,11 +421,15 @@ impl Client {
where
Q: Into<Query>,
{
let path = format!("/bots{}", query.into().query_string());

self
.inner
.send::<Bots>(req!(get, &path), None)
.send::<Bots>(
self
.inner
.http
.get(api!("/bots{}", query.into().query_string())),
None,
)
.await
.map(|res| res.results)
}
Expand Down Expand Up @@ -497,12 +467,16 @@ impl Client {
where
I: Snowflake,
{
let path = format!("/bots/votes?userId={}", user_id.as_snowflake());

// SAFETY: res.voted will always be either 0 or 1.
self
.inner
.send::<Voted>(req!(get, &path), None)
.send::<Voted>(
self
.inner
.http
.get("/bots/votes?userId={}", user_id.as_snowflake()),
None,
)
.await
.map(|res| unsafe { transmute(res.voted) })
}
Expand Down Expand Up @@ -537,7 +511,7 @@ impl Client {
pub async fn is_weekend(&self) -> Result<bool> {
self
.inner
.send::<IsWeekend>(req!(get, "/weekend"), None)
.send::<IsWeekend>(self.inner.http.get(api!("/weekend")), None)
.await
.map(|res| res.is_weekend)
}
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::error;
#[derive(Debug)]
pub enum Error {
/// An unexpected internal error coming from the client itself, preventing it from sending a request to the [Top.gg API](https://docs.top.gg).
InternalClientError(hyper::Error),
InternalClientError(reqwest::Error),

/// An unexpected error coming from [Top.gg](https://top.gg)'s servers themselves.
InternalServerError,
Expand Down

0 comments on commit dc40dcb

Please sign in to comment.