diff --git a/Cargo.toml b/Cargo.toml index efba190..bf2c5e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } @@ -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 = [] diff --git a/src/client.rs b/src/client.rs index 70020e4..c89ccc1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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! { @@ -25,67 +19,30 @@ 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) -> Result> { - 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::() - .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, 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) -> Result> { + async fn send_inner(&self, request: RequestBuilder, body: Vec) -> Result { let mut auth = String::with_capacity(self.token.len() + 7); auth.push_str("Bearer "); auth.push_str(&self.token); @@ -93,7 +50,7 @@ impl InnerClient { // SAFETY: the header keys and values should be valid ASCII match self .http - .request(unsafe { + .execute(unsafe { builder .header("Authorization", &auth) .header("Connection", "close") @@ -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::(&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, }) } @@ -139,16 +92,15 @@ impl InnerClient { } #[inline(always)] - pub(crate) async fn send(&self, builder: request::Builder, body: Option>) -> Result + pub(crate) async fn send(&self, request: RequestBuilder, body: Option>) -> Result 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<()> { @@ -156,7 +108,7 @@ impl InnerClient { 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(|_| ()) } @@ -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, }; @@ -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. @@ -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. @@ -303,7 +263,10 @@ impl Client { /// ``` #[inline(always)] pub async fn get_stats(&self) -> Result { - 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. @@ -409,7 +372,10 @@ impl Client { /// ``` #[inline(always)] pub async fn get_voters(&self) -> Result> { - 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. @@ -455,11 +421,15 @@ impl Client { where Q: Into, { - let path = format!("/bots{}", query.into().query_string()); - self .inner - .send::(req!(get, &path), None) + .send::( + self + .inner + .http + .get(api!("/bots{}", query.into().query_string())), + None, + ) .await .map(|res| res.results) } @@ -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::(req!(get, &path), None) + .send::( + self + .inner + .http + .get("/bots/votes?userId={}", user_id.as_snowflake()), + None, + ) .await .map(|res| unsafe { transmute(res.voted) }) } @@ -537,7 +511,7 @@ impl Client { pub async fn is_weekend(&self) -> Result { self .inner - .send::(req!(get, "/weekend"), None) + .send::(self.inner.http.get(api!("/weekend")), None) .await .map(|res| res.is_weekend) } diff --git a/src/error.rs b/src/error.rs index c04a145..cd2df7f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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,